diff --git a/.github/workflows/deploy_web.yml b/.github/workflows/deploy_web.yml index dc94c3c30..df59a75b2 100644 --- a/.github/workflows/deploy_web.yml +++ b/.github/workflows/deploy_web.yml @@ -1,5 +1,9 @@ name: Build Web & Deploy to GH on: [push,pull_request] +permissions: + contents: write + pull-requests: write + issues: write jobs: build-and-deploy: runs-on: ubuntu-latest @@ -53,3 +57,12 @@ jobs: target-folder: branch/dev force: false clean: false + - name: Deploy to rcheevos site 🚀 + if: github.ref == 'refs/heads/rcheevos' + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages # The branch the action should deploy to. + folder: build/bin # The folder the action should deploy. + target-folder: branch/rcheevos + force: false + clean: false \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 6926bf528..ddfd7ea02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,8 @@ set(LINK_LIBS "") # Set compile_commands.json generation for clangd on by default set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +option(ENABLE_RETRO_ACHIEVEMENTS "Enable Retro Achievements" ON) + if (EMSCRIPTEN) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s ELIMINATE\_DUPLICATE\_FUNCTIONS=1 -s ENVIRONMENT=web -s ASSERTIONS=0 -s WASM=1 -DSE_PLATFORM_WEB --shell-file ${PROJECT_SOURCE_DIR}/src/shell.html -s USE_CLOSURE_COMPILER=0 ") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s ELIMINATE\_DUPLICATE\_FUNCTIONS=1 -s ENVIRONMENT=web -s ASSERTIONS=0 -s WASM=1 -DSE_PLATFORM_WEB --shell-file ${PROJECT_SOURCE_DIR}/src/shell.html -s USE_CLOSURE_COMPILER=0 ") @@ -288,7 +290,7 @@ if(ENABLE_HTTP_CONTROL_SERVER) set(SKYEMU_SRC ${SKYEMU_SRC} src/http_control_server.cpp) endif() -set(SKYEMU_SRC ${SKYEMU_SRC} src/cloud.cpp) +set(SKYEMU_SRC ${SKYEMU_SRC} src/cloud.cpp src/https.cpp) if(UNICODE_GUI) set(SKYEMU_SRC ${SKYEMU_SRC} src/utf8proc/utf8proc.c) @@ -302,6 +304,39 @@ if(USE_SDL) include_directories(${SDL2_INCLUDE_DIRS}) endif() +if(ENABLE_RETRO_ACHIEVEMENTS) + add_definitions(-DENABLE_RETRO_ACHIEVEMENTS=1) + add_definitions(-DRC_DISABLE_LUA) + set(RCHEEVOS_SRC src/rcheevos/src/rapi/rc_api_common.c + src/rcheevos/src/rapi/rc_api_editor.c + src/rcheevos/src/rapi/rc_api_info.c + src/rcheevos/src/rapi/rc_api_runtime.c + src/rcheevos/src/rapi/rc_api_user.c + src/rcheevos/src/rcheevos/alloc.c + src/rcheevos/src/rcheevos/compat.c + src/rcheevos/src/rcheevos/condition.c + src/rcheevos/src/rcheevos/condset.c + src/rcheevos/src/rcheevos/consoleinfo.c + src/rcheevos/src/rcheevos/format.c + src/rcheevos/src/rcheevos/lboard.c + src/rcheevos/src/rcheevos/memref.c + src/rcheevos/src/rcheevos/operand.c + src/rcheevos/src/rcheevos/rc_client.c + src/rcheevos/src/rcheevos/rc_validate.c + src/rcheevos/src/rcheevos/richpresence.c + src/rcheevos/src/rcheevos/runtime.c + src/rcheevos/src/rcheevos/runtime_progress.c + src/rcheevos/src/rcheevos/trigger.c + src/rcheevos/src/rcheevos/value.c + src/rcheevos/src/rhash/cdreader.c + src/rcheevos/src/rhash/hash.c + src/rcheevos/src/rhash/md5.c + src/rcheevos/src/rurl/url.c + ) + set(SKYEMU_SRC ${SKYEMU_SRC} ${RCHEEVOS_SRC} src/retro_achievements.cpp) + include_directories(src/rcheevos/include) +endif() + if(IOS) set(SKYEMU_SRC ${SKYEMU_SRC} src/ios_support.m) add_definitions(-DSE_PLATFORM_IOS) diff --git a/src/cloud.cpp b/src/cloud.cpp index a6c4e99d1..3603d0556 100644 --- a/src/cloud.cpp +++ b/src/cloud.cpp @@ -26,15 +26,11 @@ const char* se_get_pref_path(); #define XXH_IMPLEMENTATION #define XXH_STATIC_LINKING_ONLY #include "xxhash.h" +#include "https.hpp" #ifndef EMSCRIPTEN #include "httplib.h" // for server only #include -#include -#include -extern "C" { -#include "res.h" -} #else #include #endif @@ -146,53 +142,7 @@ static void em_flush_fs() #endif } -#ifndef EMSCRIPTEN -size_t curl_write_data(void* buffer, size_t size, size_t nmemb, void* d) -{ - std::vector* data = (std::vector*)d; - data->insert(data->end(), (uint8_t*)buffer, (uint8_t*)buffer + size * nmemb); - return size * nmemb; -} -#else -EM_JS(void, em_https_request, (const char* type, const char* url, const char* body, size_t body_size, const char* headers, void* callback), { - var xhr = new XMLHttpRequest(); - var method = UTF8ToString(type); - var url_str = UTF8ToString(url); - var body_arr = new Uint8Array(Module.HEAPU8.buffer, body, body_size); - xhr.open(method, url_str); - xhr.responseType = "arraybuffer"; - - var headers_str = UTF8ToString(headers); - if (headers_str.length > 0) { - var headers_arr = headers_str.split('\n'); - for (var i = 0; i < headers_arr.length; i++) { - var header = headers_arr[i].split(':'); - if (header.length == 2) { - xhr.setRequestHeader(header[0], header[1]); - } else { - console.log('Invalid header: ' + headers_arr[i]); - } - } - } - - xhr.onload = function() { - if (xhr.status >= 200 && xhr.status < 300) { - var response_size = xhr.response.byteLength; - var response_buffer = Module._malloc(response_size); - var response_view = new Uint8Array(xhr.response); - Module.HEAPU8.set(response_view, response_buffer); - Module.ccall('em_https_request_callback_wrapper', 'void', ['number', 'number', 'number'], [callback, response_buffer, response_size]); - Module._free(response_buffer); - } else { - console.log('The request failed: ' + xhr.status + ' ' + xhr.statusText); - } - }; - xhr.onerror = function() { - console.log('The request failed!'); - }; - xhr.send(body_arr); - }); - +#ifdef EMSCRIPTEN EM_JS(void, em_oath_sign_in, (void* drive, const char* client_id), { var client_id_str = UTF8ToString(client_id); var redirect_uri = window.location.origin + '/authorized.html'; @@ -253,15 +203,6 @@ EM_JS(void, em_oath_sign_in, (void* drive, const char* client_id), { }, 500); }); -extern "C" void em_https_request_callback_wrapper(void* callback, void* data, int size) -{ - std::vector result((uint8_t*)data, (uint8_t*)data + (size_t)size); - std::function&)>* fcallback = - (std::function&)>*)callback; - (*fcallback)(result); - delete fcallback; -} - extern "C" void em_oath_sign_in_callback(cloud_drive_t* drive, const char* refresh_token, const char* access_token) { @@ -290,165 +231,6 @@ extern "C" void em_oath_sign_in_callback(cloud_drive_t* drive, const char* refre } #endif -enum class http_request_e -{ - GET, - POST, - PATCH, -}; - -#ifndef EMSCRIPTEN -// See cacertinmem.c example from libcurl -CURLcode sslctx_function(CURL *curl, void *sslctx, void *parm) -{ - CURLcode rv = CURLE_ABORTED_BY_CALLBACK; - - uint64_t cacert_pem_len; - const uint8_t* cacert_pem = se_get_resource(SE_CACERT_PEM, &cacert_pem_len); - - BIO *cbio = BIO_new_mem_buf(cacert_pem, cacert_pem_len); - X509_STORE *cts = SSL_CTX_get_cert_store((SSL_CTX *)sslctx); - int i; - STACK_OF(X509_INFO) *inf; - (void)curl; - (void)parm; - - if(!cts || !cbio) { - return rv; - } - - inf = PEM_X509_INFO_read_bio(cbio, NULL, NULL, NULL); - - if(!inf) { - BIO_free(cbio); - return rv; - } - - for(i = 0; i < sk_X509_INFO_num(inf); i++) { - X509_INFO *itmp = sk_X509_INFO_value(inf, i); - if(itmp->x509) { - X509_STORE_add_cert(cts, itmp->x509); - } - if(itmp->crl) { - X509_STORE_add_crl(cts, itmp->crl); - } - } - - sk_X509_INFO_pop_free(inf, X509_INFO_free); - BIO_free(cbio); - - rv = CURLE_OK; - return rv; -} -#endif - -// Abstraction layer for http requests -void https_request(http_request_e type, const std::string& url, const std::string& body, - const std::vector>& headers, - std::function&)> callback) -{ -#ifndef EMSCRIPTEN - CURL* curl = curl_easy_init(); - if (!curl) - { - printf("[cloud] failed to initialize curl\n"); - return; - } - - std::vector result; - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 0L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_data); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&result); - - /* Turn off the default CA locations, otherwise libcurl will load CA - * certificates from the locations that were detected/specified at - * build-time - */ - curl_easy_setopt(curl, CURLOPT_CAINFO, NULL); - curl_easy_setopt(curl, CURLOPT_CAPATH, NULL); - - curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslctx_function); - - switch (type) - { - case http_request_e::GET: - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - break; - case http_request_e::POST: - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - break; - case http_request_e::PATCH: - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - break; - default: - printf("[cloud] invalid request type\n"); - return; - } - - struct curl_slist* chunk = NULL; - for (auto& header : headers) - { - std::string header_string = header.first + ": " + header.second; - chunk = curl_slist_append(chunk, header_string.c_str()); - } - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - - CURLcode res = curl_easy_perform(curl); - if (res != CURLE_OK) - { - printf("[cloud] curl failed: %s\n", curl_easy_strerror(res)); - callback({}); - } - else - { - callback(result); - } - - curl_slist_free_all(chunk); - curl_easy_cleanup(curl); -#else - std::string method; - switch (type) - { - case http_request_e::GET: - method = "GET"; - break; - case http_request_e::POST: - method = "POST"; - break; - case http_request_e::PATCH: - method = "PATCH"; - break; - default: - return; - } - - std::string hstring; - for (auto& header : headers) - { - hstring += header.first + ":" + header.second; - if (header != headers.back()) - { - hstring += "\n"; - } - } - - // Deleted when the callback is called - std::function&)>* fcallback = - new std::function&)>(callback); - return em_https_request(method.c_str(), url.c_str(), body.c_str(), body.size(), hstring.c_str(), - (void*)fcallback); -#endif -} - void google_use_refresh_token(cloud_drive_t* drive, std::function callback) { #ifdef EMSCRIPTEN diff --git a/src/httplib.h b/src/httplib.h index 3c01e16c1..1dc0d69e0 100644 --- a/src/httplib.h +++ b/src/httplib.h @@ -831,6 +831,7 @@ std::ostream &operator<<(std::ostream &os, const Error &obj); class Result { public: + Result() {} Result(std::unique_ptr &&res, Error err, Headers &&request_headers = Headers{}) : res_(std::move(res)), err_(err), diff --git a/src/https.cpp b/src/https.cpp new file mode 100644 index 000000000..210eb678f --- /dev/null +++ b/src/https.cpp @@ -0,0 +1,222 @@ +#include "https.hpp" + +#ifndef EMSCRIPTEN +#include +#include +#include +extern "C" { +#include "res.h" +} +#else +#include +#endif + +#ifndef EMSCRIPTEN +size_t curl_write_data(void* buffer, size_t size, size_t nmemb, void* d) +{ + std::vector* data = (std::vector*)d; + data->insert(data->end(), (uint8_t*)buffer, (uint8_t*)buffer + size * nmemb); + return size * nmemb; +} +#else +EM_JS(void, em_https_request, (const char* type, const char* url, const char* body, size_t body_size, const char* headers, void* callback), { + var xhr = new XMLHttpRequest(); + var method = UTF8ToString(type); + var url_str = UTF8ToString(url); + var body_arr = new Uint8Array(Module.HEAPU8.buffer, body, body_size); + xhr.open(method, url_str); + xhr.responseType = "arraybuffer"; + + var headers_str = UTF8ToString(headers); + if (headers_str.length > 0) { + var headers_arr = headers_str.split('\n'); + for (var i = 0; i < headers_arr.length; i++) { + var header = headers_arr[i].split(':'); + if (header.length == 2) { + xhr.setRequestHeader(header[0], header[1]); + } else { + console.log('Invalid header: ' + headers_arr[i]); + } + } + } + + xhr.onload = function() { + if (xhr.status >= 200 && xhr.status < 300) { + var response_size = xhr.response.byteLength; + var response_buffer = Module._malloc(response_size); + var response_view = new Uint8Array(xhr.response); + Module.HEAPU8.set(response_view, response_buffer); + Module.ccall('em_https_request_callback_wrapper', 'void', ['number', 'number', 'number'], [callback, response_buffer, response_size]); + Module._free(response_buffer); + } else { + console.log('The request failed: ' + xhr.status + ' ' + xhr.statusText); + } + }; + xhr.onerror = function() { + console.log('The request failed!'); + }; + xhr.send(body_arr); + }); + + +extern "C" void em_https_request_callback_wrapper(void* callback, void* data, int size) +{ + std::vector result((uint8_t*)data, (uint8_t*)data + (size_t)size); + std::function&)>* fcallback = + (std::function&)>*)callback; + (*fcallback)(result); + delete fcallback; +} +#endif + +#ifndef EMSCRIPTEN +// See cacertinmem.c example from libcurl +CURLcode sslctx_function(CURL *curl, void *sslctx, void *parm) +{ + CURLcode rv = CURLE_ABORTED_BY_CALLBACK; + + uint64_t cacert_pem_len; + const uint8_t* cacert_pem = se_get_resource(SE_CACERT_PEM, &cacert_pem_len); + + BIO *cbio = BIO_new_mem_buf(cacert_pem, cacert_pem_len); + X509_STORE *cts = SSL_CTX_get_cert_store((SSL_CTX *)sslctx); + int i; + STACK_OF(X509_INFO) *inf; + (void)curl; + (void)parm; + + if(!cts || !cbio) { + return rv; + } + + inf = PEM_X509_INFO_read_bio(cbio, NULL, NULL, NULL); + + if(!inf) { + BIO_free(cbio); + return rv; + } + + for(i = 0; i < sk_X509_INFO_num(inf); i++) { + X509_INFO *itmp = sk_X509_INFO_value(inf, i); + if(itmp->x509) { + X509_STORE_add_cert(cts, itmp->x509); + } + if(itmp->crl) { + X509_STORE_add_crl(cts, itmp->crl); + } + } + + sk_X509_INFO_pop_free(inf, X509_INFO_free); + BIO_free(cbio); + + rv = CURLE_OK; + return rv; +} +#endif + +// Abstraction layer for http requests +void https_request(http_request_e type, const std::string& url, const std::string& body, + const std::vector>& headers, + std::function&)> callback) +{ +#ifndef EMSCRIPTEN + CURL* curl = curl_easy_init(); + if (!curl) + { + printf("[cloud] failed to initialize curl\n"); + return; + } + + std::vector result; + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_data); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&result); + + /* Turn off the default CA locations, otherwise libcurl will load CA + * certificates from the locations that were detected/specified at + * build-time + */ + curl_easy_setopt(curl, CURLOPT_CAINFO, NULL); + curl_easy_setopt(curl, CURLOPT_CAPATH, NULL); + + curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslctx_function); + + switch (type) + { + case http_request_e::GET: + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + break; + case http_request_e::POST: + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); + break; + case http_request_e::PATCH: + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); + break; + default: + printf("[cloud] invalid request type\n"); + return; + } + + struct curl_slist* chunk = NULL; + for (auto& header : headers) + { + std::string header_string = header.first + ": " + header.second; + chunk = curl_slist_append(chunk, header_string.c_str()); + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + printf("[cloud] curl failed: %s\n", curl_easy_strerror(res)); + return; + } + else + { + callback(result); + } + + curl_slist_free_all(chunk); + curl_easy_cleanup(curl); +#else + std::string method; + switch (type) + { + case http_request_e::GET: + method = "GET"; + break; + case http_request_e::POST: + method = "POST"; + break; + case http_request_e::PATCH: + method = "PATCH"; + break; + default: + return; + } + + std::string hstring; + for (auto& header : headers) + { + hstring += header.first + ":" + header.second; + if (header != headers.back()) + { + hstring += "\n"; + } + } + + // Deleted when the callback is called + std::function&)>* fcallback = + new std::function&)>(callback); + return em_https_request(method.c_str(), url.c_str(), body.c_str(), body.size(), hstring.c_str(), + (void*)fcallback); +#endif +} \ No newline at end of file diff --git a/src/https.hpp b/src/https.hpp new file mode 100644 index 000000000..29afc4643 --- /dev/null +++ b/src/https.hpp @@ -0,0 +1,20 @@ +#ifndef SKYEMU_HTTPS +#define SKYEMU_HTTPS 1 + +#include +#include +#include +#include + +enum class http_request_e +{ + GET, + POST, + PATCH, +}; + +void https_request(http_request_e type, const std::string& url, const std::string& body, + const std::vector>& headers, + std::function&)> callback); + +#endif \ No newline at end of file diff --git a/src/main.c b/src/main.c index ac52cd65f..138b13504 100644 --- a/src/main.c +++ b/src/main.c @@ -7,6 +7,7 @@ **/ #include +#include #include #define SE_AUDIO_SAMPLE_RATE 48000 #define SE_AUDIO_BUFF_CHANNELS 2 @@ -18,6 +19,12 @@ #include "http_control_server.h" #endif +#ifdef ENABLE_RETRO_ACHIEVEMENTS +#include "retro_achievements.h" +#include "rcheevos/include/rc_client.h" +#include "rcheevos/include/rc_consoles.h" +#endif + #include "gba.h" #include "nds.h" #include "gb.h" @@ -80,7 +87,7 @@ #define GBA_HIGAN_CORRECTION 1 #define SE_FIELD_INDENT 125 - +#define ENABLE_RETRO_ACHIEVEMENTS 0 const static char* se_keybind_names[SE_NUM_KEYBINDS]={ "A", "B", @@ -136,7 +143,6 @@ const static char* se_analog_bind_names[]={ //a users settings. #define SE_NUM_BINDS_ALLOC 64 -#define GUI_MAX_IMAGES_PER_FRAME 16 #define SE_NUM_RECENT_PATHS 32 #define SE_FONT_CACHE_PAGE_SIZE 16 #define SE_MAX_UNICODE_CODE_POINT 0xffff @@ -362,11 +368,14 @@ typedef struct{ uint8_t palettes[5*4]; se_theme_region_t regions[SE_TOTAL_REGIONS]; }se_custom_theme_t; +typedef struct se_deferred_image_free_t{ + sg_image image; + struct se_deferred_image_free_t * next; +}se_deferred_image_free_t; typedef struct { uint64_t laptime; sg_pass_action pass_action; - sg_image image_stack[GUI_MAX_IMAGES_PER_FRAME]; - int current_image; + se_deferred_image_free_t * image_free_list; int screen_width; int screen_height; float dpi_override; @@ -503,6 +512,30 @@ typedef struct{ cloud_user_info_t user_info; } se_cloud_state_t; static void se_sync_cloud_save_states(); +#ifdef ENABLE_RETRO_ACHIEVEMENTS +typedef struct se_ra_tracker_node{ + struct rc_client_leaderboard_tracker_t tracker; + struct se_ra_tracker_node* next; +}se_ra_tracker_node_t; +typedef struct se_ra_challenge_indicator_node{ + uint32_t id; + sg_image image; + struct se_ra_challenge_indicator_node* next; +} se_ra_challenge_indicator_node_t; +typedef struct{ + char username[256]; + char password[256]; + sg_image image; + rc_client_achievement_list_t* achievement_list; + sg_image** achievement_images; + // TODO: make widgets that use these lists and progress indicator + se_ra_tracker_node_t* tracker_list; + se_ra_challenge_indicator_node_t* challenge_indicator_list; + sg_image progress_indicator_image; + bool progress_indicator_shown; + char measured_progress[24]; +}se_ra_info_t; +#endif gui_state_t gui_state={ .update_font_atlas=true }; void se_draw_image(uint8_t *data, int im_width, int im_height,int x, int y, int render_width, int render_height, bool has_alpha); @@ -529,6 +562,10 @@ static bool se_load_theme_from_file(const char * filename); static bool se_draw_theme_region(int region, float x, float y, float w, float h); static bool se_draw_theme_region_tint(int region, float x, float y, float w, float h,uint32_t tint); static bool se_draw_theme_region_tint_partial(int region, float x, float y, float w, float h, float w_ratio, float h_ratio, uint32_t tint); +double se_time(); +void se_push_disabled(); +void se_pop_disabled(); + const char* se_get_pref_path(){ #if defined(EMSCRIPTEN) return "/offline/"; @@ -598,13 +635,15 @@ static inline bool se_checkbox(const char* label, bool * v){ static void se_text(const char* label,...){ va_list args; va_start(args, label); - igTextV(se_localize_and_cache(label),args); + igTextWrappedV(se_localize_and_cache(label),args); va_end(args); } static void se_text_disabled(const char* label,...){ va_list args; va_start(args, label); - igTextDisabledV(se_localize_and_cache(label),args); + se_push_disabled(); + igTextWrappedV(se_localize_and_cache(label),args); + se_pop_disabled(); va_end(args); } static bool se_combo_str(const char* label,int* current_item,const char* items_separated_by_zeros,int popup_max_height_in_items){ @@ -1019,6 +1058,9 @@ se_core_rewind_buffer_t rewind_buffer; se_save_state_t save_states[SE_NUM_SAVE_STATES]; se_cheat_t cheats[SE_NUM_CHEATS]; se_cloud_state_t cloud_state; +#ifdef ENABLE_RETRO_ACHIEVEMENTS +se_ra_info_t ra_info; +#endif bool se_more_rewind_deltas(se_core_rewind_buffer_t* rewind, uint32_t index){ return (rewind->deltas[index%SE_REWIND_BUFFER_SIZE].offset&SE_LAST_DELTA_IN_TX)==0; @@ -1336,6 +1378,20 @@ double se_fps_counter(int tick){ } return 1.0/fps; } +static void se_ra_keep_alive(){ +#ifdef ENABLE_RETRO_ACHIEVEMENTS + static uint64_t last_time=0; + if(last_time==0)last_time=stm_now(); + if(emu_state.run_mode==SB_MODE_PAUSE){ + if(stm_sec(stm_diff(stm_now(),last_time))>1.0){ + last_time = stm_now(); + // Needs to be called once every few seconds if the emulator is paused + // to keep the session alive or retrying failed unlocks + rc_client_idle(ra_get_client()); + } + } +#endif +} static void se_emscripten_flush_fs(){ #if defined(EMSCRIPTEN) @@ -1497,17 +1553,24 @@ bool se_key_is_pressed(int keycode){ return gui_state.button_state[keycode]; } static sg_image* se_get_image(){ - if(gui_state.current_imagenext = gui_state.image_free_list; + gui_state.image_free_list = tmp_image; + return &tmp_image->image; +} +static void se_free_image_deferred(sg_image image){ + se_deferred_image_free_t * tmp_image = (se_deferred_image_free_t*)calloc(1,sizeof(se_deferred_image_free_t)); + tmp_image->next = gui_state.image_free_list; + tmp_image->image=image; + gui_state.image_free_list = tmp_image; } static void se_free_all_images(){ - for(int i=0;iimage); + gui_state.image_free_list=tmp->next; + free(tmp); } - gui_state.current_image=0; } typedef uint8_t (*emu_byte_read_t)(uint64_t address); typedef void (*emu_byte_write_t)(uint64_t address,uint8_t data); @@ -1651,6 +1714,397 @@ void se_draw_emu_stats(){ igPopItemWidth(); } +#ifdef ENABLE_RETRO_ACHIEVEMENTS +static uint32_t se_ra_read_memory_callback(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client){ + if(emu_state.system==SYSTEM_GB){ + for(uint32_t i=0;inum_buckets; i++) + { + if(ra_info.achievement_images[i] != NULL) + { + for (int j = 0; j < ra_info.achievement_list->buckets[i].num_achievements; j++) + { + if(ra_info.achievement_images[i][j].id != SG_INVALID_ID) + se_free_image_deferred(ra_info.achievement_images[i][j]); + } + free(ra_info.achievement_images[i]); + } + } + free(ra_info.achievement_images); + + rc_client_destroy_achievement_list(ra_info.achievement_list); + ra_info.achievement_list = NULL; + ra_info.achievement_images = NULL; + } + { + se_ra_tracker_node_t* next = ra_info.tracker_list; + while(next){ + se_ra_tracker_node_t* tmp = next; + next = next->next; + free(tmp); + } + ra_info.tracker_list = NULL; + } + { + se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; + while(next){ + se_ra_challenge_indicator_node_t* tmp = next; + next = next->next; + free(tmp); + } + ra_info.challenge_indicator_list = NULL; + } +} +static void se_ra_load_game_callback(int result, const char* error_message, rc_client_t* client, void* userdata){ + if (result != RC_OK){ + // TODO: notification error message? + printf("[rcheevos]: failed to load game: %s\n", error_message); + return; + } + + char url[512]; + const rc_client_game_t* game = rc_client_get_game_info(ra_get_client()); + if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK){ + ra_get_image(url, se_ra_load_image_callback, &ra_info.image); + } + + ra_info.achievement_list = rc_client_create_achievement_list(ra_get_client(), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ra_info.achievement_images = (sg_image**)malloc(sizeof(sg_image*)*ra_info.achievement_list->num_buckets); + for (int i = 0; i < ra_info.achievement_list->num_buckets; i++) + { + uint32_t num_achievements=ra_info.achievement_list->buckets[i].num_achievements; + ra_info.achievement_images[i] = (sg_image*)malloc(sizeof(sg_image)*num_achievements); + memset(ra_info.achievement_images[i], 0, sizeof(sg_image)*num_achievements); + for (int j = 0; j < num_achievements; j++) + { + char url[512]; + const rc_client_achievement_t* achievement = ra_info.achievement_list->buckets[i].achievements[j]; + if(rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK){ + ra_get_image(url, se_ra_load_image_callback, &ra_info.achievement_images[i][j]); + } + printf("[rcheevos]: Achievement %s, ", achievement->title); + if (achievement->id == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + printf("unsupported\n"); + else if (achievement->unlocked) + printf("unlocked\n"); + else if (achievement->measured_percent) + printf("progress: %f%%\n", achievement->measured_percent); + else + printf("locked\n"); + } + } +} +static void se_ra_load_game(){ + if(!emu_state.rom_loaded)return; + switch(emu_state.system){ + case SYSTEM_GB: + ra_load_game(emu_state.rom_data,emu_state.rom_size,RC_CONSOLE_GAMEBOY,se_ra_load_game_callback); + break; + case SYSTEM_GBA: + ra_load_game(emu_state.rom_data,emu_state.rom_size,RC_CONSOLE_GAMEBOY_ADVANCE,se_ra_load_game_callback); + break; + case SYSTEM_NDS: + ra_load_game(emu_state.rom_data,emu_state.rom_size,RC_CONSOLE_NINTENDO_DS,se_ra_load_game_callback); + break; + } +} +static void se_ra_login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { + // TODO: show cool "logged in" banner or something + const rc_client_user_t* user = rc_client_get_user_info(client); + if(user){ + printf("[rcheevos]: logged in as %s (score: %d)\n", user->display_name, user->score); + memset(ra_info.password,0,sizeof(ra_info.password)); + + char buffer[sizeof(ra_info.username)+sizeof(ra_info.password)+2]; + memset(buffer,0,sizeof(buffer)); + snprintf(buffer,sizeof(buffer),"%s\n%s\n",ra_info.username,user->token); + + char login_info_path[SB_FILE_PATH_SIZE]; + snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_login_info.txt",se_get_pref_path()); + sb_save_file_data(login_info_path,(uint8_t*)buffer,sizeof(buffer)); + se_ra_load_game(); + } +} +static void se_ra_leaderboard_tracker_show(const rc_client_leaderboard_tracker_t* tracker) +{ + se_ra_tracker_node_t* new_tracker = (se_ra_tracker_node_t*)malloc(sizeof(se_ra_tracker_node_t)); + new_tracker->next = ra_info.tracker_list; + new_tracker->tracker.id = tracker->id; + memcpy(new_tracker->tracker.display, tracker->display, sizeof(new_tracker->tracker.display)); + ra_info.tracker_list = new_tracker; +} +static void se_ra_leaderboard_tracker_update(const rc_client_leaderboard_tracker_t* tracker) +{ + se_ra_tracker_node_t* next = ra_info.tracker_list; + while(next){ + if(next->tracker.id == tracker->id){ + memcpy(next->tracker.display, tracker->display, sizeof(next->tracker.display)); + break; + } + next = next->next; + } +} +static void se_ra_leaderboard_tracker_hide(const rc_client_leaderboard_tracker_t* tracker) +{ + // "hide" seems to be a misleading name here, the wiki says the tracker is no + // longer needed and calls a pseudo function "destroy_tracker", so we destroy it + se_ra_tracker_node_t* prev = NULL; + se_ra_tracker_node_t* next = ra_info.tracker_list; + while(next){ + if(next->tracker.id == tracker->id){ + if(prev){ + prev->next = next->next; + }else{ + ra_info.tracker_list = next->next; + } + free(next); + break; + } + prev = next; + next = next->next; + } +} +static void se_ra_game_mastered() +{ + rc_client_t* client = ra_get_client(); + char message[128]; + char submessage[128]; + const rc_client_game_t* game = rc_client_get_game_info(client); + + // The popup should say "Completed" or "Mastered" depending on whether or not hardcore is enabled. + snprintf(message, sizeof(message), "%s %s", + rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",game->title); + + // TODO: also display the time played + snprintf(submessage, sizeof(submessage), "%s", + rc_client_get_user_info(client)->display_name); + + // TODO: display a popup with the message and submessage and the game icon instead of just printing it + // also perhaps play a sound effect + printf("[rcheevos]: %s %s\n", message, submessage); +} +// TODO: display a popup on these instead +static void se_ra_leaderboard_attempt_started(const rc_client_leaderboard_t* leaderboard) +{ + printf("[rcheevos]: Leaderboard attempt started: %s - %s", leaderboard->title, leaderboard->description); +} +static void se_ra_leaderboard_attempt_failed(const rc_client_leaderboard_t* leaderboard) +{ + printf("[rcheevos]: Leaderboard attempt failed: %s", leaderboard->title); +} +static void se_ra_leaderboard_attempt_submitted(const rc_client_leaderboard_t* leaderboard) +{ + printf("[rcheevos]: Submitted %s for leaderboard attempt: %s", leaderboard->tracker_value, leaderboard->title); +} +static void se_ra_challenge_indicator_show(const rc_client_achievement_t* achievement) +{ + se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; + while(next){ + if(next->id == achievement->id){ + // It shouldn't be the case that "show" is called twice for the same achievement + // but let's be safe and check anyway + return; + } + next = next->next; + } + + se_ra_challenge_indicator_node_t* new_indicator = (se_ra_challenge_indicator_node_t*)malloc(sizeof(se_ra_challenge_indicator_node_t)); + new_indicator->next = ra_info.challenge_indicator_list; + new_indicator->id = achievement->id; + ra_info.challenge_indicator_list = new_indicator; + + char url[128]; + if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) == RC_OK) + { + ra_get_image(url, se_ra_load_image_callback, &new_indicator->image); + } +} +static void se_ra_challenge_indicator_hide(const rc_client_achievement_t* achievement) +{ + // Again "hide" seems to destroy the indicator + se_ra_challenge_indicator_node_t* prev = NULL; + se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; + while(next){ + if(next->id == achievement->id){ + if(prev){ + prev->next = next->next; + }else{ + ra_info.challenge_indicator_list = next->next; + } + free(next); + break; + } + prev = next; + next = next->next; + } +} +static void se_ra_progress_indicator_update(const rc_client_achievement_t* achievement) +{ + if(!ra_info.progress_indicator_shown){ + printf("[rcheevos]Progress indicator update while it's hidden, this shouldn't happen\n"); + } + + char url[128]; + + if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK) + { + ra_get_image(url, se_ra_load_image_callback, &ra_info.progress_indicator_image); + } + + strncpy(ra_info.measured_progress, achievement->measured_progress, sizeof(ra_info.measured_progress)); +} +static void se_ra_progress_indicator_show(const rc_client_achievement_t* achievement) +{ + ra_info.progress_indicator_shown = true; + se_ra_progress_indicator_update(achievement); +} +static void se_ra_progress_indicator_hide(const rc_client_achievement_t* achievement) +{ + // Contrary to other widgets, only one progress indicator can be shown at a time + // and can actually be hidden instead of destroyed + ra_info.progress_indicator_shown = false; +} +static void se_ra_event_handler(const rc_client_event_t* event, rc_client_t* client){ + switch (event->type) + { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + printf("[rcheevos]: Achievement unlocked: %s\n", event->achievement->title); + // TODO: notification? + break; + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + se_ra_leaderboard_attempt_started(event->leaderboard); + break; + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + se_ra_leaderboard_attempt_failed(event->leaderboard); + break; + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + se_ra_leaderboard_attempt_submitted(event->leaderboard); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + se_ra_leaderboard_tracker_update(event->leaderboard_tracker); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + se_ra_leaderboard_tracker_show(event->leaderboard_tracker); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + se_ra_leaderboard_tracker_hide(event->leaderboard_tracker); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + se_ra_challenge_indicator_show(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + se_ra_challenge_indicator_hide(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + se_ra_progress_indicator_show(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + se_ra_progress_indicator_update(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + se_ra_progress_indicator_hide(event->achievement); + break; + case RC_CLIENT_EVENT_GAME_COMPLETED: + se_ra_game_mastered(); + break; + case RC_CLIENT_EVENT_RESET: + emu_state.run_mode = SB_MODE_RESET; + break; + case RC_CLIENT_EVENT_SERVER_ERROR: + printf("[rcheevos]: Server error: %s %s\n", event->server_error->api, event->server_error->error_message); + break; + default: + printf("Unhandled event %d\n", event->type); + break; + } +} +static void se_init_retro_achievements(){ + memset(&ra_info, 0, sizeof(ra_info)); + ra_initialize_client(se_ra_read_memory_callback); + rc_client_set_event_handler(ra_get_client(),se_ra_event_handler); + + // Check if we have a token saved + char login_info_path[SB_FILE_PATH_SIZE]; + snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_login_info.txt",se_get_pref_path()); + if (sb_file_exists(login_info_path)){ + size_t size; + uint8_t *text = sb_load_file_data(login_info_path,&size); + if (text){ + int i = 0; + for(i=0;iwin_max.y){ + igSetCursorPosY(disp_y_max); + return; + } + int box_h = item_height-padding*2; + int box_w = box_h; + igPushIDStr(second_label); + ImVec2 curr_pos; + igGetCursorPos(&curr_pos); + ImVec2 next_pos=curr_pos; + next_pos.y+=box_h+padding*2; + curr_pos.y+=padding; + igSetCursorPos(curr_pos); + + igSetCursorPosX(curr_pos.x+box_w+padding); + igSetCursorPosY(curr_pos.y-padding); + se_text(first_label); + igSetCursorPosX(curr_pos.x+box_w+padding); + igSetCursorPosY(igGetCursorPosY()-5); + se_text_disabled(second_label); + igSetCursorPos(curr_pos); + if(image)igImageButton((ImTextureID)(intptr_t)image->id,(ImVec2){box_w,box_h},(ImVec2){0,0},(ImVec2){1,1},0,(ImVec4){1,1,1,1},(ImVec4){1,1,1,1}); + else se_text_centered_in_box((ImVec2){0,0}, (ImVec2){box_w,box_h},box); + igDummy((ImVec2){1,1}); + igSetCursorPos(next_pos); + igPopID(); +} #ifdef SE_PLATFORM_ANDROID #include @@ -5842,6 +6348,58 @@ void se_draw_menu_panel(){ } } } + #ifdef ENABLE_RETRO_ACHIEVEMENTS + se_text(ICON_FK_TROPHY " Retro Achievements"); + igSeparator(); + const rc_client_user_t* user = rc_client_get_user_info(ra_get_client()); + if(!user){ + igPushIDStr("RetroAchievementsLogin"); + se_text("Username"); + igSameLine(win_w-150,0); + igInputText("##Username",ra_info.username,sizeof(ra_info.username),ImGuiInputTextFlags_None,NULL,NULL); + se_text("Password"); + igSameLine(win_w-150,0); + igInputText("##Password",ra_info.password,sizeof(ra_info.password),ImGuiInputTextFlags_Password,NULL,NULL); + if(se_button(ICON_FK_SIGN_IN " Login", (ImVec2){0,0})){ + ra_login_credentials(ra_info.username,ra_info.password, se_ra_login_callback); + } + igPopID(); + }else { + const rc_client_game_t* game = rc_client_get_game_info(ra_get_client()); + ImVec2 pos; + sg_image * image = NULL; + const char* play_string = "No Game Loaded"; + char line1[256]; + char line2[256]; + snprintf(line1,256,se_localize_and_cache("Logged in as %s"),user->display_name); + if(game){ + if(ra_info.image.id!=SG_INVALID_ID)image=&ra_info.image; + snprintf(line2,256,se_localize_and_cache("Playing: %s"),game->title); + }else snprintf(line2,256,"%s",se_localize_and_cache("No Game Loaded")); + se_boxed_image_dual_label(line1,line2, ICON_FK_TROPHY, image, 0); + if(se_button(ICON_FK_SIGN_OUT " Logout", (ImVec2){0,0})){ + char login_info_path[SB_FILE_PATH_SIZE]; + snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_login_info.txt",se_get_pref_path()); + remove(login_info_path); + rc_client_logout(ra_get_client()); + } + if (ra_info.achievement_list){ + for (int i = 0; i < ra_info.achievement_list->num_buckets; i++){ + se_text(ICON_FK_LOCK " %s",ra_info.achievement_list->buckets[i].label); + for (int j = 0; j < ra_info.achievement_list->buckets[i].num_achievements; j++){ + sg_image * image = NULL; + // TODO: some games need a lot of images for achievements - instead of creating all sg_images at once, we should create them on demand + // or at least increase the sg_images limit. + if(ra_info.achievement_images && ra_info.achievement_images[i] && ra_info.achievement_images[i][j].id!=SG_INVALID_ID){ + image = &ra_info.achievement_images[i][j]; + } + se_boxed_image_dual_label(ra_info.achievement_list->buckets[i].achievements[j]->title, + ra_info.achievement_list->buckets[i].achievements[j]->description, ICON_FK_SPINNER, image, 0); + } + } + } + } + #endif { se_bios_info_t * info = &gui_state.bios_info; if(emu_state.rom_loaded){ @@ -6534,6 +7092,7 @@ static void frame(void) { if(emu_state.joy.inputs[SE_KEY_TOGGLE_FULLSCREEN]&&last_toggle_fullscreen==false)sapp_toggle_fullscreen(); last_toggle_fullscreen = emu_state.joy.inputs[SE_KEY_TOGGLE_FULLSCREEN]; #endif + se_ra_keep_alive(); #ifdef SE_PLATFORM_ANDROID //Handle Android Back Button Navigation @@ -6811,6 +7370,9 @@ static void frame(void) { } screen_x = left_padding; screen_width-=(left_padding+right_padding)*se_dpi_scale(); +#ifdef ENABLE_RETRO_ACHIEVEMENTS + ra_run_pending_callbacks(); +#endif if(gui_state.sidebar_open){ igSetNextWindowPos((ImVec2){screen_x,menu_height}, ImGuiCond_Always, (ImVec2){0,0}); igSetNextWindowSize((ImVec2){sidebar_w, (gui_state.screen_height-menu_height*se_dpi_scale())/se_dpi_scale()}, ImGuiCond_Always); @@ -7591,6 +8153,9 @@ static void init(void) { }; gui_state.last_touch_time=-10000; se_init_audio(); + #ifdef ENABLE_RETRO_ACHIEVEMENTS + se_init_retro_achievements(); + #endif sg_push_debug_group("LCD Shader Init"); gui_state.lcd_prog = sg_make_shader(lcdprog_shader_desc(sg_query_backend())); diff --git a/src/rcheevos/.editorconfig b/src/rcheevos/.editorconfig new file mode 100644 index 000000000..d1591493b --- /dev/null +++ b/src/rcheevos/.editorconfig @@ -0,0 +1,15 @@ +# More info: http://EditorConfig.org +root = true + +# * here means any file type +[*] +end_of_line = crlf +insert_final_newline = true + +# latin1 is a type of ASCII, should work with mbcs +[*.{h,c,cpp}] +charset = latin1 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +curly_bracket_next_line = false diff --git a/src/rcheevos/.github/workflows/c-cpp.yml b/src/rcheevos/.github/workflows/c-cpp.yml new file mode 100644 index 000000000..727fe8ec4 --- /dev/null +++ b/src/rcheevos/.github/workflows/c-cpp.yml @@ -0,0 +1,112 @@ +name: C/C++ CI + +on: + push: + branches: [ master,develop ] + pull_request: + branches: [ master,develop ] + +jobs: + linux-x86: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install gcc-multilib # bits/libc-header-start.h + - name: Build + run: make ARCH=x86 BUILD=c89 test + working-directory: test + - name: Run tests + run: ./test + working-directory: test + + linux-x64: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install valgrind gcc-multilib # bits/libc-header-start.h + - name: Build + run: make ARCH=x64 BUILD=c89 test + working-directory: test + - name: Run tests + run: ./test + working-directory: test + - name: Valgrind + run: make ARCH=x64 BUILD=c89 valgrind + working-directory: test + + # RetroArch compiles with gcc-8, gnu99, and a different set of warnings. + # Attempt to catch issues caused by those discrepencies. + linux-x64-retroarch: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install valgrind gcc-multilib # bits/libc-header-start.h + - name: Check ctype calls + run: make ARCH=x64 BUILD=retroarch check_ctype + working-directory: test + - name: Build + run: make ARCH=x64 BUILD=retroarch test + working-directory: test + - name: Run tests + run: ./test + working-directory: test + - name: Valgrind + run: make ARCH=x64 BUILD=retroarch valgrind + working-directory: test + + linux-x64-lua: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install valgrind gcc-multilib # bits/libc-header-start.h + - name: Build + run: make ARCH=x64 BUILD=c89 HAVE_LUA=1 test + working-directory: test + - name: Run tests + run: ./test + working-directory: test + - name: Valgrind + run: make ARCH=x64 BUILD=c89 HAVE_LUA=1 valgrind + working-directory: test + + windows-x64-msbuild: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install MSBuild + uses: microsoft/setup-msbuild@v1.0.2 + - name: Build + run: msbuild.exe rcheevos-test.sln -t:rcheevos-test -p:Configuration=Release -p:Platform=x64 + working-directory: test + - name: Run tests + run: ./rcheevos-test.exe + working-directory: test/x64/Release + + windows-x64-mingw: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Build + run: make ARCH=x64 BUILD=c89 CC=gcc test + working-directory: test + - name: Run tests + run: ./test.exe + working-directory: test \ No newline at end of file diff --git a/src/rcheevos/.gitignore b/src/rcheevos/.gitignore new file mode 100644 index 000000000..23ead4b3a --- /dev/null +++ b/src/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/src/rcheevos/CHANGELOG.md b/src/rcheevos/CHANGELOG.md new file mode 100644 index 000000000..052d06965 --- /dev/null +++ b/src/rcheevos/CHANGELOG.md @@ -0,0 +1,301 @@ +# v10.7.1 +* add rc_runtime_alloc +* add rc_libretro_memory_find_avail +* extract nginx errors from HTML returned for JSON endpoints +* fix real address for 32X extension RAM +* fix crash attempting to calculate gamecube hash for non-existent file + +# v10.7.0 +* add hash method and memory map for Gamecube +* add console enum, hash method, and memory map for DSi +* add console enum, hash method, and memory map for TI-83 +* add console enum, hash method, and memory map for Uzebox +* add constant for rcheevos version; include in start session server API call +* fix SubSource calculations using float values +* fix game identification for homebrew Jaguar CD games +* fix game identification for CD with many files at root directory +* address _CRT_SECURE_NO_WARNINGS warnings + +# v10.6.0 +* add RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED +* use optimized comparators for most common condition logic +* fix game identification of psx ISOs that have extra slashes in their boot path +* fix game identification of ndd files + +# v10.5.0 +* add RC_MEMSIZE_MBF32_LE +* add RC_OPERATOR_XOR +* add RC_CONSOLE_ATARI_JAGUAR_CD and hash/memory map for Atari Jaguar CD +* add RC_CONSOLE_ARCADIA_2001 and hash/memory map for Arcadia 2001 +* add RC_CONSOLE_INTERTON_VC_4000 and hash/memory map for Interton VC 4000 +* add RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER and hash/memory map for Elektor TV Games Computer +* split RC_CONSOLE_PC_ENGINE_CD off of RC_CONSOLE_PC_ENGINE +* add hash/memory map for RC_CONSOLE_NEO_GEO_CD +* add additional 256KB of RAM to memory map for RC_CONSOLE_SEGA_32X +* validation: don't report redundancy between trigger and non-trigger conditions +* validation: don't report range validation errors for float comparisons +* change default image host to media.retroachievements.org +* fix decoding of denormalized floats +* fix full line comments in the middle of Display: section causing RC_MISSING_DISPLAY_STRING + +# v10.4.0 +* add rc_libretro_hash_set_t with support for #SAVEDISK: m3u extension +* add rc_libretro_is_system_allowed for finer-grain control over core support +* fix measured value from hitcount not resetting while paused +* add RC_CONSOLE_WASM and hash/memory map for WASM-4 +* add scratchpad memory to RC_CONSOLE_PLAYSTATION_2 memory map +* add hash/memory map for RC_CONSOLE_FAIRCHILD_CHANNEL_F +* add hash/memory map for RC_CONSOLE_COMMODORE_64 +* add memory map for RC_CONSOLE_AMIGA + +# v10.3.3 +* add RC_CONSOLE_ARDUBOY and hash/memory map for Arduboy +* add display_name to rc_api_login_response_t +* detect logical conflicts and redundancies in validator +* fix tab sequences in JSON responses being turned into t +* fix overflow when float value has more than 9 digits after the decimal +* fix libretro memory mapping when disconnect mask breaks a region into multiple blocks +* fix non-virtualized file system call when reading some iso files + +# v10.3.2 +* fix RC_OPERAND_PRIOR for bit sizes other than RC_MEMSIZE_BIT_0 +* add memory map and hash for Amstrad CPC +* fix an issue where fetch_game_data and fetch_user_unlocks could return RC_MISSING_VALUE instead of acknowledging a server error + +# v10.3.1 +* allow empty description in rc_api_init_update_leaderboard_request +* fix buffered n64 hash when no filereader is registered +* add memory map and hash for Mega Duck + +# v10.3.0 +* support for floating point memory sizes and logic +* add built-in macros for rich presence: @Number, @Score, @Centisecs, @Seconds, @Minutes, @ASCIIChar, @UnicodeChar +* add rapi functions for fetch_code_notes, update_code_note, upload_achievement, update_leaderboard, fetch_badge_range, and add_game_hash +* add lower_is_better and hidden flags to leaderboards in rc_api_fetch_game_data_response_t +* add achievements_remaining to rc_api_award_achievement_response_t +* add console enums for PC6000, PICO, MEGADUCK and ZEEBO +* add memory map for Dreamcast +* capture leaderboard/rich presence state in rc_runtime_progress data +* support for hashing Dreamcast bin/cues +* support for hashing buffered NDS ROMs +* fix prior for sizes smaller than a byte sometimes returning current value + +# v10.2.0 + +* add RC_MEMSIZE_16_BITS_BE, RC_MEMSIZE_24_BITS_BE, and RC_MEMSIZE_32_BITS_BE +* add secondary flag for RC_CONDITION_MEASURED that tells the UI when to show progress as raw vs. as a percentage +* add rapi calls for fetch_leaderboard_info, fetch_achievement_info and fetch_game_list +* add hash support for RC_CONSOLE_PSP +* add RCHEEVOS_URL_SSL compile flag to use https in rurl functions +* add space to "PC Engine" label +* update RC_CONSOLE_INTELLIVISION memory map to acknowledge non-8-bit addresses +* standardize to z64 format when hashing RC_CONSOLE_N64 +* prevent generating hash for PSX disc when requesting RC_CONSOLE_PLAYSTATION2 +* fix wrong error message being returned when a leaderboard was only slightly malformed + +# v10.1.0 + +* add RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED +* add rc_runtime_validate_addresses +* add external memory to memory map for Magnavox Odyssey 2 +* fix memory map base address for NeoGeo Pocket +* fix bitcount always returning 0 when used in rich presence + +# v10.0.0 + +* add rapi sublibrary for communicating with server (eliminates need for client-side JSON parsing; client must still + provide HTTP functionality). rurl is now deprecated +* renamed 'rhash.h' to 'rc_hash.h' to eliminate conflict with system headers, renamed 'rconsoles.h' and 'rurl.h' for + consistency +* split non-runtime functions out of 'rcheevos.h' as they're not needed by most clients +* allow ranges in rich presence lookups +* add rc_richpresence_size_lines function to fetch line associated to error when processing rich presence script +* add rc_runtime_invalidate_address function to disable achievements when an unknown address is queried +* add RC_CONDITION_RESET_NEXT_IF +* add RC_CONDITION_SUB_HITS +* support MAXOF operator ($) for leaderboard values using trigger syntax +* allow RC_CONDITION_PAUSE_IF and RC_CONDITION_RESET_IF in leaderboard value expression +* changed track parameter of rc_hash_cdreader_open_track_handler to support three virtual tracks: + RC_HASH_CDTRACK_FIRST_DATA, RC_HASH_CDTRACK_LAST and RC_HASH_CDTRACK_LARGEST. +* changed offset parameter of rc_hash_filereader_seek_handler and return value of rc_hash_filereader_tell_handler + from size_t to int64_t to support files larger than 2GB when compiling in 32-bit mode. +* reset to default cd reader if NULL is passed to rc_hash_init_custom_cdreader +* add hash support for RC_CONSOLE_DREAMCAST, RC_CONSOLE_PLAYSTATION_2, RC_CONSOLE_SUPERVISION, and RC_CONSOLE_TIC80 +* ignore headers when generating hashs for RC_CONSOLE_PC_ENGINE and RC_CONSOLE_ATARI_7800 +* require unique identifier when hashing RC_CONSOLE_SEGA_CD and RC_CONSOLE_SATURN discs +* add expansion memory to RC_CONSOLE_SG1000 memory map +* rename RC_CONSOLE_MAGNAVOX_ODYSSEY -> RC_CONSOLE_MAGNAVOX_ODYSSEY2 +* rename RC_CONSOLE_AMIGA_ST -> RC_CONSOLE_ATARI_ST +* add RC_CONSOLE_SUPERVISION, RC_CONSOLE_SHARPX1, RC_CONSOLE_TIC80, RC_CONSOLE_THOMSONTO8 +* fix error identifying largest track when track has multiple bins +* fix memory corruption error when cue track has more than 6 INDEXs +* several improvements to data storage for conditions (rc_memref_t and rc_memref_value_t structures have been modified) + +# v9.2.0 + +* fix issue identifying some PC-FX titles where the boot code is not in the first data track +* add enums and labels for RC_CONSOLE_MAGNAVOX_ODYSSEY, RC_CONSOLE_SUPER_CASSETTEVISION, RC_CONSOLE_NEO_GEO_CD, + RC_CONSOLE_FAIRCHILD_CHANNEL_F, RC_CONSOLE_FM_TOWNS, RC_CONSOLE_ZX_SPECTRUM, RC_CONSOLE_GAME_AND_WATCH, + RC_CONSOLE_NOKIA_NGAGE, RC_CONSOLE_NINTENDO_3DS + +# v9.1.0 + +* add hash support and memory map for RC_CONSOLE_MSX +* add hash support and memory map for RC_CONSOLE_PCFX +* include parent directory when hashing non-arcade titles in arcade mode +* support absolute paths in m3u +* make cue scanning case-insensitive +* expand SRAM mapping for RC_CONSOLE_WONDERSWAN +* fix display of measured value when another group has an unmeasured hit count +* fix memory read error when hashing file with no extension +* fix possible divide by zero when using RC_CONDITION_ADD_SOURCE/RC_CONDITION_SUB_SOURCE +* fix classification of secondary RC_CONSOLE_SATURN memory region + +# v9.0.0 + +* new size: RC_MEMSIZE_BITCOUNT +* new flag: RC_CONDITION_OR_NEXT +* new flag: RC_CONDITION_TRIGGER +* new flag: RC_CONDITION_MEASURED_IF +* new operators: RC_OPERATOR_MULT / RC_OPERATOR_DIV +* is_bcd removed from memref - now part of RC_MEMSIZE +* add rc_runtime_t and associated functions +* add rc_hash_ functions +* add rc_error_str function +* add game_hash parameter to rc_url_award_cheevo +* remove hash parameter from rc_url_submit_lboard +* add rc_url_ping function +* add rc_console_ functions + +# v8.1.0 + +* new flag: RC_CONDITION_MEASURED +* new flag: RC_CONDITION_ADD_ADDRESS +* add rc_evaluate_trigger - extended version of rc_test_trigger with more granular return codes +* make rc_evaluate_value return a signed int (was unsigned int) +* new formats: RC_FORMAT_MINUTES and RC_FORMAT_SECONDS_AS_MINUTES +* removed " Points" text from RC_FORMAT_SCORE format +* removed RC_FORMAT_OTHER format. "OTHER" format now parses to RC_FORMAT_SCORE +* bugfix: AddHits will now honor AndNext on previous condition + +# v8.0.1 + +* bugfix: prevent null reference exception if rich presence contains condition without display string +* bugfix: 24-bit read from memory should only read 24-bits + +# v8.0.0 + +* support for prior operand type +* support for AndNext condition flag +* support for rich presence +* bugfix: update delta/prior memory values while group is paused +* bugfix: allow floating point number without leading 0 +* bugfix: support empty alt groups + +# v7.1.1 + +* Address signed/unsigned mismatch warnings + +# v7.1.0 + +* Added the RC_DISABLE_LUA macro to compile rcheevos without Lua support + +# v7.0.2 + +* Make sure the code is C89-compliant +* Use 32-bit types in Lua +* Only evaluate Lua operands when the Lua state is not `NULL` + +# v7.0.1 + +* Fix the alignment of memory allocations + +# v7.0.0 + +* Removed **rjson** + +# v6.5.0 + +* Added a schema for errors returned by the server + +# v6.4.0 + +* Added an enumeration with the console identifiers used in RetroAchievements + +# v6.3.1 + +* Pass the peek function and the user data to the Lua functions used in operands. + +# v6.3.0 + +* Added **rurl**, an API to build URLs to access RetroAchievements web services. + +# v6.2.0 + +* Added **rjson**, an API to easily decode RetroAchievements JSON files into C structures. + +# v6.1.0 + +* Added support for 24-bit operands with the `'W'` prefix (`RC_OPERAND_24_BITS`) + +# v6.0.2 + +* Only define RC_ALIGNMENT if it has not been already defined + +# v6.0.1 + +* Use `sizeof(void*)` as a better default for `RC_ALIGNMENT` + +# v6.0.0 + +* Simplified API: separate functions to get the buffer size and to parse `memaddr` into the provided buffer +* Fixed crash trying to call `rc_update_condition_pause` during a dry-run +* The callers are now responsible to pass down a scratch buffer to avoid accesses to out-of-scope memory + +# v5.0.0 + +* Pre-compute if a condition has a pause condition in its group +* Added a pre-computed flag that tells if the condition set has at least one pause condition +* Removed the link to the previous condition in a condition set chain + +# v4.0.0 + +* Fixed `ret` not being properly initialized in `rc_parse_trigger` +* Build the unit tests with optimizations and `-Wall` to help catch more issues +* Added `extern "C"` around the inclusion of the Lua headers so that **rcheevos** can be compiled cleanly as C++ +* Exposed `rc_parse_value` and `rc_evaluate_value` to be used with rich presence +* Removed the `reset` and `dirty` flags from the external API + +# v3.2.0 + +* Added the ability to reset triggers and leaderboards +* Add a function to parse a format string and return the format enum, and some unit tests for it + +# v3.1.0 + +* Added `rc_format_value` to the API + +# v3.0.1 + +* Fixed wrong 32-bit value on 64-bit platforms + +# v3.0.0 + +* Removed function rc_evaluate_value from the API + +# v2.0.0 + +* Removed leaderboard callbacks in favor of a simpler scheme + +# v1.1.2 + +* Fixed NULL pointer deference when there's an error during the parse + +# v1.1.1 + +* Removed unwanted garbage +* Should be v1.0.1 :/ + +# v1.0.0 + +* First version diff --git a/src/rcheevos/LICENSE b/src/rcheevos/LICENSE new file mode 100644 index 000000000..5f1faf3dc --- /dev/null +++ b/src/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/src/rcheevos/README.md b/src/rcheevos/README.md new file mode 100644 index 000000000..319c7b64a --- /dev/null +++ b/src/rcheevos/README.md @@ -0,0 +1,316 @@ +# **rcheevos** + +**rcheevos** is a set of C code, or a library if you will, that tries to make it easier for emulators to process [RetroAchievements](https://retroachievements.org) data, providing support for achievements and leaderboards for their players. + +Keep in mind that **rcheevos** does *not* provide HTTP network connections. Clients must get data from RetroAchievements, and pass the response down to **rcheevos** for processing. + +Not all structures defined by **rcheevos** can be created via the public API, but are exposed to allow interactions beyond just creation, destruction, and testing, such as the ones required by UI code that helps to create them. + +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/). + +Most of the exposed APIs are documented [here](https://github.com/RetroAchievements/rcheevos/wiki) + +### User Configuration + +There's only one thing that can be configured by users of **rcheevos**: `RC_ALIGNMENT`. This macro holds the alignment of allocations made in the buffer provided to the parsing functions, and the default value is `sizeof(void*)`. + +If your platform will benefit from a different value, define a new value for it on your compiler flags before compiling the code. It has to be a power of 2, but no checking is done. + +### Return values + +Any function in the rcheevos library that returns a success indicator will return one of the following values. + +These are in `rc_error.h`. + +```c +enum { + RC_OK = 0, + RC_INVALID_LUA_OPERAND = -1, + RC_INVALID_MEMORY_OPERAND = -2, + RC_INVALID_CONST_OPERAND = -3, + RC_INVALID_FP_OPERAND = -4, + RC_INVALID_CONDITION_TYPE = -5, + RC_INVALID_OPERATOR = -6, + RC_INVALID_REQUIRED_HITS = -7, + RC_DUPLICATED_START = -8, + RC_DUPLICATED_CANCEL = -9, + RC_DUPLICATED_SUBMIT = -10, + RC_DUPLICATED_VALUE = -11, + RC_DUPLICATED_PROGRESS = -12, + RC_MISSING_START = -13, + RC_MISSING_CANCEL = -14, + RC_MISSING_SUBMIT = -15, + RC_MISSING_VALUE = -16, + RC_INVALID_LBOARD_FIELD = -17, + RC_MISSING_DISPLAY_STRING = -18, + RC_OUT_OF_MEMORY = -19, + RC_INVALID_VALUE_FLAG = -20, + RC_MISSING_VALUE_MEASURED = -21, + RC_MULTIPLE_MEASURED = -22, + RC_INVALID_MEASURED_TARGET = -23, + RC_INVALID_COMPARISON = -24, + RC_INVALID_STATE = -25, + RC_INVALID_JSON = -26 +}; +``` + +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. + +These are in `rc_consoles.h`. + +```c +enum { + RC_CONSOLE_MEGA_DRIVE = 1, + RC_CONSOLE_NINTENDO_64 = 2, + RC_CONSOLE_SUPER_NINTENDO = 3, + RC_CONSOLE_GAMEBOY = 4, + RC_CONSOLE_GAMEBOY_ADVANCE = 5, + RC_CONSOLE_GAMEBOY_COLOR = 6, + RC_CONSOLE_NINTENDO = 7, + RC_CONSOLE_PC_ENGINE = 8, + RC_CONSOLE_SEGA_CD = 9, + RC_CONSOLE_SEGA_32X = 10, + RC_CONSOLE_MASTER_SYSTEM = 11, + RC_CONSOLE_PLAYSTATION = 12, + RC_CONSOLE_ATARI_LYNX = 13, + RC_CONSOLE_NEOGEO_POCKET = 14, + RC_CONSOLE_GAME_GEAR = 15, + RC_CONSOLE_GAMECUBE = 16, + RC_CONSOLE_ATARI_JAGUAR = 17, + RC_CONSOLE_NINTENDO_DS = 18, + RC_CONSOLE_WII = 19, + RC_CONSOLE_WII_U = 20, + RC_CONSOLE_PLAYSTATION_2 = 21, + RC_CONSOLE_XBOX = 22, + RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, + RC_CONSOLE_POKEMON_MINI = 24, + RC_CONSOLE_ATARI_2600 = 25, + RC_CONSOLE_MS_DOS = 26, + RC_CONSOLE_ARCADE = 27, + RC_CONSOLE_VIRTUAL_BOY = 28, + RC_CONSOLE_MSX = 29, + RC_CONSOLE_COMMODORE_64 = 30, + RC_CONSOLE_ZX81 = 31, + RC_CONSOLE_ORIC = 32, + RC_CONSOLE_SG1000 = 33, + RC_CONSOLE_VIC20 = 34, + RC_CONSOLE_AMIGA = 35, + RC_CONSOLE_ATARI_ST = 36, + RC_CONSOLE_AMSTRAD_PC = 37, + RC_CONSOLE_APPLE_II = 38, + RC_CONSOLE_SATURN = 39, + RC_CONSOLE_DREAMCAST = 40, + RC_CONSOLE_PSP = 41, + RC_CONSOLE_CDI = 42, + RC_CONSOLE_3DO = 43, + RC_CONSOLE_COLECOVISION = 44, + RC_CONSOLE_INTELLIVISION = 45, + RC_CONSOLE_VECTREX = 46, + RC_CONSOLE_PC8800 = 47, + RC_CONSOLE_PC9800 = 48, + RC_CONSOLE_PCFX = 49, + RC_CONSOLE_ATARI_5200 = 50, + RC_CONSOLE_ATARI_7800 = 51, + RC_CONSOLE_X68K = 52, + RC_CONSOLE_WONDERSWAN = 53, + RC_CONSOLE_CASSETTEVISION = 54, + RC_CONSOLE_SUPER_CASSETTEVISION = 55, + RC_CONSOLE_NEO_GEO_CD = 56, + RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, + RC_CONSOLE_FM_TOWNS = 58, + RC_CONSOLE_ZX_SPECTRUM = 59, + RC_CONSOLE_GAME_AND_WATCH = 60, + RC_CONSOLE_NOKIA_NGAGE = 61, + RC_CONSOLE_NINTENDO_3DS = 62, + RC_CONSOLE_SUPERVISION = 63, + RC_CONSOLE_SHARPX1 = 64, + RC_CONSOLE_TIC80 = 65, + RC_CONSOLE_THOMSONTO8 = 66, + RC_CONSOLE_PC6000 = 67, + RC_CONSOLE_PICO = 68, + RC_CONSOLE_MEGADUCK = 69, + RC_CONSOLE_ZEEBO = 70 +}; +``` + +## Runtime support + +The runtime encapsulates a set of achievements, leaderboards, and rich presence for a game and manages processing them for each frame. When important things occur, events are raised for the caller via a callback. + +These are in `rc_runtime.h`. + +The `rc_runtime_t` structure uses several forward-defines. If you need access to the actual contents of any of the forward-defined structures, those definitions are in `rc_runtime_types.h` + +```c +typedef struct rc_runtime_t { + rc_runtime_trigger_t* triggers; + unsigned trigger_count; + unsigned trigger_capacity; + + rc_runtime_lboard_t* lboards; + unsigned lboard_count; + unsigned lboard_capacity; + + rc_runtime_richpresence_t* richpresence; + + rc_memref_value_t* memrefs; + rc_memref_value_t** next_memref; + + rc_value_t* variables; + rc_value_t** next_variable; + + char owns_self; +} +rc_runtime_t; +``` + +You can have rcheevos allocate a runtime for you. In this case, the returned runtime will be initialized and can be used immediately. Note that the allocation can fail, in which case NULL is returned. +```c +rc_runtime_t* rc_runtime_alloc(void); +``` + +You can also allocate the runtime yourself. In this case, the runtime must first be initialized. +```c +void rc_runtime_init(rc_runtime_t* runtime); +``` + +You cannot use `rc_runtime_init` if `rc_runtime_alloc` was used to create the runtime. + +Then individual achievements, leaderboards, and even rich presence can be loaded into the runtime. These functions return RC_OK, or one of the negative value error codes listed above. +```c +int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); +``` + +The runtime should be called once per frame to evaluate the state of the active achievements/leaderboards: +```c +void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L); +``` + +The `event_handler` is a callback function that is called for each event that occurs when processing the frame. +```c +typedef struct rc_runtime_event_t { + unsigned id; + int value; + char type; +} +rc_runtime_event_t; + +typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); +``` + +The `event.type` field will be one of the following: +* RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED (id=achievement id) + An achievement starts in the RC_TRIGGER_STATE_WAITING state and cannot trigger until it has been false for at least one frame. This event indicates the achievement is no longer waiting and may trigger on a future frame. +* RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED (id=achievement id) + One or more conditions in the achievement have disabled the achievement. +* RC_RUNTIME_EVENT_ACHIEVEMENT_RESET (id=achievement id) + One or more conditions in the achievement have reset any progress captured in the achievement. +* RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED (id=achievement id) + All conditions for the achievement have been met and the user should be informed. + NOTE: If `rc_runtime_reset` is called without deactivating the achievement, it may trigger again. +* RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED (id=achievement id) + All non-trigger conditions for the achievement have been met. This typically indicates the achievement is a challenge achievement and the challenge is active. +* RC_RUNTIME_EVENT_LBOARD_STARTED (id=leaderboard id, value=leaderboard value) + The leaderboard's start condition has been met and the user should be informed that a leaderboard attempt has started. +* RC_RUNTIME_EVENT_LBOARD_CANCELED (id=leaderboard id, value=leaderboard value) + The leaderboard's cancel condition has been met and the user should be informed that a leaderboard attempt has failed. +* RC_RUNTIME_EVENT_LBOARD_UPDATED (id=leaderboard id, value=leaderboard value) + The leaderboard value has changed. +* RC_RUNTIME_EVENT_LBOARD_TRIGGERED (id=leaderboard id, value=leaderboard value) + The leaderboard's submit condition has been met and the user should be informed that a leaderboard attempt was successful. The value should be submitted. +* RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED (id=achievement id) + The achievement has been disabled by a call to `rc_invalidate_address`. +* RC_RUNTIME_EVENT_LBOARD_DISABLED (id=leaderboard id) + The achievement has been disabled by a call to `rc_invalidate_address`. + +When an achievement triggers, it should be deactivated so it won't trigger again: +```c +void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); +``` +Additionally, the unlock should be submitted to the server. + +When a leaderboard triggers, it should not be deactivated in case the player wants to try again for a better score. The value should be submitted to the server. + +For `RC_RUNTIME_EVENT_LBOARD_UPDATED` and `RC_RUNTIME_EVENT_LBOARD_TRIGGERED` events, there is a helper function to call if you wish to display the leaderboard value on screen. + +```c +int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format); +``` + +`rc_runtime_do_frame` also periodically updates the rich presense string (every 60 frames). To get the current value, call +```c +const char* rc_runtime_get_richpresence(const rc_runtime_t* runtime); +``` + +When the game is reset, the runtime should also be reset: +```c +void rc_runtime_reset(rc_runtime_t* runtime); +``` + +This ensures any active achievements/leaderboards are set back to their initial states and prevents unexpected triggers when the memory changes in atypical way. + +When you are finished using the runtime, you must destroy it to avoid memory leaks. +```c +void rc_runtime_destroy(rc_runtime_t* runtime); +``` + +If rcheevos allocated the runtime itself (via `rc_runtime_alloc`), then this call will also free the runtime itself. + +## Server Communication + +**rapi** builds URLs to access many RetroAchievements web services. Its purpose it to just to free the developer from having to URL-encode parameters and build correct URLs that are valid for the server. + +**rapi** does *not* make HTTP requests. + +NOTE: **rapi** is a replacement for **rurl**. **rurl** has been deprecated. + +These are in `rc_api_user.h`, `rc_api_runtime.h` and `rc_api_common.h`. + +The basic process of making an **rapi** call is to initialize a params object, call a function to convert it to a URL, send that to the server, then pass the response to a function to convert it into a response object, and handle the response values. + +An example can be found on the [rc_api_init_login_request](https://github.com/RetroAchievements/rcheevos/wiki/rc_api_init_login_request#example) page. + +### Functions + +Please see the [wiki](https://github.com/RetroAchievements/rcheevos/wiki) for details on the functions exposed for **rapi**. + +## Game Identification + +**rhash** provides logic for generating a RetroAchievements hash for a given game. There are two ways to use the API - you can pass the filename and let rhash open and process the file, or you can pass the buffered copy of the file to rhash if you've already loaded it into memory. + +These are in `rc_hash.h`. + +```c + int rc_hash_generate_from_buffer(char hash[33], int console_id, uint8_t* buffer, size_t buffer_size); + int rc_hash_generate_from_file(char hash[33], int console_id, const char* path); +``` + + + diff --git a/src/rcheevos/_config.yml b/src/rcheevos/_config.yml new file mode 100644 index 000000000..18854876c --- /dev/null +++ b/src/rcheevos/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-midnight \ No newline at end of file diff --git a/src/rcheevos/include/rc_api_editor.h b/src/rcheevos/include/rc_api_editor.h new file mode 100644 index 000000000..46880d582 --- /dev/null +++ b/src/rcheevos/include/rc_api_editor.h @@ -0,0 +1,253 @@ +#ifndef RC_API_EDITOR_H +#define RC_API_EDITOR_H + +#include "rc_api_request.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Fetch Code Notes --- */ + +/** + * API parameters for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_request_t { + /* The unique identifier of the game */ + unsigned game_id; +} +rc_api_fetch_code_notes_request_t; + +/* A code note definiton */ +typedef struct rc_api_code_note_t { + /* The address the note is associated to */ + unsigned address; + /* The name of the use who last updated the note */ + const char* author; + /* The contents of the note */ + const char* note; +} rc_api_code_note_t; + +/** + * Response data for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_response_t { + /* An array of code notes for the game */ + rc_api_code_note_t* notes; + /* The number of items in the notes array */ + unsigned num_notes; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_code_notes_response_t; + +int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params); +int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response); +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response); + +/* --- Update Code Note --- */ + +/** + * API parameters for an update code note request. + */ +typedef struct rc_api_update_code_note_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; + /* The address the note is associated to */ + unsigned address; + /* The contents of the note (NULL or empty to delete a note) */ + const char* note; +} +rc_api_update_code_note_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_update_code_note_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_code_note_response_t; + +int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params); +int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response); +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response); + +/* --- Update Achievement --- */ + +/** + * API parameters for an update achievement request. + */ +typedef struct rc_api_update_achievement_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement (0 to create a new achievement) */ + unsigned achievement_id; + /* The unique identifier of the game */ + unsigned game_id; + /* The name of the achievement */ + const char* title; + /* The description of the achievement */ + const char* description; + /* The badge name for the achievement */ + const char* badge; + /* The serialized trigger for the achievement */ + const char* trigger; + /* The number of points the achievement is worth */ + unsigned points; + /* The category of the achievement */ + unsigned category; +} +rc_api_update_achievement_request_t; + +/** + * Response data for an update achievement request. + */ +typedef struct rc_api_update_achievement_response_t { + /* The unique identifier of the achievement */ + unsigned achievement_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_achievement_response_t; + +int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params); +int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response); +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response); + +/* --- Update Leaderboard --- */ + +/** + * API parameters for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard (0 to create a new leaderboard) */ + unsigned leaderboard_id; + /* The unique identifier of the game */ + unsigned game_id; + /* The name of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The start trigger for the leaderboard */ + const char* start_trigger; + /* The submit trigger for the leaderboard */ + const char* submit_trigger; + /* The cancel trigger for the leaderboard */ + const char* cancel_trigger; + /* The value definition for the leaderboard */ + const char* value_definition; + /* The format of leaderboard values */ + const char* format; + /* Whether or not lower scores are better for the leaderboard */ + int lower_is_better; +} +rc_api_update_leaderboard_request_t; + +/** + * Response data for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_response_t { + /* The unique identifier of the leaderboard */ + unsigned leaderboard_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_leaderboard_response_t; + +int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params); +int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response); +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response); + +/* --- Fetch Badge Range --- */ + +/** + * API parameters for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_request_t { + /* Unused */ + unsigned unused; +} +rc_api_fetch_badge_range_request_t; + +/** + * Response data for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_response_t { + /* The numeric identifier of the first valid badge ID */ + unsigned first_badge_id; + /* The numeric identifier of the first unassigned badge ID */ + unsigned next_badge_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_badge_range_response_t; + +int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params); +int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response); +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response); + +/* --- Add Game Hash --- */ + +/** + * API parameters for an add game hash request. + */ +typedef struct rc_api_add_game_hash_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game (0 to create a new game entry) */ + unsigned game_id; + /* The unique identifier of the console for the game */ + unsigned console_id; + /* The title of the game */ + const char* title; + /* The hash being added */ + const char* hash; + /* A description of the hash being added (usually the normalized ROM name) */ + const char* hash_description; +} +rc_api_add_game_hash_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_add_game_hash_response_t { + /* The unique identifier of the game */ + unsigned game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_add_game_hash_response_t; + +int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params); +int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response); +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_EDITOR_H */ diff --git a/src/rcheevos/include/rc_api_info.h b/src/rcheevos/include/rc_api_info.h new file mode 100644 index 000000000..7d3a31607 --- /dev/null +++ b/src/rcheevos/include/rc_api_info.h @@ -0,0 +1,185 @@ +#ifndef RC_API_INFO_H +#define RC_API_INFO_H + +#include "rc_api_request.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Fetch Achievement Info --- */ + +/** + * API parameters for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + unsigned achievement_id; + /* The 1-based index of the first entry to retrieve */ + unsigned first_entry; + /* The number of entries to retrieve */ + unsigned count; + /* Non-zero to only return unlocks earned by the user's friends */ + unsigned friends_only; +} +rc_api_fetch_achievement_info_request_t; + +/* An achievement awarded entry */ +typedef struct rc_api_achievement_awarded_entry_t { + /* The user associated to the entry */ + const char* username; + /* When the achievement was awarded */ + time_t awarded; +} +rc_api_achievement_awarded_entry_t; + +/** + * Response data for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_response_t { + /* The unique identifier of the achievement */ + unsigned id; + /* The unique identifier of the game to which the leaderboard is associated */ + unsigned game_id; + /* The number of times the achievement has been awarded */ + unsigned num_awarded; + /* The number of players that have earned at least one achievement for the game */ + unsigned num_players; + + /* An array of recently rewarded entries */ + rc_api_achievement_awarded_entry_t* recently_awarded; + /* The number of items in the recently_awarded array */ + unsigned num_recently_awarded; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_achievement_info_response_t; + +int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); +int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); + +/* --- Fetch Leaderboard Info --- */ + +/** + * API parameters for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_request_t { + /* The unique identifier of the leaderboard */ + unsigned leaderboard_id; + /* The number of entries to retrieve */ + unsigned count; + /* The 1-based index of the first entry to retrieve */ + unsigned first_entry; + /* The username of the player around whom the entries should be returned */ + const char* username; +} +rc_api_fetch_leaderboard_info_request_t; + +/* A leaderboard info entry */ +typedef struct rc_api_lboard_info_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + unsigned rank; + /* The index of the entry */ + unsigned index; + /* The value of the entry */ + int score; + /* When the entry was submitted */ + time_t submitted; +} +rc_api_lboard_info_entry_t; + +/** + * Response data for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_response_t { + /* The unique identifier of the leaderboard */ + unsigned id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* If non-zero, indicates that lower scores appear first */ + int lower_is_better; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ + const char* definition; + /* The unique identifier of the game to which the leaderboard is associated */ + unsigned game_id; + /* The author of the leaderboard */ + const char* author; + /* When the leaderboard was first uploaded to the server */ + time_t created; + /* When the leaderboard was last modified on the server */ + time_t updated; + + /* An array of requested entries */ + rc_api_lboard_info_entry_t* entries; + /* The number of items in the entries array */ + unsigned num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_leaderboard_info_response_t; + +int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); +int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); + +/* --- Fetch Games List --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_request_t { + /* The unique identifier of the console to query */ + unsigned console_id; +} +rc_api_fetch_games_list_request_t; + +/* A game list entry */ +typedef struct rc_api_game_list_entry_t { + /* The unique identifier of the game */ + unsigned id; + /* The name of the game */ + const char* name; +} +rc_api_game_list_entry_t; + +/** + * Response data for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_response_t { + /* An array of requested entries */ + rc_api_game_list_entry_t* entries; + /* The number of items in the entries array */ + unsigned num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_games_list_response_t; + +int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); +int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_INFO_H */ diff --git a/src/rcheevos/include/rc_api_request.h b/src/rcheevos/include/rc_api_request.h new file mode 100644 index 000000000..81c855fd3 --- /dev/null +++ b/src/rcheevos/include/rc_api_request.h @@ -0,0 +1,86 @@ +#ifndef RC_API_REQUEST_H +#define RC_API_REQUEST_H + +#include "rc_error.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_api_buffer_chunk_t { + /* The current location where data is being written */ + char* write; + /* The first byte past the end of data where writing cannot occur */ + char* end; + /* The first byte of the data */ + char* start; + /* The next block in the allocated memory chain */ + struct rc_api_buffer_chunk_t* next; +} +rc_api_buffer_chunk_t; + +/** + * A preallocated block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_api_buffer_t { + /* The chunk data (will point at the local data member) */ + struct rc_api_buffer_chunk_t chunk; + /* Small chunk of memory pre-allocated for the chunk */ + char data[256]; +} +rc_api_buffer_t; + +/** + * A constructed request to send to the retroachievements server. + */ +typedef struct rc_api_request_t { + /* The URL to send the request to (contains protocol, host, path, and query args) */ + const char* url; + /* Additional query args that should be sent via a POST command. If null, GET may be used */ + const char* post_data; + /* The HTTP Content-Type of the POST data. */ + const char* content_type; + + /* Storage for the url and post_data */ + rc_api_buffer_t buffer; +} +rc_api_request_t; + +/** + * Common attributes for all server responses. + */ +typedef struct rc_api_response_t { + /* Server-provided success indicator (non-zero on success, zero on failure) */ + int succeeded; + /* Server-provided message associated to the failure */ + const char* error_message; + + /* Storage for the response data */ + rc_api_buffer_t buffer; +} +rc_api_response_t; + +void rc_api_destroy_request(rc_api_request_t* request); + +void rc_api_set_host(const char* hostname); +void rc_api_set_image_host(const char* hostname); + +typedef struct rc_api_server_response_t { + /* Pointer to the data returned from the server */ + const char* body; + /* Length of data returned from the server (Content-Length) */ + size_t body_length; + /* HTTP status code returned from the server */ + int http_status_code; +} rc_api_server_response_t; + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_REQUEST_H */ diff --git a/src/rcheevos/include/rc_api_runtime.h b/src/rcheevos/include/rc_api_runtime.h new file mode 100644 index 000000000..4534ff182 --- /dev/null +++ b/src/rcheevos/include/rc_api_runtime.h @@ -0,0 +1,298 @@ +#ifndef RC_API_RUNTIME_H +#define RC_API_RUNTIME_H + +#include "rc_api_request.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Fetch Image --- */ + +/** + * API parameters for a fetch image request. + * NOTE: fetch image server response is the raw image data. There is no rc_api_process_fetch_image_response function. + */ +typedef struct rc_api_fetch_image_request_t { + /* The name of the image to fetch */ + const char* image_name; + /* The type of image to fetch */ + int image_type; +} +rc_api_fetch_image_request_t; + +#define RC_IMAGE_TYPE_GAME 1 +#define RC_IMAGE_TYPE_ACHIEVEMENT 2 +#define RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED 3 +#define RC_IMAGE_TYPE_USER 4 + +int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params); + +/* --- Resolve Hash --- */ + +/** + * API parameters for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_request_t { + /* Unused - hash lookup does not require credentials */ + const char* username; + /* Unused - hash lookup does not require credentials */ + const char* api_token; + /* The generated hash of the game to be identified */ + const char* game_hash; +} +rc_api_resolve_hash_request_t; + +/** + * Response data for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_response_t { + /* The unique identifier of the game, 0 if no match was found */ + unsigned game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_resolve_hash_response_t; + +int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); +int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); + +/* --- Fetch Game Data --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; +} +rc_api_fetch_game_data_request_t; + +/* A leaderboard definition */ +typedef struct rc_api_leaderboard_definition_t { + /* The unique identifier of the leaderboard */ + unsigned id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ + const char* definition; + /* Non-zero if lower values are better for this leaderboard */ + int lower_is_better; + /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ + int hidden; +} +rc_api_leaderboard_definition_t; + +/* An achievement definition */ +typedef struct rc_api_achievement_definition_t { + /* The unique identifier of the achievement */ + unsigned id; + /* The number of points the achievement is worth */ + unsigned points; + /* The achievement category (core, unofficial) */ + unsigned category; + /* The title of the achievement */ + const char* title; + /* The dscription of the achievement */ + const char* description; + /* The definition of the achievement to be passed to rc_runtime_activate_achievement */ + const char* definition; + /* The author of the achievment */ + const char* author; + /* The image name for the achievement badge */ + const char* badge_name; + /* When the achievement was first uploaded to the server */ + time_t created; + /* When the achievement was last modified on the server */ + time_t updated; +} +rc_api_achievement_definition_t; + +#define RC_ACHIEVEMENT_CATEGORY_CORE 3 +#define RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL 5 + +/** + * Response data for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_response_t { + /* The unique identifier of the game */ + unsigned id; + /* The console associated to the game */ + unsigned console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + + /* An array of achievements for the game */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + unsigned num_achievements; + + /* An array of leaderboards for the game */ + rc_api_leaderboard_definition_t* leaderboards; + /* The number of items in the leaderboards array */ + unsigned num_leaderboards; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_data_response_t; + +int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); +int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); + +/* --- Ping --- */ + +/** + * API parameters for a ping request. + */ +typedef struct rc_api_ping_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; + /* (optional) The current rich presence evaluation for the user */ + const char* rich_presence; +} +rc_api_ping_request_t; + +/** + * Response data for a ping request. + */ +typedef struct rc_api_ping_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_ping_response_t; + +int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); +int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_ping_response(rc_api_ping_response_t* response); + +/* --- Award Achievement --- */ + +/** + * API parameters for an award achievement request. + */ +typedef struct rc_api_award_achievement_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + unsigned achievement_id; + /* Non-zero if the achievement was earned in hardcore */ + int hardcore; + /* The hash associated to the game being played */ + const char* game_hash; +} +rc_api_award_achievement_request_t; + +/** + * Response data for an award achievement request. + */ +typedef struct rc_api_award_achievement_response_t { + /* The unique identifier of the achievement that was awarded */ + unsigned awarded_achievement_id; + /* The updated player score */ + unsigned new_player_score; + /* The updated player softcore score */ + unsigned new_player_score_softcore; + /* The number of achievements the user has not yet unlocked for this game + * (in hardcore/non-hardcore per hardcore flag in request) */ + unsigned achievements_remaining; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_award_achievement_response_t; + +int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); +int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); + +/* --- Submit Leaderboard Entry --- */ + +/** + * API parameters for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard */ + unsigned leaderboard_id; + /* The value being submitted */ + int score; + /* The hash associated to the game being played */ + const char* game_hash; +} +rc_api_submit_lboard_entry_request_t; + +/* A leaderboard entry */ +typedef struct rc_api_lboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + unsigned rank; + /* The value of the entry */ + int score; +} +rc_api_lboard_entry_t; + +/** + * Response data for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_response_t { + /* The value that was submitted */ + int submitted_score; + /* The player's best submitted value */ + int best_score; + /* The player's new rank within the leaderboard */ + unsigned new_rank; + /* The total number of entries in the leaderboard */ + unsigned num_entries; + + /* An array of the top entries for the leaderboard */ + rc_api_lboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + unsigned num_top_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_submit_lboard_entry_response_t; + +int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); +int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_RUNTIME_H */ diff --git a/src/rcheevos/include/rc_api_user.h b/src/rcheevos/include/rc_api_user.h new file mode 100644 index 000000000..b04b6271b --- /dev/null +++ b/src/rcheevos/include/rc_api_user.h @@ -0,0 +1,148 @@ +#ifndef RC_API_USER_H +#define RC_API_USER_H + +#include "rc_api_request.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Login --- */ + +/** + * API parameters for a login request. + * If both password and api_token are provided, api_token will be ignored. + */ +typedef struct rc_api_login_request_t { + /* The username of the player being logged in */ + const char* username; + /* The API token from a previous login */ + const char* api_token; + /* The player's password */ + const char* password; +} +rc_api_login_request_t; + +/** + * Response data for a login request. + */ +typedef struct rc_api_login_response_t { + /* The case-corrected username of the player */ + const char* username; + /* The API token to use for all future requests */ + const char* api_token; + /* The current score of the player */ + unsigned score; + /* The current softcore score of the player */ + unsigned score_softcore; + /* The number of unread messages waiting for the player on the web site */ + unsigned num_unread_messages; + /* The preferred name to display for the player */ + const char* display_name; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_login_response_t; + +int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); +int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_login_response(rc_api_login_response_t* response); + +/* --- Start Session --- */ + +/** + * API parameters for a start session request. + */ +typedef struct rc_api_start_session_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; +} +rc_api_start_session_request_t; + +/** + * Response data for an achievement unlock. + */ +typedef struct rc_api_unlock_entry_t { + /* The unique identifier of the unlocked achievement */ + unsigned achievement_id; + /* When the achievement was unlocked */ + time_t when; +} +rc_api_unlock_entry_t; + +/** + * Response data for a start session request. + */ +typedef struct rc_api_start_session_response_t { + /* An array of hardcore user unlocks */ + rc_api_unlock_entry_t* hardcore_unlocks; + /* An array of user unlocks */ + rc_api_unlock_entry_t* unlocks; + + /* The number of items in the hardcore_unlocks array */ + unsigned num_hardcore_unlocks; + /* The number of items in the unlocks array */ + unsigned num_unlocks; + + /* The server timestamp when the response was generated */ + time_t server_now; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_start_session_response_t; + +int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); +int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); + +/* --- Fetch User Unlocks --- */ + +/** + * API parameters for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; + /* Non-zero to fetch hardcore unlocks, 0 to fetch non-hardcore unlocks */ + int hardcore; +} +rc_api_fetch_user_unlocks_request_t; + +/** + * Response data for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_response_t { + /* An array of achievement IDs previously unlocked by the user */ + unsigned* achievement_ids; + /* The number of items in the achievement_ids array */ + unsigned num_achievement_ids; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_user_unlocks_response_t; + +int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); +int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); +void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_H */ diff --git a/src/rcheevos/include/rc_client.h b/src/rcheevos/include/rc_client.h new file mode 100644 index 000000000..fc90b1ad0 --- /dev/null +++ b/src/rcheevos/include/rc_client.h @@ -0,0 +1,592 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +/* implementation abstracted in rc_client_internal.h */ +typedef struct rc_client_t rc_client_t; +typedef struct rc_client_async_handle_t rc_client_async_handle_t; + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address into buffer. + * Returns the number of bytes read. A return value of 0 indicates the address was invalid. + */ +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); + +/** + * Internal method passed to rc_client_server_call_t to process the server response. + */ +typedef void (*rc_client_server_callback_t)(const rc_api_server_response_t* server_response, void* callback_data); + +/** + * Callback used to issue a request to the server. + */ +typedef void (*rc_client_server_call_t)(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +/** + * Generic callback for asynchronous eventing. + */ +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +/** + * Callback for logging or displaying a message. + */ +typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function); + +/** + * Releases resources associated to a rc_client_t object. + * Pointer will no longer be valid after making this call. + */ +void rc_client_destroy(rc_client_t* client); + +/** + * Sets whether hardcore is enabled (on by default). + * Can be called with a game loaded. + * Enabling hardcore with a game loaded will raise an RC_CLIENT_EVENT_RESET + * event. Processing will be disabled until rc_client_reset is called. + */ +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +int rc_client_get_hardcore_enabled(const rc_client_t* client); + +/** + * Sets whether encore mode is enabled (off by default). + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +int rc_client_get_encore_mode_enabled(const rc_client_t* client); + +/** + * Sets whether unofficial achievements should be loaded. + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +int rc_client_get_unofficial_enabled(const rc_client_t* client); + +/** + * Sets whether spectator mode is enabled (off by default). + * If enabled, events for achievement unlocks and leaderboard submissions will be + * raised, but server calls to actually perform the unlock/submit will not occur. + * Can be modified while a game is loaded. Evaluated at unlock/submit time. + * Cannot be modified if disabled before a game is loaded. + */ +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +int rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +void rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +void* rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +void rc_client_set_host(const rc_client_t* client, const char* hostname); + +typedef uint64_t rc_clock_t; +typedef rc_clock_t (*rc_get_time_millisecs_func_t)(const rc_client_t* client); + +/** + * Specifies a function that returns a value that increases once per millisecond. + */ +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler); + +/** + * Marks an async process as aborted. The associated callback will not be called. + */ +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback); +enum +{ + RC_CLIENT_LOG_LEVEL_NONE = 0, + RC_CLIENT_LOG_LEVEL_ERROR = 1, + RC_CLIENT_LOG_LEVEL_WARN = 2, + RC_CLIENT_LOG_LEVEL_INFO = 3, + RC_CLIENT_LOG_LEVEL_VERBOSE = 4 +}; + +/*****************************************************************************\ +| User | +\*****************************************************************************/ + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Logout the user. + */ +void rc_client_logout(rc_client_t* client); + +typedef struct rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; +} rc_client_user_t; + +/** + * Gets information about the logged in user. Will return NULL if the user is not logged in. + */ +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size); + +typedef struct rc_client_user_game_summary_t +{ + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + + uint32_t points_core; + uint32_t points_unlocked; +} rc_client_user_game_summary_t; + +/** + * Gets a breakdown of the number of achievements in the game, and how many the user has unlocked. + * Used for the "You have unlocked X of Y achievements" message shown when the game starts. + */ +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +/** + * Start loading an unidentified game. + */ +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Start loading a game. + */ +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Unloads the current game. + */ +void rc_client_unload_game(rc_client_t* client); + +typedef struct rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} rc_client_game_t; + +/** + * Get information about the current game. Returns NULL if no game is loaded. + */ +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size); + +/** + * Changes the active disc in a multi-disc game. + */ +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +typedef struct rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + + uint32_t num_achievements; + uint32_t num_leaderboards; +} rc_client_subset_t; + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE = 0, /* unprocessed */ + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE = 1, /* eligible to trigger */ + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED = 2, /* earned by user */ + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED = 3 /* not supported by this version of the runtime */ +}; + +enum { + RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE = 0, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL = (1 << 1), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE | RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL +}; + +enum { + RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0, + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE = 0, + RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE = (1 << 1), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE | RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE +}; + +typedef struct rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size); + +typedef struct rc_client_achievement_bucket_t { + rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_achievement_bucket_t; + +typedef struct rc_client_achievement_list_t { + rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_achievement_list_t; + +enum { + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE = 0, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS = 1 +}; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_get_achievement_list. + */ +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_STATE_INACTIVE = 0, + RC_CLIENT_LEADERBOARD_STATE_ACTIVE = 1, + RC_CLIENT_LEADERBOARD_STATE_TRACKING = 2, + RC_CLIENT_LEADERBOARD_STATE_DISABLED = 3 +}; + +typedef struct rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t lower_is_better; +} rc_client_leaderboard_t; + +/** + * Get information about a leaderboard. Returns NULL if not found. + */ +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id); + +typedef struct rc_client_leaderboard_tracker_t { + char display[24]; + uint32_t id; +} rc_client_leaderboard_tracker_t; + +typedef struct rc_client_leaderboard_bucket_t { + rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_leaderboard_bucket_t; + +typedef struct rc_client_leaderboard_list_t { + rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_leaderboard_list_t; + +enum { + RC_CLIENT_LEADERBOARD_BUCKET_UNKNOWN = 0, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE = 1, + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4 +}; + +enum { + RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE = 0, + RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING = 1 +}; + +/** + * Creates a list of leaderboards matching the specified grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_get_leaderboard_list. + */ +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +typedef struct rc_client_leaderboard_entry_t { + const char* user; + char display[24]; + time_t submitted; + uint32_t rank; + uint32_t index; +} rc_client_leaderboard_entry_t; + +typedef struct rc_client_leaderboard_entry_list_t { + rc_client_leaderboard_entry_t* entries; + uint32_t num_entries; + int32_t user_index; +} rc_client_leaderboard_entry_list_t; + +typedef void (*rc_client_fetch_leaderboard_entries_callback_t)(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server containing the logged-in user. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Gets the URL for the profile picture of the user associated to a leaderboard entry. + * Returns RC_OK on success. + */ +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size); + +/** + * Destroys a list allocated by rc_client_begin_fetch_leaderboard_entries or rc_client_begin_fetch_leaderboard_entries_around_user. + */ +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list); + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Processing | +\*****************************************************************************/ + +enum { + RC_CLIENT_EVENT_TYPE_NONE = 0, + RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED = 1, /* [achievement] was earned by the player */ + RC_CLIENT_EVENT_LEADERBOARD_STARTED = 2, /* [leaderboard] attempt has started */ + RC_CLIENT_EVENT_LEADERBOARD_FAILED = 3, /* [leaderboard] attempt failed */ + RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED = 4, /* [leaderboard] attempt submitted */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW = 5, /* [achievement] challenge indicator should be shown */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE = 6, /* [achievement] challenge indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW = 7, /* progress indicator should be shown for [achievement] */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE = 8, /* progress indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE = 9, /* progress indicator should be updated to reflect new badge/progress for [achievement] */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 10, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 11, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_RESET = 13, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 14, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 15 /* an API response returned a [server_error] and will not be retried */ +}; + +typedef struct rc_client_server_error_t +{ + const char* error_message; + const char* api; +} rc_client_server_error_t; + +typedef struct rc_client_event_t +{ + uint32_t type; + + rc_client_achievement_t* achievement; + rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_tracker_t* leaderboard_tracker; + rc_client_server_error_t* server_error; + +} rc_client_event_t; + +/** + * Callback used to notify the client when certain events occur. + */ +typedef void (*rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); + +/** + * Determines if there are any active achievements/leaderboards/rich presence that need processing. + */ +int rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +void rc_client_do_frame(rc_client_t* client); + +/** + * Processes the periodic queue. + * Called internally by rc_client_do_frame. + * Should be explicitly called if rc_client_do_frame is not being called because emulation is paused. + */ +void rc_client_idle(rc_client_t* client); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +void rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +size_t rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer); + +/** + * Deserializes the runtime state from a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/src/rcheevos/include/rc_consoles.h b/src/rcheevos/include/rc_consoles.h new file mode 100644 index 000000000..0dd9c1ee1 --- /dev/null +++ b/src/rcheevos/include/rc_consoles.h @@ -0,0 +1,137 @@ +#ifndef RC_CONSOLES_H +#define RC_CONSOLES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/*****************************************************************************\ +| Console identifiers | +\*****************************************************************************/ + +enum { + RC_CONSOLE_UNKNOWN = 0, + RC_CONSOLE_MEGA_DRIVE = 1, + RC_CONSOLE_NINTENDO_64 = 2, + RC_CONSOLE_SUPER_NINTENDO = 3, + RC_CONSOLE_GAMEBOY = 4, + RC_CONSOLE_GAMEBOY_ADVANCE = 5, + RC_CONSOLE_GAMEBOY_COLOR = 6, + RC_CONSOLE_NINTENDO = 7, + RC_CONSOLE_PC_ENGINE = 8, + RC_CONSOLE_SEGA_CD = 9, + RC_CONSOLE_SEGA_32X = 10, + RC_CONSOLE_MASTER_SYSTEM = 11, + RC_CONSOLE_PLAYSTATION = 12, + RC_CONSOLE_ATARI_LYNX = 13, + RC_CONSOLE_NEOGEO_POCKET = 14, + RC_CONSOLE_GAME_GEAR = 15, + RC_CONSOLE_GAMECUBE = 16, + RC_CONSOLE_ATARI_JAGUAR = 17, + RC_CONSOLE_NINTENDO_DS = 18, + RC_CONSOLE_WII = 19, + RC_CONSOLE_WII_U = 20, + RC_CONSOLE_PLAYSTATION_2 = 21, + RC_CONSOLE_XBOX = 22, + RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, + RC_CONSOLE_POKEMON_MINI = 24, + RC_CONSOLE_ATARI_2600 = 25, + RC_CONSOLE_MS_DOS = 26, + RC_CONSOLE_ARCADE = 27, + RC_CONSOLE_VIRTUAL_BOY = 28, + RC_CONSOLE_MSX = 29, + RC_CONSOLE_COMMODORE_64 = 30, + RC_CONSOLE_ZX81 = 31, + RC_CONSOLE_ORIC = 32, + RC_CONSOLE_SG1000 = 33, + RC_CONSOLE_VIC20 = 34, + RC_CONSOLE_AMIGA = 35, + RC_CONSOLE_ATARI_ST = 36, + RC_CONSOLE_AMSTRAD_PC = 37, + RC_CONSOLE_APPLE_II = 38, + RC_CONSOLE_SATURN = 39, + RC_CONSOLE_DREAMCAST = 40, + RC_CONSOLE_PSP = 41, + RC_CONSOLE_CDI = 42, + RC_CONSOLE_3DO = 43, + RC_CONSOLE_COLECOVISION = 44, + RC_CONSOLE_INTELLIVISION = 45, + RC_CONSOLE_VECTREX = 46, + RC_CONSOLE_PC8800 = 47, + RC_CONSOLE_PC9800 = 48, + RC_CONSOLE_PCFX = 49, + RC_CONSOLE_ATARI_5200 = 50, + RC_CONSOLE_ATARI_7800 = 51, + RC_CONSOLE_X68K = 52, + RC_CONSOLE_WONDERSWAN = 53, + RC_CONSOLE_CASSETTEVISION = 54, + RC_CONSOLE_SUPER_CASSETTEVISION = 55, + RC_CONSOLE_NEO_GEO_CD = 56, + RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, + RC_CONSOLE_FM_TOWNS = 58, + RC_CONSOLE_ZX_SPECTRUM = 59, + RC_CONSOLE_GAME_AND_WATCH = 60, + RC_CONSOLE_NOKIA_NGAGE = 61, + RC_CONSOLE_NINTENDO_3DS = 62, + RC_CONSOLE_SUPERVISION = 63, + RC_CONSOLE_SHARPX1 = 64, + RC_CONSOLE_TIC80 = 65, + RC_CONSOLE_THOMSONTO8 = 66, + RC_CONSOLE_PC6000 = 67, + RC_CONSOLE_PICO = 68, + RC_CONSOLE_MEGADUCK = 69, + RC_CONSOLE_ZEEBO = 70, + RC_CONSOLE_ARDUBOY = 71, + RC_CONSOLE_WASM4 = 72, + RC_CONSOLE_ARCADIA_2001 = 73, + RC_CONSOLE_INTERTON_VC_4000 = 74, + RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER = 75, + RC_CONSOLE_PC_ENGINE_CD = 76, + RC_CONSOLE_ATARI_JAGUAR_CD = 77, + RC_CONSOLE_NINTENDO_DSI = 78, + RC_CONSOLE_TI83 = 79, + RC_CONSOLE_UZEBOX = 80, + + RC_CONSOLE_HUBS = 100, + RC_CONSOLE_EVENTS = 101 +}; + +const char* rc_console_name(int console_id); + +/*****************************************************************************\ +| Memory mapping | +\*****************************************************************************/ + +enum { + RC_MEMORY_TYPE_SYSTEM_RAM, /* normal system memory */ + RC_MEMORY_TYPE_SAVE_RAM, /* memory that persists between sessions */ + RC_MEMORY_TYPE_VIDEO_RAM, /* memory reserved for graphical processing */ + RC_MEMORY_TYPE_READONLY, /* memory that maps to read only data */ + RC_MEMORY_TYPE_HARDWARE_CONTROLLER, /* memory for interacting with system components */ + RC_MEMORY_TYPE_VIRTUAL_RAM, /* secondary address space that maps to real memory in system RAM */ + RC_MEMORY_TYPE_UNUSED /* these addresses don't really exist */ +}; + +typedef struct rc_memory_region_t { + 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 /* RC_CONSOLES_H */ diff --git a/src/rcheevos/include/rc_error.h b/src/rcheevos/include/rc_error.h new file mode 100644 index 000000000..b80bc0bab --- /dev/null +++ b/src/rcheevos/include/rc_error.h @@ -0,0 +1,54 @@ +#ifndef RC_ERROR_H +#define RC_ERROR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/*****************************************************************************\ +| Return values | +\*****************************************************************************/ + +enum { + RC_OK = 0, + RC_INVALID_LUA_OPERAND = -1, + RC_INVALID_MEMORY_OPERAND = -2, + RC_INVALID_CONST_OPERAND = -3, + RC_INVALID_FP_OPERAND = -4, + RC_INVALID_CONDITION_TYPE = -5, + RC_INVALID_OPERATOR = -6, + RC_INVALID_REQUIRED_HITS = -7, + RC_DUPLICATED_START = -8, + RC_DUPLICATED_CANCEL = -9, + RC_DUPLICATED_SUBMIT = -10, + RC_DUPLICATED_VALUE = -11, + RC_DUPLICATED_PROGRESS = -12, + RC_MISSING_START = -13, + RC_MISSING_CANCEL = -14, + RC_MISSING_SUBMIT = -15, + RC_MISSING_VALUE = -16, + RC_INVALID_LBOARD_FIELD = -17, + RC_MISSING_DISPLAY_STRING = -18, + RC_OUT_OF_MEMORY = -19, + RC_INVALID_VALUE_FLAG = -20, + RC_MISSING_VALUE_MEASURED = -21, + RC_MULTIPLE_MEASURED = -22, + RC_INVALID_MEASURED_TARGET = -23, + RC_INVALID_COMPARISON = -24, + RC_INVALID_STATE = -25, + RC_INVALID_JSON = -26, + RC_API_FAILURE = -27, + RC_LOGIN_REQUIRED = -28, + RC_NO_GAME_LOADED = -29, + RC_HARDCORE_DISABLED = -30, + RC_ABORTED = -31, + RC_NO_RESPONSE = -32 +}; + +const char* rc_error_str(int ret); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_ERROR_H */ diff --git a/src/rcheevos/include/rc_hash.h b/src/rcheevos/include/rc_hash.h new file mode 100644 index 000000000..ba9ea1c02 --- /dev/null +++ b/src/rcheevos/include/rc_hash.h @@ -0,0 +1,134 @@ +#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, const 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 + */ + typedef struct rc_hash_iterator + { + const uint8_t* buffer; + size_t buffer_size; + uint8_t consoles[12]; + int index; + const char* path; + } rc_hash_iterator_t; + + /* initializes a rc_hash_iterator + * - path must be provided + * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) + */ + void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); + + /* releases resources associated to a rc_hash_iterator + */ + 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, int64_t offset, int origin); + + /* locates the file pointer */ + typedef int64_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) /* the first data track (skip audio tracks) */ + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ + #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) /* the first data/audio track of the second session */ + + /* opens a track from the specified file. see the RC_HASH_CDTRACK_ defines for special tracks. + * returns a handle to be passed to the other functions, or NULL if the track could not be opened. + */ + typedef void* (*rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); + + /* attempts to read the specified number of bytes from the file starting at the specified absolute sector. + * returns the number of bytes actually read. + */ + typedef size_t (*rc_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); + + /* gets the absolute sector index for the first sector of a track */ + typedef uint32_t(*rc_hash_cdreader_first_track_sector_handler)(void* track_handle); + + struct rc_hash_cdreader + { + rc_hash_cdreader_open_track_handler open_track; + rc_hash_cdreader_read_sector_handler read_sector; + rc_hash_cdreader_close_track_handler close_track; + rc_hash_cdreader_first_track_sector_handler first_track_sector; + }; + + void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader); + void rc_hash_init_default_cdreader(void); + void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader); + + /* ===================================================== */ + +#ifdef __cplusplus +} +#endif + +#endif /* RC_HASH_H */ diff --git a/src/rcheevos/include/rc_runtime.h b/src/rcheevos/include/rc_runtime.h new file mode 100644 index 000000000..d7a25c707 --- /dev/null +++ b/src/rcheevos/include/rc_runtime.h @@ -0,0 +1,155 @@ +#ifndef RC_RUNTIME_H +#define RC_RUNTIME_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_error.h" + +#include + +/*****************************************************************************\ +| Forward Declarations (defined in rc_runtime_types.h) | +\*****************************************************************************/ + +#ifndef RC_RUNTIME_TYPES_H /* prevents pedantic redefinition error */ + +typedef struct lua_State lua_State; + +typedef struct rc_trigger_t rc_trigger_t; +typedef struct rc_lboard_t rc_lboard_t; +typedef struct rc_richpresence_t rc_richpresence_t; +typedef struct rc_memref_t rc_memref_t; +typedef struct rc_value_t rc_value_t; + +#endif + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address. If + * num_bytes is greater than 1, the value is read in little-endian from + * memory. + */ +typedef unsigned (*rc_runtime_peek_t)(unsigned address, unsigned num_bytes, void* ud); + +/*****************************************************************************\ +| 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]; + int serialized_size; + 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; + unsigned char md5[16]; + 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; + + char owns_self; +} +rc_runtime_t; + +rc_runtime_t* rc_runtime_alloc(void); +void rc_runtime_init(rc_runtime_t* runtime); +void rc_runtime_destroy(rc_runtime_t* runtime); + +int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* runtime, unsigned id); +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id, unsigned* measured_value, unsigned* measured_target); +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned id, char *buffer, size_t buffer_size); + +int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_lboard(rc_runtime_t* runtime, unsigned id); +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* runtime, unsigned id); +int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format); + + +int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); +int rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, unsigned buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L); + +enum { + RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, /* from WAITING, PAUSED, or PRIMED to ACTIVE */ + RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, + RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, + RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, + RC_RUNTIME_EVENT_LBOARD_STARTED, + RC_RUNTIME_EVENT_LBOARD_CANCELED, + RC_RUNTIME_EVENT_LBOARD_UPDATED, + RC_RUNTIME_EVENT_LBOARD_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, + RC_RUNTIME_EVENT_LBOARD_DISABLED, + RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED +}; + +typedef struct rc_runtime_event_t { + 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_runtime_peek_t peek, void* ud, lua_State* L); +void rc_runtime_reset(rc_runtime_t* runtime); + +typedef int (*rc_runtime_validate_address_t)(unsigned address); +void rc_runtime_validate_addresses(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); +void rc_runtime_invalidate_address(rc_runtime_t* runtime, unsigned address); + +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); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/src/rcheevos/include/rc_runtime_types.h b/src/rcheevos/include/rc_runtime_types.h new file mode 100644 index 000000000..06990cbc6 --- /dev/null +++ b/src/rcheevos/include/rc_runtime_types.h @@ -0,0 +1,423 @@ +#ifndef RC_RUNTIME_TYPES_H +#define RC_RUNTIME_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_error.h" + +#ifndef RC_RUNTIME_H /* prevents pedantic redefiniton error */ + +typedef struct lua_State lua_State; + +typedef struct rc_trigger_t rc_trigger_t; +typedef struct rc_lboard_t rc_lboard_t; +typedef struct rc_richpresence_t rc_richpresence_t; +typedef struct rc_memref_t rc_memref_t; +typedef struct rc_value_t rc_value_t; + +#endif + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address. If + * num_bytes is greater than 1, the value is read in little-endian from + * memory. + */ +typedef 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_16_BITS_BE, + RC_MEMSIZE_24_BITS_BE, + RC_MEMSIZE_32_BITS_BE, + RC_MEMSIZE_FLOAT, + RC_MEMSIZE_MBF32, + RC_MEMSIZE_MBF32_LE, + RC_MEMSIZE_FLOAT_BE, + RC_MEMSIZE_VARIABLE +}; + +typedef struct rc_memref_value_t { + /* The current value of this memory reference. */ + 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; + /* The value type of the value (for variables) */ + char type; + /* True if the reference will be used in indirection. + * NOTE: This is actually a property of the rc_memref_t, but we put it here to save space */ + char is_indirect; +} +rc_memref_value_t; + +struct rc_memref_t { + /* The current value at the specified memory address. */ + rc_memref_value_t value; + + /* The memory address of this variable. */ + 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 rc_operand_t { + 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; + +int rc_operand_is_memref(const rc_operand_t* operand); + +/*****************************************************************************\ +| Conditions | +\*****************************************************************************/ + +/* types */ +enum { + /* NOTE: this enum is ordered to optimize the switch statements in rc_test_condset_internal. the values may change between releases */ + + /* non-combining conditions (third switch) */ + RC_CONDITION_STANDARD, /* this should always be 0 */ + RC_CONDITION_PAUSE_IF, + RC_CONDITION_RESET_IF, + RC_CONDITION_MEASURED_IF, + RC_CONDITION_TRIGGER, + RC_CONDITION_MEASURED, /* measured also appears in the first switch, so place it at the border between them */ + + /* modifiers (first switch) */ + RC_CONDITION_ADD_SOURCE, /* everything from this point on affects the condition after it */ + RC_CONDITION_SUB_SOURCE, + RC_CONDITION_ADD_ADDRESS, + + /* logic flags (second switch) */ + RC_CONDITION_ADD_HITS, + RC_CONDITION_SUB_HITS, + RC_CONDITION_RESET_NEXT_IF, + RC_CONDITION_AND_NEXT, + RC_CONDITION_OR_NEXT +}; + +/* operators */ +enum { + RC_OPERATOR_EQ, + RC_OPERATOR_LT, + RC_OPERATOR_LE, + RC_OPERATOR_GT, + RC_OPERATOR_GE, + RC_OPERATOR_NE, + RC_OPERATOR_NONE, + RC_OPERATOR_MULT, + RC_OPERATOR_DIV, + RC_OPERATOR_AND, + RC_OPERATOR_XOR +}; + +typedef struct rc_condition_t rc_condition_t; + +struct rc_condition_t { + /* The condition's operands. */ + rc_operand_t operand1; + rc_operand_t operand2; + + /* Required hits to fire this condition. */ + 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; + + /* Unique identifier of optimized comparator to use */ + char optimized_comparator; +}; + +/*****************************************************************************\ +| Condition sets | +\*****************************************************************************/ + +typedef struct rc_condset_t rc_condset_t; + +struct rc_condset_t { + /* The next condition set in the chain. */ + rc_condset_t* next; + + /* The list of conditions in this condition set. */ + rc_condition_t* conditions; + + /* True if any condition in the set is a pause condition. */ + char has_pause; + + /* True if the set is currently paused. */ + char is_paused; + + /* True if the set has indirect memory references. */ + char has_indirect_memrefs; +}; + +/*****************************************************************************\ +| Trigger | +\*****************************************************************************/ + +enum { + RC_TRIGGER_STATE_INACTIVE, /* achievement is not being processed */ + RC_TRIGGER_STATE_WAITING, /* achievement cannot trigger until it has been false for at least one frame */ + RC_TRIGGER_STATE_ACTIVE, /* achievement is active and may trigger */ + RC_TRIGGER_STATE_PAUSED, /* achievement is currently paused and will not trigger */ + RC_TRIGGER_STATE_RESET, /* achievement hit counts were reset */ + RC_TRIGGER_STATE_TRIGGERED, /* achievement has triggered */ + RC_TRIGGER_STATE_PRIMED, /* all non-Trigger conditions are true */ + RC_TRIGGER_STATE_DISABLED /* achievement cannot be processed at this time */ +}; + +struct rc_trigger_t { + /* The main condition set. */ + rc_condset_t* requirement; + + /* The list of sub condition sets in this test. */ + rc_condset_t* alternative; + + /* The memory references required by the trigger. */ + rc_memref_t* memrefs; + + /* The current state of the MEASURED condition. */ + 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; + + /* True if the measured value should be displayed as a percentage */ + char measured_as_percent; +}; + +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 | +\*****************************************************************************/ + +struct rc_value_t { + /* The current value of the variable. */ + rc_memref_value_t value; + + /* The list of conditions to evaluate. */ + rc_condset_t* conditions; + + /* The memory references required by the variable. */ + rc_memref_t* memrefs; + + /* The name of the variable. */ + const char* name; + + /* The next variable in the chain. */ + rc_value_t* next; +}; + +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 */ +}; + +struct rc_lboard_t { + rc_trigger_t start; + rc_trigger_t submit; + rc_trigger_t cancel; + rc_value_t value; + rc_value_t* progress; + rc_memref_t* memrefs; + + char state; +}; + +int rc_lboard_size(const char* memaddr); +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); +int rc_evaluate_lboard(rc_lboard_t* lboard, int* value, rc_peek_t peek, void* peek_ud, lua_State* L); +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, + RC_FORMAT_FLOAT1, + RC_FORMAT_FLOAT2, + RC_FORMAT_FLOAT3, + RC_FORMAT_FLOAT4, + RC_FORMAT_FLOAT5, + RC_FORMAT_FLOAT6 +}; + +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; +}; + +struct rc_richpresence_t { + rc_richpresence_display_t* first_display; + rc_richpresence_lookup_t* first_lookup; + rc_memref_t* memrefs; + rc_value_t* variables; +}; + +int rc_richpresence_size(const char* script); +int rc_richpresence_size_lines(const char* script, int* lines_read); +rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx); +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +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); +void rc_reset_richpresence(rc_richpresence_t* self); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_TYPES_H */ diff --git a/src/rcheevos/include/rc_url.h b/src/rcheevos/include/rc_url.h new file mode 100644 index 000000000..b61cd808c --- /dev/null +++ b/src/rcheevos/include/rc_url.h @@ -0,0 +1,38 @@ +#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_lboard_entries(char* buffer, size_t size, unsigned lboard_id, unsigned first_index, unsigned count); +int rc_url_get_lboard_entries_near_user(char* buffer, size_t size, unsigned lboard_id, const char* user_name, unsigned count); + +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/src/rcheevos/include/rcheevos.h b/src/rcheevos/include/rcheevos.h new file mode 100644 index 000000000..0d9600214 --- /dev/null +++ b/src/rcheevos/include/rcheevos.h @@ -0,0 +1,8 @@ +#ifndef RCHEEVOS_H +#define RCHEEVOS_H + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "rc_consoles.h" + +#endif /* RCHEEVOS_H */ diff --git a/src/rcheevos/src/rapi/rc_api_common.c b/src/rcheevos/src/rapi/rc_api_common.c new file mode 100644 index 000000000..1abcebe40 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_common.c @@ -0,0 +1,1231 @@ +#include "rc_api_common.h" +#include "rc_api_request.h" +#include "rc_api_runtime.h" + +#include "../rcheevos/rc_compat.h" + +#include +#include +#include +#include + +#define RETROACHIEVEMENTS_HOST "https://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org" +#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org" +static char* g_host = NULL; +static char* g_imagehost = NULL; + +#undef DEBUG_BUFFERS + +/* --- rc_json --- */ + +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); + +static int rc_json_match_char(rc_json_iterator_t* iterator, char c) +{ + if (iterator->json < iterator->end && *iterator->json == c) { + ++iterator->json; + return 1; + } + + return 0; +} + +static void rc_json_skip_whitespace(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json)) + ++iterator->json; +} + +static int rc_json_find_closing_quote(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end) { + if (*iterator->json == '"') + return 1; + + if (*iterator->json == '\\') { + ++iterator->json; + if (iterator->json == iterator->end) + return 0; + } + + if (*iterator->json == '\0') + return 0; + + ++iterator->json; + } + + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + int result; + + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; + + field->value_start = iterator->json; + + switch (*iterator->json) + { + case '"': /* quoted string */ + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; + break; + + case '-': + case '+': /* signed number */ + ++iterator->json; + /* fallthrough to number */ + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': /* number */ + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + + if (rc_json_match_char(iterator, '.')) { + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + } + break; + + case '[': /* array */ + result = rc_json_parse_array(iterator, field); + if (result != RC_OK) + return result; + + break; + + case '{': /* object */ + result = rc_json_parse_object(iterator, NULL, 0, &field->array_size); + if (result != RC_OK) + return result; + + break; + + default: /* non-quoted text [true,false,null] */ + if (!isalpha((unsigned char)*iterator->json)) + return RC_INVALID_JSON; + + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; + break; + } + + field->value_end = iterator->json; + return RC_OK; +} + +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_field_t unused_field; + int result; + + if (!rc_json_match_char(iterator, '[')) + return RC_INVALID_JSON; + + field->array_size = 0; + + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; + + do + { + rc_json_skip_whitespace(iterator); + + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; + + ++field->array_size; + + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); + + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; + + return RC_OK; +} + +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, '"')) + return RC_INVALID_JSON; + + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) + return RC_INVALID_JSON; + ++iterator->json; + } + + if (iterator->json == iterator->end) + return RC_INVALID_JSON; + + field->name_len = iterator->json - field->name; + ++iterator->json; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ':')) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); + + if (rc_json_parse_field(iterator, field) < 0) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); + + return RC_OK; +} + +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { + size_t i; + unsigned num_fields = 0; + rc_json_field_t field; + int result; + + if (fields_seen) + *fields_seen = 0; + + for (i = 0; i < field_count; ++i) + fields[i].value_start = fields[i].value_end = NULL; + + if (!rc_json_match_char(iterator, '{')) + return RC_INVALID_JSON; + + if (rc_json_match_char(iterator, '}')) /* empty object */ + return RC_OK; + + do + { + result = rc_json_get_next_field(iterator, &field); + if (result != RC_OK) + return result; + + for (i = 0; i < field_count; ++i) { + if (!fields[i].value_start && fields[i].name_len == field.name_len && + memcmp(fields[i].name, field.name, field.name_len) == 0) { + fields[i].value_start = field.value_start; + fields[i].value_end = field.value_end; + fields[i].array_size = field.array_size; + break; + } + } + + ++num_fields; + + } while (rc_json_match_char(iterator, ',')); + + if (!rc_json_match_char(iterator, '}')) + return RC_INVALID_JSON; + + if (fields_seen) + *fields_seen = num_fields; + + return RC_OK; +} + +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{')) + return 0; + + return (rc_json_get_next_field(iterator, field) == RC_OK); +} + +int rc_json_get_object_string_length(const char* json) { + const char* json_start = json; + + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */ + + rc_json_parse_object(&iterator, NULL, 0, NULL); + + return (int)(iterator.json - json_start); +} + +static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) { + const char* json = server_response->body; + const char* end = json; + + const char* title_start = strstr(json, ""); + if (title_start) { + title_start += 7; + if (isdigit((int)*title_start)) { + const char* title_end = strstr(title_start + 7, ""); + if (title_end) { + char* dst = rc_buf_reserve(&response->buffer, (title_end - title_start) + 1); + response->error_message = dst; + memcpy(dst, title_start, title_end - title_start); + dst += (title_end - title_start); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + response->succeeded = 0; + return RC_INVALID_JSON; + } + } + } + + while (*end && *end != '\n' && end - json < 200) + ++end; + + if (end > json && end[-1] == '\r') + --end; + + if (end > json) { + char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); + response->error_message = dst; + memcpy(dst, json, end - json); + dst += (end - json); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + } + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) { + int result; + +#ifndef NDEBUG + if (field_count < 2) + return RC_INVALID_STATE; + if (strcmp(fields[0].name, "Success") != 0) + return RC_INVALID_STATE; + if (strcmp(fields[1].name, "Error") != 0) + return RC_INVALID_STATE; +#endif + + response->error_message = NULL; + + if (!server_response || !server_response->body || !*server_response->body) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + if (*server_response->body != '{') { + result = rc_json_extract_html_error(response, server_response); + } + else { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + result = rc_json_parse_object(&iterator, fields, field_count, NULL); + + rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); + rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + } + + return result; +} + +static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { + const char* not_found = " not found in response"; + const size_t not_found_len = strlen(not_found); + const size_t field_len = strlen(field->name); + + char* write = rc_buf_reserve(&response->buffer, field_len + not_found_len + 1); + if (write) { + response->error_message = write; + memcpy(write, field->name, field_len); + write += field_len; + memcpy(write, not_found, not_found_len + 1); + write += not_found_len + 1; + rc_buf_consume(&response->buffer, response->error_message, write); + } + + response->succeeded = 0; + return 0; +} + +int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { + rc_json_iterator_t iterator; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!field->value_start) + return rc_json_missing_field(response, field); + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = field->value_start; + iterator.end = field->value_end; + return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK); +} + +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) + return 0; + + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); + + return 1; +} + +int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_iterator_t iterator; + rc_json_field_t array; + rc_json_field_t value; + unsigned* entry; + + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) + return RC_MISSING_VALUE; + + if (*num_entries) { + *entries = (unsigned*)rc_buf_alloc(&response->buffer, *num_entries * sizeof(unsigned)); + if (!*entries) + return RC_OUT_OF_MEMORY; + + value.name = field_name; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array.value_start; + iterator.end = array.value_end; + + entry = *entries; + while (rc_json_get_array_entry_value(&value, &iterator)) { + if (!rc_json_get_unum(entry, &value, field_name)) + return RC_MISSING_VALUE; + + ++entry; + } + } + else { + *entries = NULL; + } + + return RC_OK; +} + +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!rc_json_get_optional_array(num_entries, array_field, response, field, field_name)) + return rc_json_missing_field(response, field); + + return 1; +} + +int rc_json_get_optional_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!field->value_start || *field->value_start != '[') { + *num_entries = 0; + return 0; + } + + memcpy(array_field, field, sizeof(*array_field)); + ++array_field->value_start; /* skip [ */ + + *num_entries = field->array_size; + return 1; +} + +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) + return 0; + + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); + + return 1; +} + +static unsigned rc_json_decode_hex4(const char* input) { + char hex[5]; + + memcpy(hex, input, 4); + hex[4] = '\0'; + + return (unsigned)strtoul(hex, NULL, 16); +} + +static int rc_json_ucs32_to_utf8(unsigned char* dst, unsigned ucs32_char) { + if (ucs32_char < 0x80) { + dst[0] = (ucs32_char & 0x7F); + return 1; + } + + if (ucs32_char < 0x0800) { + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xC0 | (ucs32_char & 0x1F); + return 2; + } + + if (ucs32_char < 0x010000) { + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xE0 | (ucs32_char & 0x0F); + return 3; + } + + if (ucs32_char < 0x200000) { + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF0 | (ucs32_char & 0x07); + return 4; + } + + if (ucs32_char < 0x04000000) { + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF8 | (ucs32_char & 0x03); + return 5; + } + + dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xFC | (ucs32_char & 0x01); + return 6; +} + +int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + size_t len = field->value_end - field->value_start; + char* dst; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = NULL; + return 0; + } + + if (len == 4 && memcmp(field->value_start, "null", 4) == 0) { + *out = NULL; + return 1; + } + + if (*src == '\"') { + ++src; + + if (*src == '\"') { + /* simple optimization for empty string - don't allocate space */ + *out = ""; + return 1; + } + + *out = dst = rc_buf_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ + + do { + if (*src == '\\') { + ++src; + if (*src == 'n') { + /* newline */ + ++src; + *dst++ = '\n'; + continue; + } + + if (*src == 'r') { + /* carriage return */ + ++src; + *dst++ = '\r'; + continue; + } + + if (*src == 'u') { + /* unicode character */ + unsigned ucs32_char = rc_json_decode_hex4(src + 1); + src += 5; + + if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) { + /* surrogate lead - look for surrogate tail */ + if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') { + const unsigned surrogate = rc_json_decode_hex4(src + 2); + src += 6; + + if (surrogate >= 0xDC00 && surrogate < 0xE000) { + /* found a surrogate tail, merge them */ + ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000; + } + } + + if (!(ucs32_char & 0xFFFF0000)) { + /* invalid surrogate pair, fallback to replacement char */ + ucs32_char = 0xFFFD; + } + } + + dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char); + continue; + } + + if (*src == 't') { + /* tab */ + ++src; + *dst++ = '\t'; + continue; + } + + /* just an escaped character, fallthrough to normal copy */ + } + + *dst++ = *src++; + } while (*src != '\"'); + + } else { + *out = dst = rc_buf_reserve(buffer, len + 1); /* +1 for null terminator */ + memcpy(dst, src, len); + dst += len; + } + + *dst++ = '\0'; + rc_buf_consume(buffer, *out, dst); + return 1; +} + +void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value) { + if (!rc_json_get_string(out, &response->buffer, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_string(out, &response->buffer, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + int value = 0; + int negative = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + /* assert: string contains only numerals and an optional sign per rc_json_parse_field */ + if (*src == '-') { + negative = 1; + ++src; + } else if (*src == '+') { + ++src; + } else if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + if (negative) + *out = -value; + else + *out = value; + + return 1; +} + +void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_num(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_num(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + int value = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + /* assert: string contains only numerals per rc_json_parse_field */ + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + *out = value; + return 1; +} + +void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value) { + if (!rc_json_get_unum(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_unum(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) { + struct tm tm; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (*field->value_start == '\"') { + memset(&tm, 0, sizeof(tm)); + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { + tm.tm_mon--; /* 0-based */ + tm.tm_year -= 1900; /* 1900 based */ + + /* mktime converts a struct tm to a time_t using the local timezone. + * the input string is UTC. since timegm is not universally cross-platform, + * figure out the offset between UTC and local time by applying the + * timezone conversion twice and manually removing the difference */ + { + time_t local_timet = mktime(&tm); + time_t skewed_timet, tz_offset; + struct tm gmt_tm; + gmtime_s(&gmt_tm, &local_timet); + skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */ + tz_offset = skewed_timet - local_timet; + *out = local_timet - tz_offset; + } + + return 1; + } + } + + *out = 0; + return 0; +} + +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_datetime(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (src) { + const size_t len = field->value_end - field->value_start; + if (len == 4 && strncasecmp(src, "true", 4) == 0) { + *out = 1; + return 1; + } else if (len == 5 && strncasecmp(src, "false", 5) == 0) { + *out = 0; + return 1; + } else if (len == 1) { + *out = (*src != '0'); + return 1; + } + } + + *out = 0; + return 0; +} + +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_bool(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_bool(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +/* --- rc_buf --- */ + +void rc_buf_init(rc_api_buffer_t* buffer) { + buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; + buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; + buffer->chunk.next = NULL; +} + +void rc_buf_destroy(rc_api_buffer_t* buffer) { + rc_api_buffer_chunk_t *chunk; +#ifdef DEBUG_BUFFERS + int count = 0; + int wasted = 0; + int total = 0; +#endif + + /* first chunk is not allocated. skip it. */ + chunk = buffer->chunk.next; + + /* deallocate any additional buffers */ + while (chunk) { + rc_api_buffer_chunk_t* next = chunk->next; +#ifdef DEBUG_BUFFERS + total += (int)(chunk->end - chunk->data); + wasted += (int)(chunk->end - chunk->write); + ++count; +#endif + free(chunk); + chunk = next; + } + +#ifdef DEBUG_BUFFERS + printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, + total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); +#endif +} + +char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount) { + rc_api_buffer_chunk_t* chunk = &buffer->chunk; + size_t remaining; + while (chunk) { + remaining = chunk->end - chunk->write; + if (remaining >= amount) + return chunk->write; + + if (!chunk->next) { + /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated + * to the chunk header, and the remaining will be used for data. + */ + const size_t chunk_header_size = sizeof(rc_api_buffer_chunk_t); + const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; + chunk->next = (rc_api_buffer_chunk_t*)malloc(alloc_size); + if (!chunk->next) + break; + + chunk->next->start = (char*)chunk->next + chunk_header_size; + chunk->next->write = chunk->next->start; + chunk->next->end = (char*)chunk->next + alloc_size; + chunk->next->next = NULL; + } + + chunk = chunk->next; + } + + return NULL; +} + +void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end) { + rc_api_buffer_chunk_t* chunk = &buffer->chunk; + do { + if (chunk->write == start) { + size_t offset = (end - chunk->start); + offset = (offset + 7) & ~7; + chunk->write = &chunk->start[offset]; + + if (chunk->write > chunk->end) + chunk->write = chunk->end; + break; + } + + chunk = chunk->next; + } while (chunk); +} + +void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { + char* ptr = rc_buf_reserve(buffer, amount); + rc_buf_consume(buffer, ptr, ptr + amount); + return (void*)ptr; +} + +char* rc_buf_strncpy(rc_api_buffer_t* buffer, const char* src, size_t len) { + char* dst = rc_buf_reserve(buffer, len + 1); + memcpy(dst, src, len); + dst[len] = '\0'; + rc_buf_consume(buffer, dst, dst + len + 2); + return dst; +} + +char* rc_buf_strcpy(rc_api_buffer_t* buffer, const char* src) { + return rc_buf_strncpy(buffer, src, strlen(src)); +} + +void rc_api_destroy_request(rc_api_request_t* request) { + rc_buf_destroy(&request->buffer); +} + +void rc_api_format_md5(char checksum[33], const unsigned char digest[16]) { + snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); +} + +/* --- rc_url_builder --- */ + +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size) { + rc_api_buffer_chunk_t* used_buffer; + + memset(builder, 0, sizeof(*builder)); + builder->buffer = buffer; + builder->write = builder->start = rc_buf_reserve(buffer, estimated_size); + + used_buffer = &buffer->chunk; + while (used_buffer && used_buffer->write != builder->write) + used_buffer = used_buffer->next; + + builder->end = (used_buffer) ? used_buffer->end : builder->start + estimated_size; +} + +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { + rc_url_builder_append(builder, "", 1); + + if (builder->result != RC_OK) + return NULL; + + rc_buf_consume(builder->buffer, builder->start, builder->write); + return builder->start; +} + +static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) { + if (builder->result == RC_OK) { + size_t remaining = builder->end - builder->write; + if (remaining < amount) { + const size_t used = builder->write - builder->start; + const size_t current_size = builder->end - builder->start; + const size_t buffer_prefix_size = sizeof(rc_api_buffer_chunk_t); + char* new_start; + size_t new_size = (current_size < 256) ? 256 : current_size * 2; + do { + remaining = new_size - used; + if (remaining >= amount) + break; + + new_size *= 2; + } while (1); + + /* rc_buf_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ + if ((remaining - amount) > buffer_prefix_size) + new_size -= buffer_prefix_size; + + new_start = rc_buf_reserve(builder->buffer, new_size); + if (!new_start) { + builder->result = RC_OUT_OF_MEMORY; + return RC_OUT_OF_MEMORY; + } + + if (new_start != builder->start) { + memcpy(new_start, builder->start, used); + builder->start = new_start; + builder->write = new_start + used; + } + + builder->end = builder->start + new_size; + } + } + + return builder->result; +} + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) { + static const char hex[] = "0123456789abcdef"; + const char* start = str; + size_t len = 0; + for (;;) { + const char c = *str++; + switch (c) { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + case '-': case '_': case '.': case '~': + len++; + continue; + + case '\0': + if (len) + rc_url_builder_append(builder, start, len); + + return; + + default: + if (rc_url_builder_reserve(builder, len + 3) != RC_OK) + return; + + if (len) { + memcpy(builder->write, start, len); + builder->write += len; + } + + if (c == ' ') { + *builder->write++ = '+'; + } else { + *builder->write++ = '%'; + *builder->write++ = hex[((unsigned char)c) >> 4]; + *builder->write++ = hex[c & 0x0F]; + } + break; + } + + start = str; + len = 0; + } +} + +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) { + if (rc_url_builder_reserve(builder, len) == RC_OK) { + memcpy(builder->write, data, len); + builder->write += len; + } +} + +static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) { + size_t param_len = strlen(param); + + if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) { + if (builder->write > builder->start) { + if (builder->write[-1] != '?') + *builder->write++ = '&'; + } + + memcpy(builder->write, param, param_len); + builder->write += param_len; + *builder->write++ = '='; + } + + return builder->result; +} + +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value) { + if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%u", value); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value) { + if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%d", value); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) { + rc_url_builder_append_param_equals(builder, param); + rc_url_builder_append_encoded_str(builder, value); +} + +void rc_api_url_build_dorequest_url(rc_api_request_t* request) { + #define DOREQUEST_ENDPOINT "/dorequest.php" + rc_buf_init(&request->buffer); + + if (!g_host) { + request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; + } + else { + const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT); + const size_t host_len = strlen(g_host); + const size_t url_len = host_len + endpoint_len; + char* url = rc_buf_reserve(&request->buffer, url_len); + + memcpy(url, g_host, host_len); + memcpy(url + host_len, DOREQUEST_ENDPOINT, endpoint_len); + rc_buf_consume(&request->buffer, url, url + url_len); + + request->url = url; + } + #undef DOREQUEST_ENDPOINT +} + +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) { + if (!username || !*username || !api_token || !*api_token) { + builder->result = RC_INVALID_STATE; + return 0; + } + + rc_url_builder_append_str_param(builder, "r", api); + rc_url_builder_append_str_param(builder, "u", username); + rc_url_builder_append_str_param(builder, "t", api_token); + + return (builder->result == RC_OK); +} + +/* --- Set Host --- */ + +static void rc_api_update_host(char** host, const char* hostname) { + if (*host != NULL) + free(*host); + + if (hostname != NULL) { + if (strstr(hostname, "://")) { + *host = strdup(hostname); + } + else { + const size_t hostname_len = strlen(hostname); + if (hostname_len == 0) { + *host = NULL; + } + else { + char* newhost = (char*)malloc(hostname_len + 7 + 1); + if (newhost) { + memcpy(newhost, "http://", 7); + memcpy(&newhost[7], hostname, hostname_len + 1); + *host = newhost; + } + else { + *host = NULL; + } + } + } + } + else { + *host = NULL; + } +} + +void rc_api_set_host(const char* hostname) { + rc_api_update_host(&g_host, hostname); + + if (!hostname) { + /* also clear out the image hostname */ + rc_api_set_image_host(NULL); + } + else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if just pointing at the non-HTTPS host, explicitly use the default image host + * so it doesn't try to use the web host directly */ + rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL); + } +} + +void rc_api_set_image_host(const char* hostname) { + rc_api_update_host(&g_imagehost, hostname); +} + +/* --- Fetch Image --- */ + +int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_buf_init(&request->buffer); + rc_url_builder_init(&builder, &request->buffer, 64); + + if (g_imagehost) { + rc_url_builder_append(&builder, g_imagehost, strlen(g_imagehost)); + } + else if (g_host) { + rc_url_builder_append(&builder, g_host, strlen(g_host)); + } + else { + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); + } + + switch (api_params->image_type) + { + case RC_IMAGE_TYPE_GAME: + rc_url_builder_append(&builder, "/Images/", 8); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, "_lock.png", 9); + break; + + case RC_IMAGE_TYPE_USER: + rc_url_builder_append(&builder, "/UserPic/", 9); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + default: + return RC_INVALID_STATE; + } + + request->url = rc_url_builder_finalize(&builder); + request->post_data = NULL; + + return builder.result; +} diff --git a/src/rcheevos/src/rapi/rc_api_common.h b/src/rcheevos/src/rapi/rc_api_common.h new file mode 100644 index 000000000..c8c534898 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_common.h @@ -0,0 +1,90 @@ +#ifndef RC_API_COMMON_H +#define RC_API_COMMON_H + +#include "rc_api_request.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define RC_CONTENT_TYPE_URLENCODED "application/x-www-form-urlencoded" + +typedef struct rc_api_url_builder_t { + char* write; + char* start; + char* end; + /* pointer to a preallocated rc_api_buffer_t */ + rc_api_buffer_t* buffer; + int result; +} +rc_api_url_builder_t; + +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size); +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); + +#define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} + +typedef struct rc_json_field_t { + const char* value_start; + const char* value_end; + const char* name; + size_t name_len; + unsigned array_size; +} +rc_json_field_t; + +typedef struct rc_json_iterator_t { + const char* json; + const char* end; +} +rc_json_iterator_t; + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count); +int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); +int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name); +void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value); +void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value); +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +int rc_json_get_optional_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); +int rc_json_get_object_string_length(const char* json); + +void rc_buf_init(rc_api_buffer_t* buffer); +void rc_buf_destroy(rc_api_buffer_t* buffer); +char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount); +void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end); +void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount); +char* rc_buf_strcpy(rc_api_buffer_t* buffer, const char* src); +char* rc_buf_strncpy(rc_api_buffer_t* buffer, const char* src, size_t len); + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value); +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value); +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value); + +void rc_api_url_build_dorequest_url(rc_api_request_t* request); +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token); +void rc_api_format_md5(char checksum[33], const unsigned char digest[16]); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_COMMON_H */ diff --git a/src/rcheevos/src/rapi/rc_api_editor.c b/src/rcheevos/src/rapi/rc_api_editor.c new file mode 100644 index 000000000..ea4cbe19b --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_editor.c @@ -0,0 +1,518 @@ +#include "rc_api_editor.h" +#include "rc_api_common.h" + +#include "../rcheevos/rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +/* --- Fetch Code Notes --- */ + +int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "codenotes2"); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_code_notes_server_response(response, &response_obj); +} + +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; + rc_api_code_note_t* note; + const char* address_str; + const char* last_author = ""; + size_t last_author_len = 0; + size_t len; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("CodeNotes") + }; + + rc_json_field_t note_fields[] = { + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Note") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_array(&response->num_notes, &array_field, &response->response, &fields[2], "CodeNotes")) + return RC_MISSING_VALUE; + + if (response->num_notes) { + response->notes = (rc_api_code_note_t*)rc_buf_alloc(&response->response.buffer, response->num_notes * sizeof(rc_api_code_note_t)); + if (!response->notes) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + note = response->notes; + while (rc_json_get_array_entry_object(note_fields, sizeof(note_fields) / sizeof(note_fields[0]), &iterator)) { + /* an empty note represents a record that was deleted on the server */ + /* a note set to '' also represents a deleted note (remnant of a bug) */ + /* NOTE: len will include the quotes */ + if (note_fields[2].value_start) { + len = note_fields[2].value_end - note_fields[2].value_start; + if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { + --response->num_notes; + continue; + } + } + + if (!rc_json_get_required_string(&address_str, &response->response, ¬e_fields[0], "Address")) + return RC_MISSING_VALUE; + note->address = (unsigned)strtol(address_str, NULL, 16); + if (!rc_json_get_required_string(¬e->note, &response->response, ¬e_fields[2], "Note")) + return RC_MISSING_VALUE; + + len = note_fields[1].value_end - note_fields[1].value_start; + if (len == last_author_len && memcmp(note_fields[1].value_start, last_author, len) == 0) { + note->author = last_author; + } + else { + if (!rc_json_get_required_string(¬e->author, &response->response, ¬e_fields[1], "User")) + return RC_MISSING_VALUE; + + last_author = note->author; + last_author_len = len; + } + + ++note; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Update Code Note --- */ + +int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitcodenote", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "m", api_params->address); + + if (api_params->note && *api_params->note) + rc_url_builder_append_str_param(&builder, "n", api_params->note); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_code_note_server_response(response, &response_obj); +} + +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") + /* unused fields + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("Note") + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + return RC_OK; +} + +void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Update Achievement --- */ + +int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0 || api_params->category == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description || !*api_params->description) + return RC_INVALID_STATE; + if (!api_params->trigger || !*api_params->trigger) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadachievement", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->achievement_id) + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "m", api_params->trigger); + rc_url_builder_append_unum_param(&builder, "z", api_params->points); + rc_url_builder_append_unum_param(&builder, "f", api_params->category); + if (api_params->badge) + rc_url_builder_append_str_param(&builder, "b", api_params->badge); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->trigger, (int)strlen(api_params->trigger)); + snprintf(buffer, sizeof(buffer), "%u", api_params->points); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + snprintf(buffer, sizeof(buffer), "%u", api_params->points * 3); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, hash); + rc_api_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_achievement_server_response(response, &response_obj); +} + +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->achievement_id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Update Leaderboard --- */ + +int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description) + return RC_INVALID_STATE; + if (!api_params->start_trigger || !*api_params->start_trigger) + return RC_INVALID_STATE; + if (!api_params->submit_trigger || !*api_params->submit_trigger) + return RC_INVALID_STATE; + if (!api_params->cancel_trigger || !*api_params->cancel_trigger) + return RC_INVALID_STATE; + if (!api_params->value_definition || !*api_params->value_definition) + return RC_INVALID_STATE; + if (!api_params->format || !*api_params->format) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadleaderboard", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->leaderboard_id) + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "s", api_params->start_trigger); + rc_url_builder_append_str_param(&builder, "b", api_params->submit_trigger); + rc_url_builder_append_str_param(&builder, "c", api_params->cancel_trigger); + rc_url_builder_append_str_param(&builder, "l", api_params->value_definition); + rc_url_builder_append_num_param(&builder, "w", api_params->lower_is_better); + rc_url_builder_append_str_param(&builder, "f", api_params->format); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->start_trigger, (int)strlen(api_params->start_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->submit_trigger, (int)strlen(api_params->submit_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->cancel_trigger, (int)strlen(api_params->cancel_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->value_definition, (int)strlen(api_params->value_definition)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + md5_append(&md5, (md5_byte_t*)api_params->format, (int)strlen(api_params->format)); + md5_finish(&md5, hash); + rc_api_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_leaderboard_server_response(response, &response_obj); +} + +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardID") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->leaderboard_id, &response->response, &fields[2], "LeaderboardID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Badge Range --- */ + +int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "badgeiter"); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + (void)api_params; + + return builder.result; +} + +int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_badge_range_server_response(response, &response_obj); +} + +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("FirstBadge"), + RC_JSON_NEW_FIELD("NextBadge") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->first_badge_id, &response->response, &fields[2], "FirstBadge")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->next_badge_id, &response->response, &fields[3], "NextBadge")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Add Game Hash --- */ + +int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + if (!api_params->hash || !*api_params->hash) + return RC_INVALID_STATE; + if (api_params->game_id == 0 && (!api_params->title || !*api_params->title)) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitgametitle", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + rc_url_builder_append_str_param(&builder, "m", api_params->hash); + if (api_params->title) + rc_url_builder_append_str_param(&builder, "i", api_params->title); + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + if (api_params->hash_description && *api_params->hash_description) + rc_url_builder_append_str_param(&builder, "d", api_params->hash_description); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_add_game_hash_server_response(response, &response_obj); +} + +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("GameID") + /* unused fields + RC_JSON_NEW_FIELD("MD5"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("Success") + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[0], "GameID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rapi/rc_api_info.c b/src/rcheevos/src/rapi/rc_api_info.c new file mode 100644 index 000000000..0ee4de79c --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_info.c @@ -0,0 +1,373 @@ +#include "rc_api_info.h" +#include "rc_api_common.h" + +#include "rc_runtime_types.h" + +#include +#include + +/* --- Fetch Achievement Info --- */ + +int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementwondata", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + + if (api_params->friends_only) + rc_url_builder_append_unum_param(&builder, "f", 1); + if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + rc_url_builder_append_unum_param(&builder, "c", api_params->count); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_achievement_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_achievement_awarded_entry_t* entry; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + unsigned timet; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("Response") + /* unused fields + RC_JSON_NEW_FIELD("Offset"), + RC_JSON_NEW_FIELD("Count"), + RC_JSON_NEW_FIELD("FriendsOnly") + * unused fields */ + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("NumEarned"), + RC_JSON_NEW_FIELD("TotalPlayers"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("RecentWinner") /* array */ + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("DateAwarded") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[3], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->num_awarded, &response->response, &response_fields[0], "NumEarned")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->num_players, &response->response, &response_fields[1], "TotalPlayers")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_array(&response->num_recently_awarded, &array_field, &response->response, &response_fields[3], "RecentWinner")) + return RC_MISSING_VALUE; + + if (response->num_recently_awarded) { + response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); + if (!response->recently_awarded) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->recently_awarded; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[1], "DateAwarded")) + return RC_MISSING_VALUE; + entry->awarded = (time_t)timet; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Leaderboard Info --- */ + +int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "lbinfo"); + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + + if (api_params->username) + rc_url_builder_append_str_param(&builder, "u", api_params->username); + else if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + + rc_url_builder_append_unum_param(&builder, "c", api_params->count); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_leaderboard_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_lboard_info_entry_t* entry; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + unsigned timet; + int result; + size_t len; + char format[16]; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardData") + }; + + rc_json_field_t leaderboarddata_fields[] = { + RC_JSON_NEW_FIELD("LBID"), + RC_JSON_NEW_FIELD("LBFormat"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("LBTitle"), + RC_JSON_NEW_FIELD("LBDesc"), + RC_JSON_NEW_FIELD("LBMem"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("LBAuthor"), + RC_JSON_NEW_FIELD("LBCreated"), + RC_JSON_NEW_FIELD("LBUpdated"), + RC_JSON_NEW_FIELD("Entries") /* array */ + /* unused fields + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ConsoleName"), + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("GameIcon") + * unused fields */ + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Index"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("DateSubmitted") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_object(leaderboarddata_fields, sizeof(leaderboarddata_fields) / sizeof(leaderboarddata_fields[0]), &response->response, &fields[2], "LeaderboardData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &leaderboarddata_fields[0], "LBID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &leaderboarddata_fields[3], "LBTitle")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->description, &response->response, &leaderboarddata_fields[4], "LBDesc")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->definition, &response->response, &leaderboarddata_fields[5], "LBMem")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &leaderboarddata_fields[6], "GameID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->author, &response->response, &leaderboarddata_fields[7], "LBAuthor")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->created, &response->response, &leaderboarddata_fields[8], "LBCreated")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->updated, &response->response, &leaderboarddata_fields[9], "LBUpdated")) + return RC_MISSING_VALUE; + + if (!leaderboarddata_fields[1].value_end) + return RC_MISSING_VALUE; + len = leaderboarddata_fields[1].value_end - leaderboarddata_fields[1].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboarddata_fields[1].value_start + 1, len); + format[len] = '\0'; + response->format = rc_parse_format(format); + } + else { + response->format = RC_FORMAT_VALUE; + } + + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &leaderboarddata_fields[10], "Entries")) + return RC_MISSING_VALUE; + + if (response->num_entries) { + response->entries = (rc_api_lboard_info_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->entries; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->index, &response->response, &entry_fields[2], "Index")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[3], "Score")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[4], "DateSubmitted")) + return RC_MISSING_VALUE; + entry->submitted = (time_t)timet; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Games List --- */ + +int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameslist"); + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_games_list_server_response(response, &response_obj); +} + +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_game_list_entry_t* entry; + rc_json_iterator_t iterator; + rc_json_field_t field; + int result; + char* end; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + rc_buf_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); + + response->entries = (rc_api_game_list_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->id = strtol(field.name, &end, 10); + + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) + return RC_MISSING_VALUE; + + ++entry; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rapi/rc_api_runtime.c b/src/rcheevos/src/rapi/rc_api_runtime.c new file mode 100644 index 000000000..7bac4ec53 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_runtime.c @@ -0,0 +1,599 @@ +#include "rc_api_runtime.h" +#include "rc_api_common.h" + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "../rcheevos/rc_compat.h" +#include "../rhash/md5.h" + +#include +#include +#include + +/* --- Resolve Hash --- */ + +int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (!api_params->game_hash || !*api_params->game_hash) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameid"); + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_resolve_hash_server_response(response, &response_obj); +} + +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("GameID") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID"); + return RC_OK; +} + +void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Game Data --- */ + +int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_game_data_server_response(response, &response_obj); +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_achievement_definition_t* achievement; + rc_api_leaderboard_definition_t* leaderboard; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + const char* str; + const char* last_author = ""; + const char* last_author_field = ""; + size_t last_author_len = 0; + size_t len; + unsigned timet; + int result; + char format[16]; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("PatchData") /* nested object */ + }; + + rc_json_field_t patchdata_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ + /* unused fields + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("Flags") + * unused fields */ + }; + + rc_json_field_t achievement_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Flags"), + RC_JSON_NEW_FIELD("Points"), + RC_JSON_NEW_FIELD("MemAddr"), + RC_JSON_NEW_FIELD("Author"), + RC_JSON_NEW_FIELD("BadgeName"), + RC_JSON_NEW_FIELD("Created"), + RC_JSON_NEW_FIELD("Modified") + }; + + rc_json_field_t leaderboard_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Mem"), + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("Hidden") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[2], "PatchData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID")) + return RC_MISSING_VALUE; + + /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ + if (patchdata_fields[3].value_end) { + str = patchdata_fields[3].value_end - 5; + if (memcmp(str, ".png\"", 5) == 0) { + patchdata_fields[3].value_end -= 5; + + while (str > patchdata_fields[3].value_start && str[-1] != '/') + --str; + + patchdata_fields[3].value_start = str; + } + } + rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", ""); + + /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards. + determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) + and add space for the structures. */ + len = patchdata_fields[4].value_end - patchdata_fields[4].value_start; /* rich presence */ + + len += (patchdata_fields[5].value_end - patchdata_fields[5].value_start) - /* achievements */ + patchdata_fields[5].array_size * (130 - sizeof(rc_api_achievement_definition_t)); + + len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* leaderboards */ + patchdata_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); + + rc_buf_reserve(&response->response.buffer, len); + /* end estimation */ + + rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[4], "RichPresencePatch", ""); + if (!response->rich_presence_script) + response->rich_presence_script = ""; + + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements")) + return RC_MISSING_VALUE; + + if (response->num_achievements) { + response->achievements = (rc_api_achievement_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); + if (!response->achievements) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + achievement = response->achievements; + while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName")) + return RC_MISSING_VALUE; + + len = achievement_fields[6].value_end - achievement_fields[6].value_start; + if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { + achievement->author = last_author; + } + else { + if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author")) + return RC_MISSING_VALUE; + + last_author = achievement->author; + last_author_field = achievement_fields[6].value_start; + last_author_len = len; + } + + if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created")) + return RC_MISSING_VALUE; + achievement->created = (time_t)timet; + if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified")) + return RC_MISSING_VALUE; + achievement->updated = (time_t)timet; + + ++achievement; + } + } + + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards")) + return RC_MISSING_VALUE; + + if (response->num_leaderboards) { + response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + if (!response->leaderboards) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + leaderboard = response->leaderboards; + while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem")) + return RC_MISSING_VALUE; + rc_json_get_optional_bool(&leaderboard->lower_is_better, &leaderboard_fields[5], "LowerIsBetter", 0); + rc_json_get_optional_bool(&leaderboard->hidden, &leaderboard_fields[6], "Hidden", 0); + + if (!leaderboard_fields[4].value_end) + return RC_MISSING_VALUE; + len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboard_fields[4].value_start + 1, len); + format[len] = '\0'; + leaderboard->format = rc_parse_format(format); + } + else { + leaderboard->format = RC_FORMAT_VALUE; + } + + ++leaderboard; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Ping --- */ + +int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + if (api_params->rich_presence && *api_params->rich_presence) + rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_ping_server_response(response, &response_obj); +} + +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); +} + +void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Award Achievement --- */ + +int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t digest[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 96); + if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); + if (api_params->game_hash && *api_params->game_hash) + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + /* Evaluate the signature. */ + md5_init(&md5); + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, digest); + rc_api_format_md5(buffer, digest); + rc_url_builder_append_str_param(&builder, "v", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_award_achievement_server_response(response, &response_obj); +} + +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("AchievementsRemaining") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!response->response.succeeded) { + if (response->response.error_message && + memcmp(response->response.error_message, "User already has", 16) == 0) { + /* not really an error, the achievement is unlocked, just not by the current call. + * hardcore: User already has hardcore and regular achievements awarded. + * non-hardcore: User already has this achievement awarded. + */ + response->response.succeeded = 1; + } else { + return result; + } + } + + rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); + rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1); + + return RC_OK; +} + +void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Submit Leaderboard Entry --- */ + +int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t digest[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 96); + if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_num_param(&builder, "s", api_params->score); + + if (api_params->game_hash && *api_params->game_hash) + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + /* Evaluate the signature. */ + md5_init(&md5); + snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + snprintf(buffer, sizeof(buffer), "%d", api_params->score); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, digest); + rc_api_format_md5(buffer, digest); + rc_url_builder_append_str_param(&builder, "v", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_submit_lboard_entry_server_response(response, &response_obj); +} + +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_lboard_entry_t* entry; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + const char* str; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") /* nested object */ + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("BestScore"), + RC_JSON_NEW_FIELD("RankInfo"), /* nested object */ + RC_JSON_NEW_FIELD("TopEntries") /* array */ + /* unused fields + RC_JSON_NEW_FIELD("LBData"), / * array * / + RC_JSON_NEW_FIELD("ScoreFormatted"), + RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * / + * unused fields */ + }; + + /* unused fields + rc_json_field_t lbdata_fields[] = { + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LeaderboardID"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("LowerIsBetter") + }; + * unused fields */ + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Score") + /* unused fields + RC_JSON_NEW_FIELD("DateSubmitted") + * unused fields */ + }; + + rc_json_field_t rank_info_fields[] = { + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("NumEntries") + /* unused fields + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("UserRank") + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries")) + return RC_MISSING_VALUE; + response->num_entries = (unsigned)atoi(str); + + if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries")) + return RC_MISSING_VALUE; + + if (response->num_top_entries) { + response->top_entries = (rc_api_lboard_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); + if (!response->top_entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->top_entries; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rapi/rc_api_user.c b/src/rcheevos/src/rapi/rc_api_user.c new file mode 100644 index 000000000..d1d76b20b --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_user.c @@ -0,0 +1,246 @@ +#include "rc_api_user.h" +#include "rc_api_common.h" + +#include "../rcheevos/rc_version.h" + +#include + +/* --- Login --- */ + +int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (!api_params->username || !*api_params->username) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "login"); + rc_url_builder_append_str_param(&builder, "u", api_params->username); + + if (api_params->password && api_params->password[0]) + rc_url_builder_append_str_param(&builder, "p", api_params->password); + else if (api_params->api_token && api_params->api_token[0]) + rc_url_builder_append_str_param(&builder, "t", api_params->api_token); + else + return RC_INVALID_STATE; + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_login_server_response(response, &response_obj); +} + +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Token"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), + RC_JSON_NEW_FIELD("Messages"), + RC_JSON_NEW_FIELD("DisplayName") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_string(&response->username, &response->response, &fields[2], "User")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[3], "Token")) + return RC_MISSING_VALUE; + + rc_json_get_optional_unum(&response->score, &fields[4], "Score", 0); + rc_json_get_optional_unum(&response->score_softcore, &fields[5], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[6], "Messages", 0); + + rc_json_get_optional_string(&response->display_name, &response->response, &fields[7], "DisplayName", response->username); + + return RC_OK; +} + +void rc_api_destroy_login_response(rc_api_login_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Start Session --- */ + +int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "startsession", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_start_session_server_response(response, &response_obj); +} + +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_unlock_entry_t* unlock; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + unsigned timet; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Unlocks"), + RC_JSON_NEW_FIELD("HardcoreUnlocks"), + RC_JSON_NEW_FIELD("ServerNow") + }; + + rc_json_field_t unlock_entry_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("When") + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (rc_json_get_optional_array(&response->num_unlocks, &array_field, &response->response, &fields[2], "Unlocks") && response->num_unlocks) { + response->unlocks = (rc_api_unlock_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + if (rc_json_get_optional_array(&response->num_hardcore_unlocks, &array_field, &response->response, &fields[3], "HardcoreUnlocks") && response->num_hardcore_unlocks) { + response->hardcore_unlocks = (rc_api_unlock_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_hardcore_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->hardcore_unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->hardcore_unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + rc_json_get_optional_unum(&timet, &fields[4], "ServerNow", 0); + response->server_now = (time_t)timet; + + return RC_OK; +} + +void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch User Unlocks --- */ + +int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "unlocks", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_user_unlocks_server_response(response, &response_obj); +} + +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("UserUnlocks") + /* unused fields + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("HardcoreMode") + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + result = rc_json_get_required_unum_array(&response->achievement_ids, &response->num_achievement_ids, &response->response, &fields[2], "UserUnlocks"); + return result; +} + +void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rcheevos/alloc.c b/src/rcheevos/src/rcheevos/alloc.c new file mode 100644 index 000000000..ac2a07bc0 --- /dev/null +++ b/src/rcheevos/src/rcheevos/alloc.c @@ -0,0 +1,211 @@ +#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_scratch_string_t*)rc_alloc_scratch(NULL, &used, sizeof(rc_scratch_string_t), RC_ALIGNOF(rc_scratch_string_t), &parse->scratch, RC_OFFSETOF(parse->scratch.objs, __rc_scratch_string_t)); + ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, length + 1, RC_ALIGNOF(char), &parse->scratch, -1); + + 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->lines_read = 0; + parse->has_required_hits = 0; + parse->measured_as_percent = 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; + } +} + +unsigned rc_djb2(const char* input) +{ + unsigned result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +const char* rc_error_str(int ret) +{ + switch (ret) { + case RC_OK: return "OK"; + case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; + case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; + case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; + case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; + case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; + case RC_INVALID_OPERATOR: return "Invalid operator"; + case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; + case RC_DUPLICATED_START: return "Duplicated start condition"; + case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; + case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; + case RC_DUPLICATED_VALUE: return "Duplicated value expression"; + case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; + case RC_MISSING_START: return "Missing start condition"; + case RC_MISSING_CANCEL: return "Missing cancel condition"; + case RC_MISSING_SUBMIT: return "Missing submit condition"; + case RC_MISSING_VALUE: return "Missing value expression"; + case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; + case RC_MISSING_DISPLAY_STRING: return "Missing display string"; + case RC_OUT_OF_MEMORY: return "Out of memory"; + case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; + case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; + case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; + case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; + case RC_INVALID_COMPARISON: return "Invalid comparison"; + case RC_INVALID_STATE: return "Invalid state"; + case RC_INVALID_JSON: return "Invalid JSON"; + case RC_API_FAILURE: return "API call failed"; + case RC_LOGIN_REQUIRED: return "Login required"; + case RC_NO_GAME_LOADED: return "No game loaded"; + case RC_HARDCORE_DISABLED: return "Hardcore disabled"; + case RC_ABORTED: return "Aborted"; + case RC_NO_RESPONSE: return "No response"; + default: return "Unknown error"; + } +} diff --git a/src/rcheevos/src/rcheevos/compat.c b/src/rcheevos/src/rcheevos/compat.c new file mode 100644 index 000000000..0ba7601ad --- /dev/null +++ b/src/rcheevos/src/rcheevos/compat.c @@ -0,0 +1,139 @@ +#include "rc_compat.h" + +#include +#include + +#ifdef RC_C89_HELPERS + +int rc_strncasecmp(const char* left, const char* right, size_t length) +{ + while (length) + { + if (*left != *right) + { + const int diff = tolower(*left) - tolower(*right); + if (diff != 0) + return diff; + } + + ++left; + ++right; + --length; + } + + return 0; +} + +int rc_strcasecmp(const char* left, const char* right) +{ + while (*left || *right) + { + if (*left != *right) + { + const int diff = tolower(*left) - tolower(*right); + if (diff != 0) + return diff; + } + + ++left; + ++right; + } + + return 0; +} + +char* rc_strdup(const char* str) +{ + const size_t length = strlen(str); + char* buffer = (char*)malloc(length + 1); + if (buffer) + memcpy(buffer, str, length + 1); + return buffer; +} + +int rc_snprintf(char* buffer, size_t size, const char* format, ...) +{ + int result; + va_list args; + + va_start(args, format); + +#ifdef __STDC_WANT_SECURE_LIB__ + result = vsprintf_s(buffer, size, format, args); +#else + /* assume buffer is large enough and ignore size */ + (void)size; + result = vsprintf(buffer, format, args); +#endif + + va_end(args); + + return result; +} + +#endif + +#ifndef __STDC_WANT_SECURE_LIB__ + +struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) +{ + struct tm* tm = gmtime(timer); + memcpy(buf, tm, sizeof(*tm)); + return buf; +} + +#endif + +#ifndef RC_NO_THREADS +#ifdef _WIN32 + +/* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */ + +#define WIN32_LEAN_AND_MEAN +#include + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* default security, not owned by calling thread, unnamed */ + mutex->handle = CreateMutex(NULL, FALSE, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + CloseHandle(mutex->handle); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + WaitForSingleObject(mutex->handle, 0xFFFFFFFF); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + ReleaseMutex(mutex->handle); +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + pthread_mutex_init(mutex, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + pthread_mutex_destroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + pthread_mutex_lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + pthread_mutex_unlock(mutex); +} + +#endif +#endif /* RC_NO_THREADS */ diff --git a/src/rcheevos/src/rcheevos/condition.c b/src/rcheevos/src/rcheevos/condition.c new file mode 100644 index 000000000..55a8084e7 --- /dev/null +++ b/src/rcheevos/src/rcheevos/condition.c @@ -0,0 +1,555 @@ +#include "rc_internal.h" + +#include +#include + +static int rc_test_condition_compare(unsigned value1, unsigned value2, char oper) { + switch (oper) { + case RC_OPERATOR_EQ: return value1 == value2; + case RC_OPERATOR_NE: return value1 != value2; + case RC_OPERATOR_LT: return value1 < value2; + case RC_OPERATOR_LE: return value1 <= value2; + case RC_OPERATOR_GT: return value1 > value2; + case RC_OPERATOR_GE: return value1 >= value2; + default: return 1; + } +} + +static char rc_condition_determine_comparator(const rc_condition_t* self) { + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + break; + + default: + /* not a comparison. should not be getting compared. but if it is, legacy behavior was to return 1 */ + return RC_PROCESSING_COMPARE_ALWAYS_TRUE; + } + + if ((self->operand1.type == RC_OPERAND_ADDRESS || self->operand1.type == RC_OPERAND_DELTA) && + !self->operand1.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand1)) { + /* left side is an integer memory reference */ + int needs_translate = (self->operand1.size != self->operand1.value.memref->value.size); + + if (self->operand2.type == RC_OPERAND_CONST) { + /* right side is a constant */ + if (self->operand1.type == RC_OPERAND_ADDRESS) + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_CONST; + + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_CONST; + } + else if ((self->operand2.type == RC_OPERAND_ADDRESS || self->operand2.type == RC_OPERAND_DELTA) && + !self->operand2.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand2)) { + /* right side is an integer memory reference */ + const int is_same_memref = (self->operand1.value.memref == self->operand2.value.memref); + needs_translate |= (self->operand2.size != self->operand2.value.memref->value.size); + + if (self->operand1.type == RC_OPERAND_ADDRESS) { + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref && !needs_translate) { + /* comparing a memref to itself, will evaluate to a constant */ + return rc_test_condition_compare(0, 0, self->oper) ? RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF; + } + + assert(self->operand2.type == RC_OPERAND_DELTA); + + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_DELTA; + } + } + else { + assert(self->operand1.type == RC_OPERAND_DELTA); + + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_MEMREF; + } + } + } + } + } + + if (self->operand1.type == RC_OPERAND_CONST && self->operand2.type == RC_OPERAND_CONST) { + /* comparing constants will always generate a constant result */ + return rc_test_condition_compare(self->operand1.value.num, self->operand2.value.num, self->oper) ? + RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return RC_PROCESSING_COMPARE_DEFAULT; +} + +static int rc_parse_operator(const char** memaddr) { + const char* oper = *memaddr; + + switch (*oper) { + case '=': + ++(*memaddr); + (*memaddr) += (**memaddr == '='); + return RC_OPERATOR_EQ; + + case '!': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_NE; + } + /* fall through */ + default: + return RC_INVALID_OPERATOR; + + case '<': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_LE; + } + + ++(*memaddr); + return RC_OPERATOR_LT; + + case '>': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_GE; + } + + ++(*memaddr); + return RC_OPERATOR_GT; + + case '*': + ++(*memaddr); + return RC_OPERATOR_MULT; + + case '/': + ++(*memaddr); + return RC_OPERATOR_DIV; + + case '&': + ++(*memaddr); + return RC_OPERATOR_AND; + + case '^': + ++(*memaddr); + return RC_OPERATOR_XOR; + + case '\0':/* end of string */ + case '_': /* next condition */ + case 'S': /* next condset */ + case ')': /* end of macro */ + case '$': /* maximum of values */ + /* valid condition separator, condition may not have an operator */ + return RC_OPERATOR_NONE; + } +} + +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect) { + rc_condition_t* self; + const char* aux; + int result; + int can_modify = 0; + + aux = *memaddr; + self = RC_ALLOC(rc_condition_t, parse); + self->current_hits = 0; + self->is_true = 0; + self->pause = 0; + self->optimized_comparator = RC_PROCESSING_COMPARE_DEFAULT; + + if (*aux != 0 && aux[1] == ':') { + switch (*aux) { + case 'p': case 'P': self->type = RC_CONDITION_PAUSE_IF; break; + case 'r': case 'R': self->type = RC_CONDITION_RESET_IF; break; + case 'a': case 'A': self->type = RC_CONDITION_ADD_SOURCE; can_modify = 1; break; + case 'b': case 'B': self->type = RC_CONDITION_SUB_SOURCE; can_modify = 1; break; + case 'c': case 'C': self->type = RC_CONDITION_ADD_HITS; break; + case 'd': case 'D': self->type = RC_CONDITION_SUB_HITS; break; + case 'n': case 'N': self->type = RC_CONDITION_AND_NEXT; break; + case 'o': case 'O': self->type = RC_CONDITION_OR_NEXT; break; + case 'm': case 'M': self->type = RC_CONDITION_MEASURED; break; + case 'q': case 'Q': self->type = RC_CONDITION_MEASURED_IF; break; + case 'i': case 'I': self->type = RC_CONDITION_ADD_ADDRESS; can_modify = 1; break; + case 't': case 'T': self->type = RC_CONDITION_TRIGGER; break; + case 'z': case 'Z': self->type = RC_CONDITION_RESET_NEXT_IF; break; + case 'g': case 'G': + parse->measured_as_percent = 1; + self->type = RC_CONDITION_MEASURED; + break; + /* e f h j k l s u v w x y */ + default: parse->offset = RC_INVALID_CONDITION_TYPE; return 0; + } + + aux += 2; + } + else { + self->type = RC_CONDITION_STANDARD; + } + + result = rc_parse_operand(&self->operand1, &aux, is_indirect, parse); + if (result < 0) { + parse->offset = result; + return 0; + } + + result = rc_parse_operator(&aux); + if (result < 0) { + parse->offset = result; + return 0; + } + + self->oper = (char)result; + switch (self->oper) { + case RC_OPERATOR_NONE: + /* non-modifying statements must have a second operand */ + if (!can_modify) { + /* measured does not require a second operand when used in a value */ + if (self->type != RC_CONDITION_MEASURED) { + parse->offset = RC_INVALID_OPERATOR; + return 0; + } + } + + /* provide dummy operand of '1' and no required hits */ + self->operand2.type = RC_OPERAND_CONST; + self->operand2.value.num = 1; + self->required_hits = 0; + *memaddr = aux; + return self; + + case RC_OPERATOR_MULT: + case RC_OPERATOR_DIV: + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + /* modifying operators are only valid on modifying statements */ + if (can_modify) + break; + /* fallthrough */ + + default: + /* comparison operators are not valid on modifying statements */ + if (can_modify) { + switch (self->type) { + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_ADDRESS: + /* prevent parse errors on legacy achievements where a condition was present before changing the type */ + self->oper = RC_OPERATOR_NONE; + break; + + default: + parse->offset = RC_INVALID_OPERATOR; + return 0; + } + } + break; + } + + result = rc_parse_operand(&self->operand2, &aux, is_indirect, parse); + if (result < 0) { + parse->offset = result; + return 0; + } + + if (self->oper == RC_OPERATOR_NONE) { + /* if operator is none, explicitly clear out the right side */ + self->operand2.type = RC_OPERAND_CONST; + self->operand2.value.num = 0; + } + + if (*aux == '(') { + char* end; + self->required_hits = (unsigned)strtoul(++aux, &end, 10); + + if (end == aux || *end != ')') { + parse->offset = RC_INVALID_REQUIRED_HITS; + return 0; + } + + /* if operator is none, explicitly clear out the required hits */ + if (self->oper == RC_OPERATOR_NONE) + self->required_hits = 0; + else + parse->has_required_hits = 1; + + aux = end + 1; + } + else if (*aux == '.') { + char* end; + self->required_hits = (unsigned)strtoul(++aux, &end, 10); + + if (end == aux || *end != '.') { + parse->offset = RC_INVALID_REQUIRED_HITS; + return 0; + } + + /* if operator is none, explicitly clear out the required hits */ + if (self->oper == RC_OPERATOR_NONE) + self->required_hits = 0; + else + parse->has_required_hits = 1; + + aux = end + 1; + } + else { + self->required_hits = 0; + } + + if (parse->buffer != 0) + self->optimized_comparator = rc_condition_determine_comparator(self); + + *memaddr = aux; + return self; +} + +int rc_condition_is_combining(const rc_condition_t* self) { + switch (self->type) { + case RC_CONDITION_STANDARD: + case RC_CONDITION_PAUSE_IF: + case RC_CONDITION_RESET_IF: + case RC_CONDITION_MEASURED_IF: + case RC_CONDITION_TRIGGER: + case RC_CONDITION_MEASURED: + return 0; + + default: + return 1; + } +} + +static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { + const unsigned value1 = self->operand1.value.memref->value.value; + const unsigned value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const(rc_condition_t* self) { + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const unsigned value1 = (memref1->changed) ? memref1->prior : memref1->value; + const unsigned value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref(rc_condition_t* self) { + const unsigned value1 = self->operand1.value.memref->value.value; + const unsigned value2 = self->operand2.value.memref->value.value; + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->value, memref->prior, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->prior, memref->value, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const unsigned value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const unsigned value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref_transformed(rc_condition_t* self) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = self->operand2.value.memref->value.value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->prior; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->prior; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { + rc_typed_value_t value1, value2; + + if (eval_state->add_value.type != RC_VALUE_TYPE_NONE) { + /* if there's an accumulator, we can't use the optimized comparators */ + rc_evaluate_operand(&value1, &self->operand1, eval_state); + rc_typed_value_add(&value1, &eval_state->add_value); + } else { + /* use an optimized comparator whenever possible */ + switch (self->optimized_comparator) { + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST: + return rc_test_condition_compare_memref_to_const(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA: + return rc_test_condition_compare_memref_to_delta(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF: + return rc_test_condition_compare_memref_to_memref(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST: + return rc_test_condition_compare_delta_to_const(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF: + return rc_test_condition_compare_delta_to_memref(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_memref_to_const_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED: + return rc_test_condition_compare_memref_to_delta_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_memref_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_delta_to_const_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_delta_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_ALWAYS_TRUE: + return 1; + case RC_PROCESSING_COMPARE_ALWAYS_FALSE: + return 0; + default: + rc_evaluate_operand(&value1, &self->operand1, eval_state); + break; + } + } + + rc_evaluate_operand(&value2, &self->operand2, eval_state); + + return rc_typed_value_compare(&value1, &value2, self->oper); +} + +void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state) { + rc_typed_value_t amount; + + rc_evaluate_operand(value, &self->operand1, eval_state); + rc_evaluate_operand(&amount, &self->operand2, eval_state); + + switch (self->oper) { + case RC_OPERATOR_MULT: + rc_typed_value_multiply(value, &amount); + break; + + case RC_OPERATOR_DIV: + rc_typed_value_divide(value, &amount); + break; + + case RC_OPERATOR_AND: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 &= amount.value.u32; + break; + + case RC_OPERATOR_XOR: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 ^= amount.value.u32; + break; + } +} diff --git a/src/rcheevos/src/rcheevos/condset.c b/src/rcheevos/src/rcheevos/condset.c new file mode 100644 index 000000000..e77a23581 --- /dev/null +++ b/src/rcheevos/src/rcheevos/condset.c @@ -0,0 +1,437 @@ +#include "rc_internal.h" + +#include /* memcpy */ + +static void rc_update_condition_pause(rc_condition_t* condition) { + rc_condition_t* subclause = condition; + + while (condition) { + if (condition->type == RC_CONDITION_PAUSE_IF) { + while (subclause != condition) { + subclause->pause = 1; + subclause = subclause->next; + } + condition->pause = 1; + } + else { + condition->pause = 0; + } + + if (!rc_condition_is_combining(condition)) + subclause = condition->next; + + condition = condition->next; + } +} + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, int is_value) { + rc_condset_t* self; + rc_condition_t** next; + int in_add_address; + unsigned measured_target = 0; + + self = RC_ALLOC(rc_condset_t, parse); + self->has_pause = self->is_paused = self->has_indirect_memrefs = 0; + next = &self->conditions; + + if (**memaddr == 'S' || **memaddr == 's' || !**memaddr) { + /* empty group - editor allows it, so we have to support it */ + *next = 0; + return self; + } + + in_add_address = 0; + for (;;) { + *next = rc_parse_condition(memaddr, parse, in_add_address); + + if (parse->offset < 0) { + return 0; + } + + if ((*next)->oper == RC_OPERATOR_NONE) { + switch ((*next)->type) { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + /* these conditions don't require a right hand size (implied *1) */ + break; + + case RC_CONDITION_MEASURED: + /* right hand side is not required when Measured is used in a value */ + if (is_value) + break; + /* fallthrough to default */ + + default: + parse->offset = RC_INVALID_OPERATOR; + return 0; + } + } + + self->has_pause |= (*next)->type == RC_CONDITION_PAUSE_IF; + in_add_address = (*next)->type == RC_CONDITION_ADD_ADDRESS; + self->has_indirect_memrefs |= in_add_address; + + switch ((*next)->type) { + case RC_CONDITION_MEASURED: + if (measured_target != 0) { + /* multiple Measured flags cannot exist in the same group */ + parse->offset = RC_MULTIPLE_MEASURED; + return 0; + } + else if (is_value) { + measured_target = (unsigned)-1; + switch ((*next)->oper) + { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_DIV: + case RC_OPERATOR_MULT: + case RC_OPERATOR_NONE: + /* measuring value. leave required_hits at 0 */ + break; + + default: + /* comparison operator, measuring hits. set required_hits to MAX_INT */ + (*next)->required_hits = measured_target; + break; + } + } + else if ((*next)->required_hits != 0) { + measured_target = (*next)->required_hits; + } + else if ((*next)->operand2.type == RC_OPERAND_CONST) { + measured_target = (*next)->operand2.value.num; + } + else if ((*next)->operand2.type == RC_OPERAND_FP) { + measured_target = (unsigned)(*next)->operand2.value.dbl; + } + else { + parse->offset = RC_INVALID_MEASURED_TARGET; + return 0; + } + + if (parse->measured_target && measured_target != parse->measured_target) { + /* multiple Measured flags in separate groups must have the same target */ + parse->offset = RC_MULTIPLE_MEASURED; + return 0; + } + + parse->measured_target = measured_target; + break; + + case RC_CONDITION_STANDARD: + case RC_CONDITION_TRIGGER: + /* these flags are not allowed in value expressions */ + if (is_value) { + parse->offset = RC_INVALID_VALUE_FLAG; + return 0; + } + break; + + default: + break; + } + + next = &(*next)->next; + + if (**memaddr != '_') { + break; + } + + (*memaddr)++; + } + + *next = 0; + + if (parse->buffer != 0) + rc_update_condition_pause(self->conditions); + + return self; +} + +static void rc_condset_update_indirect_memrefs(rc_condition_t* condition, int processing_pause, rc_eval_state_t* eval_state) { + for (; condition != 0; condition = condition->next) { + if (condition->pause != processing_pause) + continue; + + if (condition->type == RC_CONDITION_ADD_ADDRESS) { + rc_typed_value_t value; + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + eval_state->add_address = value.value.u32; + continue; + } + + /* call rc_get_memref_value to update the indirect memrefs. it won't do anything with non-indirect + * memrefs and avoids a second check of is_indirect. also, we ignore the response, so it doesn't + * matter what operand type we pass. assume RC_OPERAND_ADDRESS is the quickest. */ + if (rc_operand_is_memref(&condition->operand1)) + rc_get_memref_value(condition->operand1.value.memref, RC_OPERAND_ADDRESS, eval_state); + + if (rc_operand_is_memref(&condition->operand2)) + rc_get_memref_value(condition->operand2.value.memref, RC_OPERAND_ADDRESS, eval_state); + + eval_state->add_address = 0; + } +} + +static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc_eval_state_t* eval_state) { + rc_condition_t* condition; + rc_typed_value_t value; + int set_valid, cond_valid, and_next, or_next, reset_next, measured_from_hits, can_measure; + rc_typed_value_t measured_value; + unsigned total_hits; + + measured_value.type = RC_VALUE_TYPE_NONE; + measured_from_hits = 0; + can_measure = 1; + total_hits = 0; + + eval_state->primed = 1; + set_valid = 1; + and_next = 1; + or_next = 0; + reset_next = 0; + eval_state->add_value.type = RC_VALUE_TYPE_NONE; + eval_state->add_hits = eval_state->add_address = 0; + + for (condition = self->conditions; condition != 0; condition = condition->next) { + if (condition->pause != processing_pause) + continue; + + /* STEP 1: process modifier conditions */ + switch (condition->type) { + case RC_CONDITION_ADD_SOURCE: + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_add(&eval_state->add_value, &value); + eval_state->add_address = 0; + continue; + + case RC_CONDITION_SUB_SOURCE: + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_negate(&value); + rc_typed_value_add(&eval_state->add_value, &value); + eval_state->add_address = 0; + continue; + + case RC_CONDITION_ADD_ADDRESS: + rc_evaluate_condition_value(&value, condition, eval_state); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + eval_state->add_address = value.value.u32; + continue; + + case RC_CONDITION_MEASURED: + if (condition->required_hits == 0 && can_measure) { + /* Measured condition without a hit target measures the value of the left operand */ + rc_evaluate_condition_value(&measured_value, condition, eval_state); + rc_typed_value_add(&measured_value, &eval_state->add_value); + } + break; + + default: + break; + } + + /* STEP 2: evaluate the current condition */ + condition->is_true = (char)rc_test_condition(condition, eval_state); + eval_state->add_value.type = RC_VALUE_TYPE_NONE; + eval_state->add_address = 0; + + /* apply logic flags and reset them for the next condition */ + cond_valid = condition->is_true; + cond_valid &= and_next; + cond_valid |= or_next; + and_next = 1; + or_next = 0; + + if (reset_next) { + /* previous ResetNextIf resets the hit count on this condition and prevents it from being true */ + if (condition->current_hits) + eval_state->was_cond_reset = 1; + + condition->current_hits = 0; + cond_valid = 0; + } + else if (cond_valid) { + /* true conditions should update hit count */ + eval_state->has_hits = 1; + + if (condition->required_hits == 0) { + /* no target hit count, just keep tallying */ + ++condition->current_hits; + } + else if (condition->current_hits < condition->required_hits) { + /* target hit count hasn't been met, tally and revalidate - only true if hit count becomes met */ + ++condition->current_hits; + cond_valid = (condition->current_hits == condition->required_hits); + } + else { + /* target hit count has been met, do nothing */ + } + } + else if (condition->current_hits > 0) { + /* target has been true in the past, if the hit target is met, consider it true now */ + eval_state->has_hits = 1; + cond_valid = (condition->current_hits == condition->required_hits); + } + + /* STEP 3: handle logic flags */ + switch (condition->type) { + case RC_CONDITION_ADD_HITS: + eval_state->add_hits += condition->current_hits; + reset_next = 0; /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + continue; + + case RC_CONDITION_SUB_HITS: + eval_state->add_hits -= condition->current_hits; + reset_next = 0; /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + continue; + + case RC_CONDITION_RESET_NEXT_IF: + reset_next = cond_valid; + continue; + + case RC_CONDITION_AND_NEXT: + and_next = cond_valid; + continue; + + case RC_CONDITION_OR_NEXT: + or_next = cond_valid; + continue; + + default: + break; + } + + /* reset logic flags for next condition */ + reset_next = 0; + + /* STEP 4: calculate total hits */ + total_hits = condition->current_hits; + + if (eval_state->add_hits) { + if (condition->required_hits != 0) { + /* if the condition has a target hit count, we have to recalculate cond_valid including the AddHits counter */ + const int signed_hits = (int)condition->current_hits + eval_state->add_hits; + total_hits = (signed_hits >= 0) ? (unsigned)signed_hits : 0; + cond_valid = (total_hits >= condition->required_hits); + } + else { + /* no target hit count. we can't tell if the add_hits value is from this frame or not, so ignore it. + complex condition will only be true if the current condition is true */ + } + + eval_state->add_hits = 0; + } + + /* STEP 5: handle special flags */ + switch (condition->type) { + case RC_CONDITION_PAUSE_IF: + /* as soon as we find a PauseIf that evaluates to true, stop processing the rest of the group */ + if (cond_valid) { + /* indirect memrefs are not updated as part of the rc_update_memref_values call. + * an active pause aborts processing of the remaining part of the pause subset and the entire non-pause subset. + * if the set has any indirect memrefs, manually update them now so the deltas are correct */ + if (self->has_indirect_memrefs) { + /* first, update any indirect memrefs in the remaining part of the pause subset */ + rc_condset_update_indirect_memrefs(condition->next, 1, eval_state); + + /* then, update all indirect memrefs in the non-pause subset */ + rc_condset_update_indirect_memrefs(self->conditions, 0, eval_state); + } + + return 1; + } + + /* if we make it to the end of the function, make sure we indicate that nothing matched. if we do find + a later PauseIf match, it'll automatically return true via the previous condition. */ + set_valid = 0; + + if (condition->required_hits == 0) { + /* PauseIf didn't evaluate true, and doesn't have a HitCount, reset the HitCount to indicate the condition didn't match */ + condition->current_hits = 0; + } + else { + /* PauseIf has a HitCount that hasn't been met, ignore it for now. */ + } + + continue; + + case RC_CONDITION_RESET_IF: + if (cond_valid) { + eval_state->was_reset = 1; /* let caller know to reset all hit counts */ + set_valid = 0; /* cannot be valid if we've hit a reset condition */ + } + continue; + + case RC_CONDITION_MEASURED: + if (condition->required_hits != 0) { + /* if there's a hit target, capture the current hits for recording Measured value later */ + measured_from_hits = 1; + if (can_measure) { + measured_value.value.u32 = total_hits; + measured_value.type = RC_VALUE_TYPE_UNSIGNED; + } + } + break; + + case RC_CONDITION_MEASURED_IF: + if (!cond_valid) { + measured_value.value.u32 = 0; + measured_value.type = RC_VALUE_TYPE_UNSIGNED; + can_measure = 0; + } + break; + + case RC_CONDITION_TRIGGER: + /* update truthiness of set, but do not update truthiness of primed state */ + set_valid &= cond_valid; + continue; + + default: + break; + } + + /* STEP 5: update overall truthiness of set and primed state */ + eval_state->primed &= cond_valid; + set_valid &= cond_valid; + } + + if (measured_value.type != RC_VALUE_TYPE_NONE) { + /* if no previous Measured value was captured, or the new one is greater, keep the new one */ + if (eval_state->measured_value.type == RC_VALUE_TYPE_NONE || + rc_typed_value_compare(&measured_value, &eval_state->measured_value, RC_OPERATOR_GT)) { + memcpy(&eval_state->measured_value, &measured_value, sizeof(measured_value)); + eval_state->measured_from_hits = (char)measured_from_hits; + } + } + + return set_valid; +} + +int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state) { + if (self->conditions == 0) { + /* important: empty group must evaluate true */ + return 1; + } + + if (self->has_pause) { + /* one or more Pause conditions exists, if any of them are true, stop processing this group */ + self->is_paused = (char)rc_test_condset_internal(self, 1, eval_state); + if (self->is_paused) { + eval_state->primed = 0; + return 0; + } + } + + return rc_test_condset_internal(self, 0, eval_state); +} + +void rc_reset_condset(rc_condset_t* self) { + rc_condition_t* condition; + + for (condition = self->conditions; condition != 0; condition = condition->next) { + condition->current_hits = 0; + } +} diff --git a/src/rcheevos/src/rcheevos/consoleinfo.c b/src/rcheevos/src/rcheevos/consoleinfo.c new file mode 100644 index 000000000..0ef0089e8 --- /dev/null +++ b/src/rcheevos/src/rcheevos/consoleinfo.c @@ -0,0 +1,1060 @@ +#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_ARCADIA_2001: + return "Arcadia 2001"; + + case RC_CONSOLE_ARDUBOY: + return "Arduboy"; + + case RC_CONSOLE_ATARI_2600: + return "Atari 2600"; + + case RC_CONSOLE_ATARI_5200: + return "Atari 5200"; + + case RC_CONSOLE_ATARI_7800: + return "Atari 7800"; + + case RC_CONSOLE_ATARI_JAGUAR: + return "Atari Jaguar"; + + case RC_CONSOLE_ATARI_JAGUAR_CD: + return "Atari Jaguar CD"; + + case RC_CONSOLE_ATARI_LYNX: + return "Atari Lynx"; + + case RC_CONSOLE_ATARI_ST: + return "Atari ST"; + + case RC_CONSOLE_CASSETTEVISION: + return "CassetteVision"; + + case RC_CONSOLE_CDI: + return "CD-I"; + + case RC_CONSOLE_COLECOVISION: + return "ColecoVision"; + + case RC_CONSOLE_COMMODORE_64: + return "Commodore 64"; + + case RC_CONSOLE_DREAMCAST: + return "Dreamcast"; + + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + return "Elektor TV Games Computer"; + + case RC_CONSOLE_EVENTS: + return "Events"; + + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return "Fairchild Channel F"; + + case RC_CONSOLE_FM_TOWNS: + return "FM Towns"; + + case RC_CONSOLE_GAME_AND_WATCH: + return "Game & Watch"; + + case RC_CONSOLE_GAMEBOY: + return "GameBoy"; + + case RC_CONSOLE_GAMEBOY_ADVANCE: + return "GameBoy Advance"; + + case RC_CONSOLE_GAMEBOY_COLOR: + return "GameBoy Color"; + + case RC_CONSOLE_GAMECUBE: + return "GameCube"; + + case RC_CONSOLE_GAME_GEAR: + return "Game Gear"; + + case RC_CONSOLE_HUBS: + return "Hubs"; + + case RC_CONSOLE_INTELLIVISION: + return "Intellivision"; + + case RC_CONSOLE_INTERTON_VC_4000: + return "Interton VC 4000"; + + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + return "Magnavox Odyssey 2"; + + case RC_CONSOLE_MASTER_SYSTEM: + return "Master System"; + + case RC_CONSOLE_MEGA_DRIVE: + return "Sega Genesis"; + + case RC_CONSOLE_MEGADUCK: + return "Mega Duck"; + + case RC_CONSOLE_MS_DOS: + return "MS-DOS"; + + case RC_CONSOLE_MSX: + return "MSX"; + + case RC_CONSOLE_NEO_GEO_CD: + return "Neo Geo CD"; + + case RC_CONSOLE_NEOGEO_POCKET: + return "Neo Geo Pocket"; + + case RC_CONSOLE_NINTENDO: + return "Nintendo Entertainment System"; + + case RC_CONSOLE_NINTENDO_64: + return "Nintendo 64"; + + case RC_CONSOLE_NINTENDO_DS: + return "Nintendo DS"; + + case RC_CONSOLE_NINTENDO_DSI: + return "Nintendo DSi"; + + case RC_CONSOLE_NINTENDO_3DS: + return "Nintendo 3DS"; + + case RC_CONSOLE_NOKIA_NGAGE: + return "Nokia N-Gage"; + + case RC_CONSOLE_ORIC: + return "Oric"; + + case RC_CONSOLE_PC6000: + return "PC-6000"; + + case RC_CONSOLE_PC8800: + return "PC-8000/8800"; + + case RC_CONSOLE_PC9800: + return "PC-9800"; + + case RC_CONSOLE_PCFX: + return "PC-FX"; + + case RC_CONSOLE_PC_ENGINE: + return "PC Engine"; + + case RC_CONSOLE_PC_ENGINE_CD: + return "PC Engine CD"; + + case RC_CONSOLE_PLAYSTATION: + return "PlayStation"; + + case RC_CONSOLE_PLAYSTATION_2: + return "PlayStation 2"; + + case RC_CONSOLE_PSP: + return "PlayStation Portable"; + + case RC_CONSOLE_POKEMON_MINI: + return "Pokemon Mini"; + + case RC_CONSOLE_SEGA_32X: + return "Sega 32X"; + + case RC_CONSOLE_SEGA_CD: + return "Sega CD"; + + case RC_CONSOLE_PICO: + return "Sega Pico"; + + case RC_CONSOLE_SATURN: + return "Sega Saturn"; + + case RC_CONSOLE_SG1000: + return "SG-1000"; + + case RC_CONSOLE_SHARPX1: + return "Sharp X1"; + + case RC_CONSOLE_SUPER_NINTENDO: + return "Super Nintendo Entertainment System"; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return "Super CassetteVision"; + + case RC_CONSOLE_SUPERVISION: + return "Watara Supervision"; + + case RC_CONSOLE_THOMSONTO8: + return "Thomson TO8"; + + case RC_CONSOLE_TI83: + return "TI-83"; + + case RC_CONSOLE_TIC80: + return "TIC-80"; + + case RC_CONSOLE_UZEBOX: + return "Uzebox"; + + case RC_CONSOLE_VECTREX: + return "Vectrex"; + + case RC_CONSOLE_VIC20: + return "VIC-20"; + + case RC_CONSOLE_VIRTUAL_BOY: + return "Virtual Boy"; + + case RC_CONSOLE_WASM4: + return "WASM-4"; + + case RC_CONSOLE_WII: + return "Wii"; + + case RC_CONSOLE_WII_U: + return "Wii-U"; + + case RC_CONSOLE_WONDERSWAN: + return "WonderSwan"; + + case RC_CONSOLE_X68K: + return "X68K"; + + case RC_CONSOLE_XBOX: + return "XBOX"; + + case RC_CONSOLE_ZEEBO: + return "Zeebo"; + + case RC_CONSOLE_ZX81: + return "ZX-81"; + + case RC_CONSOLE_ZX_SPECTRUM: + return "ZX Spectrum"; + + default: + return "Unknown"; + } +} + +/* ===== 3DO ===== */ +/* http://www.arcaderestoration.com/memorymap/48/3DO+Bios.aspx */ +/* NOTE: the Opera core attempts to expose the NVRAM as RETRO_SAVE_RAM, but the 3DO documentation + * says that applications should only access NVRAM through API calls as it's shared across mulitple + * games. This suggests that even if the core does expose it, it may change depending on which other + * games the user has played - so ignore it. + */ +static const rc_memory_region_t _rc_memory_regions_3do[] = { + { 0x000000U, 0x1FFFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_3do = { _rc_memory_regions_3do, 1 }; + +/* ===== Amiga ===== */ +/* http://amigadev.elowar.com/read/ADCD_2.1/Hardware_Manual_guide/node00D3.html */ +static const rc_memory_region_t _rc_memory_regions_amiga[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, /* 512KB main RAM */ + { 0x080000U, 0x0FFFFFU, 0x080000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, /* 512KB extended RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_amiga = { _rc_memory_regions_amiga, 2 }; + +/* ===== Amstrad CPC ===== */ +/* http://www.cpcalive.com/docs/amstrad_cpc_6128_memory_map.html */ +/* https://www.cpcwiki.eu/index.php/File:AWMG_page151.jpg */ +/* The original CPC only had 64KB of memory, but the newer model has 128KB (expandable to 576KB) */ +/* https://www.grimware.org/doku.php/documentations/devices/gatearraydo=export_xhtml#mmr */ +static const rc_memory_region_t _rc_memory_regions_amstrad_pc[] = { + { 0x000000U, 0x00003FU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Firmware" }, + { 0x000040U, 0x00B0FFU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00B100U, 0x00BFFFU, 0x00B100U, RC_MEMORY_TYPE_SYSTEM_RAM, "Stack and Firmware Data" }, + { 0x00C000U, 0x00FFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen Memory" }, + { 0x010000U, 0x08FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_amstrad_pc = { _rc_memory_regions_amstrad_pc, 5 }; + +/* ===== Apple II ===== */ +static const rc_memory_region_t _rc_memory_regions_appleii[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, + { 0x010000U, 0x01FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Auxillary RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_appleii = { _rc_memory_regions_appleii, 2 }; + +/* ===== Arcadia 2001 ===== */ +/* https://amigan.yatho.com/a-coding.txt */ +/* RAM banks 1 and 2 only exist on some variant models - no game actually uses them */ +static const rc_memory_region_t _rc_memory_regions_arcadia_2001[] = { + { 0x000000U, 0x0000FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 3 */ + { 0x000100U, 0x0001FFU, 0x001900U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, + { 0x000200U, 0x0002FFU, 0x001A00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 4 */ +}; +static const rc_memory_regions_t rc_memory_regions_arcadia_2001 = { _rc_memory_regions_arcadia_2001, 3 }; + +/* ===== Arduboy ===== */ +/* https://scienceprog.com/avr-microcontroller-memory-map/ (Atmega32) */ +static const rc_memory_region_t _rc_memory_regions_arduboy[] = { + { 0x000000U, 0x0000FFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Registers" }, + /* https://www.dailydot.com/debug/arduboy-kickstarter/ 2.5KB of RAM */ + /* https://github.com/buserror/simavr/blob/1d227277b3d0039f9faef9ea62880ca3051b14f8/simavr/cores/avr/iom32u4.h#L1444-L1445 */ + { 0x000100U, 0x000AFFU, 0x00000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* 1KB of EEPROM https://github.com/libretro/arduous/blob/93e1a6289b42ef48de1fcfb96443981725955ad0/src/arduous/arduous.cpp#L453-L455 + * https://github.com/buserror/simavr/blob/1d227277b3d0039f9faef9ea62880ca3051b14f8/simavr/cores/avr/iom32u4.h#L1450 */ + /* EEPROM has it's own addressing scheme starting at $0000. I've chosen to virtualize the address + * at $80000000 to avoid a conflict */ + { 0x000B00U, 0x000EFFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "EEPROM" } +}; +static const rc_memory_regions_t rc_memory_regions_arduboy = { _rc_memory_regions_arduboy, 3 }; + +/* ===== Atari 2600 ===== */ +static const rc_memory_region_t _rc_memory_regions_atari2600[] = { + { 0x000000U, 0x00007FU, 0x000080U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari2600 = { _rc_memory_regions_atari2600, 1 }; + +/* ===== Atari 7800 ===== */ +/* http://www.atarihq.com/danb/files/78map.txt */ +/* http://pdf.textfiles.com/technical/7800_devkit.pdf */ +static const rc_memory_region_t _rc_memory_regions_atari7800[] = { + { 0x000000U, 0x0017FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware Interface" }, + { 0x001800U, 0x0027FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x002800U, 0x002FFFU, 0x002800U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x003000U, 0x0037FFU, 0x003000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x003800U, 0x003FFFU, 0x003800U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x008000U, 0x00FFFFU, 0x008000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari7800 = { _rc_memory_regions_atari7800, 7 }; + +/* ===== Atari Jaguar ===== */ +/* https://www.mulle-kybernetik.com/jagdox/memorymap.html */ +static const rc_memory_region_t _rc_memory_regions_atari_jaguar[] = { + { 0x000000U, 0x1FFFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari_jaguar = { _rc_memory_regions_atari_jaguar, 1 }; + +/* ===== Atari Lynx ===== */ +/* http://www.retroisle.com/atari/lynx/Technical/Programming/lynxprgdumm.php */ +static const rc_memory_region_t _rc_memory_regions_atari_lynx[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Zero Page" }, + { 0x000100U, 0x0001FFU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "Stack" }, + { 0x000200U, 0x00FBFFU, 0x000200U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00FC00U, 0x00FCFFU, 0x00FC00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "SUZY hardware access" }, + { 0x00FD00U, 0x00FDFFU, 0x00FD00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "MIKEY hardware access" }, + { 0x00FE00U, 0x00FFF7U, 0x00FE00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Boot ROM" }, + { 0x00FFF8U, 0x00FFFFU, 0x00FFF8U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware vectors" } +}; +static const rc_memory_regions_t rc_memory_regions_atari_lynx = { _rc_memory_regions_atari_lynx, 7 }; + +/* ===== ColecoVision ===== */ +static const rc_memory_region_t _rc_memory_regions_colecovision[] = { + { 0x000000U, 0x0003FFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_regions_colecovision, 1 }; + +/* ===== Commodore 64 ===== */ +/* https://www.c64-wiki.com/wiki/Memory_Map */ +/* https://sta.c64.org/cbm64mem.html */ +static const rc_memory_region_t _rc_memory_regions_c64[] = { + { 0x000000U, 0x0003FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x000400U, 0x0007FFU, 0x000400U, RC_MEMORY_TYPE_VIDEO_RAM, "Screen RAM" }, + { 0x000800U, 0x009FFFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* BASIC Program Storage Area */ + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / BASIC ROM Area */ + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area */ + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "I/O Area" }, /* also Character ROM */ + { 0x00E000U, 0x00FFFFU, 0x00E000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / Kernal ROM */ +}; +static const rc_memory_regions_t rc_memory_regions_c64 = { _rc_memory_regions_c64, 7 }; + +/* ===== Dreamcast ===== */ +/* http://archiv.sega-dc.de/munkeechuff/hardware/Memory.html */ +static const rc_memory_region_t _rc_memory_regions_dreamcast[] = { + { 0x00000000U, 0x00FFFFFFU, 0x0C000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_dreamcast = { _rc_memory_regions_dreamcast, 1 }; + +/* ===== Elektor TV Games Computer ===== */ +/* https://amigan.yatho.com/e-coding.txt */ +static const rc_memory_region_t _rc_memory_regions_elektor_tv_games[] = { + { 0x000000U, 0x0013FFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x001400U, 0x0014FFU, 0x001C00U, RC_MEMORY_TYPE_UNUSED, "Unused" }, /* mirror of $1D00-$1DFF */ + { 0x001500U, 0x0016FFU, 0x001D00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, /* two 256-byte I/O areas */ + { 0x001700U, 0x0017FFU, 0x001F00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_elektor_tv_games = { _rc_memory_regions_elektor_tv_games, 4 }; + +/* ===== Fairchild Channel F ===== */ +static const rc_memory_region_t _rc_memory_regions_fairchild_channel_f[] = { + /* "System RAM" is actually just a bunch of registers internal to CPU so all carts have it. + * "Video RAM" is part of the console so it's always available but it is write-only by the ROMs. + * "Cartridge RAM" is the cart BUS. Most carts only have ROMs on this bus. Exception are + * German Schach and homebrew carts that have 2K of RAM there in addition to ROM. + * "F2102 RAM" is used by Maze for 1K of RAM. + * https://discord.com/channels/310192285306454017/645777658319208448/967001438087708714 */ + { 0x00000000U, 0x0000003FU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00000040U, 0x0000083FU, 0x00300000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x00000840U, 0x0001083FU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x00010840U, 0x00010C3FU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "F2102 RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_fairchild_channel_f = { _rc_memory_regions_fairchild_channel_f, 4 }; + +/* ===== GameBoy / GameBoy Color ===== */ +static const rc_memory_region_t _rc_memory_regions_gameboy[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt vector" }, + { 0x000100U, 0x00014FU, 0x000100U, RC_MEMORY_TYPE_READONLY, "Cartridge header" }, + { 0x000150U, 0x003FFFU, 0x000150U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (fixed)" }, /* bank 0 */ + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (paged)" }, /* bank 1-XX (switchable) */ + { 0x008000U, 0x0097FFU, 0x008000U, RC_MEMORY_TYPE_VIDEO_RAM, "Tile RAM" }, + { 0x009800U, 0x009BFFU, 0x009800U, RC_MEMORY_TYPE_VIDEO_RAM, "BG1 map data" }, + { 0x009C00U, 0x009FFFU, 0x009C00U, RC_MEMORY_TYPE_VIDEO_RAM, "BG2 map data" }, + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM"}, + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (fixed)" }, + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (bank 1)" }, + { 0x00E000U, 0x00FDFFU, 0x00C000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Echo RAM" }, + { 0x00FE00U, 0x00FE9FU, 0x00FE00U, RC_MEMORY_TYPE_VIDEO_RAM, "Sprite RAM"}, + { 0x00FEA0U, 0x00FEFFU, 0x00FEA0U, RC_MEMORY_TYPE_UNUSED, ""}, + { 0x00FF00U, 0x00FF7FU, 0x00FF00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware I/O"}, + { 0x00FF80U, 0x00FFFEU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Quick RAM"}, + { 0x00FFFFU, 0x00FFFFU, 0x00FFFFU, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt enable"}, + + /* GameBoy Color provides six extra banks of memory that can be paged out through the $DXXX + * memory space, but the timing of that does not correspond with blanks, which is when achievements + * are processed. As such, it is desirable to always have access to these extra banks. We do this + * by expecting the extra banks to be addressable at addresses not supported by the native system. */ + { 0x010000U, 0x015FFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (banks 2-7, GBC only)" } +}; +static const rc_memory_regions_t rc_memory_regions_gameboy = { _rc_memory_regions_gameboy, 16 }; +static const rc_memory_regions_t rc_memory_regions_gameboy_color = { _rc_memory_regions_gameboy, 17 }; + +/* ===== GameBoy Advance ===== */ +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 }; + +/* ===== GameCube ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_gamecube[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_gamecube = { _rc_memory_regions_gamecube, 1 }; + +/* ===== Game Gear ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_game_gear[] = { + { 0x000000U, 0x001FFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_game_gear = { _rc_memory_regions_game_gear, 1 }; + +/* ===== Intellivision ===== */ +/* http://wiki.intellivision.us/index.php/Memory_Map */ +/* NOTE: Intellivision memory addresses point at 16-bit values. FreeIntv exposes them as little-endian + * 32-bit values. As such, the addresses are off by a factor of 4 _and_ the data is only where we + * expect it on little-endian systems. + */ +static const rc_memory_region_t _rc_memory_regions_intellivision[] = { + /* For backwards compatibility, register a 128-byte chunk of video RAM so the system memory + * will start at $0080. $0000-$007F previously tried to map to the STIC video registers as + * RETRO_MEMORY_VIDEO_RAM, and FreeIntv didn't expose any RETRO_MEMORY_VIDEO_RAM, so the first + * byte of RETRO_MEMORY_SYSTEM_RAM was registered at $0080. The data at $0080 is actually the + * STIC registers (4 bytes each), so we need to provide an arbitrary 128-byte padding that + * claims to be video RAM to ensure the system RAM ends up at the right address. + */ + { 0x000000U, 0x00007FU, 0xFFFFFFU, RC_MEMORY_TYPE_VIDEO_RAM, "" }, + + /* RetroAchievements address = real address x4 + 0x80. + * These all have to map to RETRO_MEMORY_SYSTEM_RAM (even the video-related fields) as the + * entire block is exposed as a single entity by FreeIntv */ + + /* $0000-$007F: STIC registers, $0040-$007F are readonly */ + { 0x000080U, 0x00027FU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "STIC Registers" }, + /* $0080-$00FF: unused */ + { 0x000280U, 0x00047FU, 0x000080U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $0100-$035F: system RAM, $0100-$01EF is scratch memory and only 8-bits per address */ + { 0x000480U, 0x000DFFU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* $0360-$03FF: unused */ + { 0x000E00U, 0x00107FU, 0x000360U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $0400-$0FFF: cartridge RAM */ + { 0x001080U, 0x00407FU, 0x000400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + /* $1000-$1FFF: unused */ + { 0x004080U, 0x00807FU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $2000-$2FFF: cartridge RAM */ + { 0x008080U, 0x00C07FU, 0x002000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + /* $3000-$3FFF: video RAM */ + { 0x00C080U, 0x01007FU, 0x003000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Video RAM" }, + /* $4000-$FFFF: cartridge RAM */ + { 0x010080U, 0x04007FU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_intellivision = { _rc_memory_regions_intellivision, 10 }; + +/* ===== Interton VC 4000 ===== */ +/* https://amigan.yatho.com/i-coding.txt */ +/* Cartridge RAM is not persisted, it's just expanded storage */ +static const rc_memory_region_t _rc_memory_regions_interton_vc_4000[] = { + { 0x000000U, 0x0003FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x000400U, 0x0004FFU, 0x001E00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, + { 0x000500U, 0x0005FFU, 0x001F00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_interton_vc_4000 = { _rc_memory_regions_interton_vc_4000, 3 }; + +/* ===== Magnavox Odyssey 2 ===== */ +/* https://sudonull.com/post/76885-Architecture-and-programming-Philips-Videopac-Magnavox-Odyssey-2 */ +static const rc_memory_region_t _rc_memory_regions_magnavox_odyssey_2[] = { + /* Internal and external RAMs are reachable using unique instructions. + * The real addresses provided are virtual and for mapping purposes only. */ + { 0x000000U, 0x00003FU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Internal RAM" }, + { 0x000040U, 0x00013FU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "External RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_magnavox_odyssey_2 = { _rc_memory_regions_magnavox_odyssey_2, 2 }; + +/* ===== Master System ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_master_system[] = { + { 0x000000U, 0x001FFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_master_system = { _rc_memory_regions_master_system, 1 }; + +/* ===== MegaDrive (Genesis) ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_megadrive[] = { + { 0x000000U, 0x00FFFFU, 0xFF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x010000U, 0x01FFFFU, 0x000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; + +/* ===== MegaDrive 32X (Genesis 32X) ===== */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ +static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Main MegaDrive RAM */ + { 0x010000U, 0x04FFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, /* Additional 32X RAM */ + { 0x050000U, 0x05FFFFU, 0x00000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive_32x = { _rc_memory_regions_megadrive_32x, 3 }; + +/* ===== MSX ===== */ +/* https://www.msx.org/wiki/The_Memory */ +/* MSX only has 64KB of addressable RAM, of which 32KB is reserved for the system/BIOS. + * However, the system has up to 512KB of RAM, which is paged into the addressable RAM + * We expect the raw RAM to be exposed, rather than force the devs to worry about the + * paging system. The entire RAM is expected to appear starting at $10000, which is not + * addressable by the system itself. + */ +static const rc_memory_region_t _rc_memory_regions_msx[] = { + { 0x000000U, 0x07FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_msx = { _rc_memory_regions_msx, 1 }; + +/* ===== Neo Geo Pocket ===== */ +/* http://neopocott.emuunlim.com/docs/tech-11.txt */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = { + /* The docs suggest there's Work RAM exposed from $0000-$6FFF, Sound RAM from $7000-$7FFF, and Video + * RAM from $8000-$BFFF, but both MednafenNGP and FBNeo only expose system RAM from $4000-$7FFF */ + { 0x000000U, 0x003FFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_pocket = { _rc_memory_regions_neo_geo_pocket, 1 }; + +/* ===== Neo Geo CD ===== */ +/* https://wiki.neogeodev.org/index.php?title=68k_memory_map */ +/* NeoCD exposes $000000-$1FFFFF as System RAM, but it seems like only the WORKRAM section is used. + * This is consistent with http://www.hardmvs.fr/manuals/NeoGeoProgrammersGuide.pdf (page25), which says: + * + * Furthermore, the NEO-GEO provides addresses 100000H-10FFFFH as a work area, out of which the + * addresses 10F300H-10FFFFH are reserved exclusively for use by the system program. Therefore, + * every game is to use addresses 100000H-10F2FFH. + * + * Also note that PRG files (game ROM) can be loaded anywhere else in the $000000-$1FFFFF range. + * AoF3 illustrates this pretty clearly: https://wiki.neogeodev.org/index.php?title=IPL_file + * + * PROG_CD.PRG,0,0 + * PROG_CDX.PRG,0,058000 + * CNV_NM.PRG,0,0C0000 + * FIX_DATA.PRG,0,0FD000 + * OBJACTLK.PRG,0,130000 + * SSEL_CNV.PRG,0,15A000 + * SSEL_BAK.PRG,0,16F000 + * HITMSG.PRG,0,170000 + * SSEL_SPR.PRG,0,19D000 + */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_cd[] = { + { 0x000000U, 0x00F2FFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* NOTE: some BIOS settings are exposed through the reserved RAM: https://wiki.neogeodev.org/index.php?title=68k_ASM_defines */ + { 0x00F300U, 0x00FFFFU, 0x0010F300U, RC_MEMORY_TYPE_SYSTEM_RAM, "Reserved RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_cd = { _rc_memory_regions_neo_geo_cd, 2 }; + +/* ===== Nintendo Entertainment System ===== */ +/* https://wiki.nesdev.com/w/index.php/CPU_memory_map */ +static const rc_memory_region_t _rc_memory_regions_nes[] = { + { 0x0000U, 0x07FFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x0800U, 0x0FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1000U, 0x17FFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1800U, 0x1FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x2000U, 0x2007U, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "PPU Register" }, + { 0x2008U, 0x3FFFU, 0x2008U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored PPU Register" }, /* repeats every 8 bytes */ + { 0x4000U, 0x4017U, 0x4000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O register" }, + { 0x4018U, 0x401FU, 0x4018U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O test register" }, + + /* NOTE: these are for the original NES/Famicom */ + { 0x4020U, 0x5FFFU, 0x4020U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, /* varies by mapper */ + { 0x6000U, 0x7FFFU, 0x6000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM"}, + { 0x8000U, 0xFFFFU, 0x8000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM"}, + + /* NOTE: these are the correct mappings for FDS: https://fms.komkon.org/EMUL8/NES.html + * 0x6000-0xDFFF is RAM on the FDS system and 0xE000-0xFFFF is FDS BIOS. + * If the core implements a memory map, we should still be able to translate the addresses + * correctly as we only use the classifications when a memory map is not provided + + { 0x4020U, 0x40FFU, 0x4020U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "FDS I/O registers"}, + { 0x4100U, 0x5FFFU, 0x4100U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, // varies by mapper + { 0x6000U, 0xDFFFU, 0x6000U, RC_MEMORY_TYPE_SYSTEM_RAM, "FDS RAM"}, + { 0xE000U, 0xFFFFU, 0xE000U, RC_MEMORY_TYPE_READONLY, "FDS BIOS ROM"}, + + */ +}; +static const rc_memory_regions_t rc_memory_regions_nes = { _rc_memory_regions_nes, 11 }; + +/* ===== Nintendo 64 ===== */ +/* https://raw.githubusercontent.com/mikeryan/n64dev/master/docs/n64ops/n64ops%23h.txt */ +static const rc_memory_region_t _rc_memory_regions_n64[] = { + { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ + { 0x200000U, 0x3FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ + { 0x400000U, 0x7FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak - cannot find any details for real address */ +}; +static const rc_memory_regions_t rc_memory_regions_n64 = { _rc_memory_regions_n64, 3 }; + +/* ===== Nintendo DS ===== */ +/* https://www.akkit.org/info/gbatek.htm#dsmemorymaps */ +static const rc_memory_region_t _rc_memory_regions_nintendo_ds[] = { + { 0x000000U, 0x3FFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 1 }; + +/* ===== Nintendo DSi ===== */ +/* https://problemkaputt.de/gbatek.htm#dsiiomap */ +static const rc_memory_region_t _rc_memory_regions_nintendo_dsi[] = { + { 0x000000U, 0xFFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 1 }; + +/* ===== Oric ===== */ +static const rc_memory_region_t _rc_memory_regions_oric[] = { + /* actual size depends on machine type - up to 64KB */ + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_oric = { _rc_memory_regions_oric, 1 }; + +/* ===== PC-8800 ===== */ +static const rc_memory_region_t _rc_memory_regions_pc8800[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, + { 0x010000U, 0x010FFFU, 0x010000U, RC_MEMORY_TYPE_VIDEO_RAM, "Text VRAM" } /* technically VRAM, but often used as system RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_pc8800 = { _rc_memory_regions_pc8800, 2 }; + +/* ===== PC Engine ===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 1 }; + +/* ===== PC Engine CD===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine_cd[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x002000U, 0x011FFFU, 0x100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD RAM" }, + { 0x012000U, 0x041FFFU, 0x0D0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Super System Card RAM" }, + { 0x042000U, 0x0427FFU, 0x1EE000U, RC_MEMORY_TYPE_SAVE_RAM, "CD Battery-backed RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine_cd = { _rc_memory_regions_pc_engine_cd, 4 }; + +/* ===== PC-FX ===== */ +/* http://daifukkat.su/pcfx/data/memmap.html */ +static const rc_memory_region_t _rc_memory_regions_pcfx[] = { + { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x200000U, 0x207FFFU, 0xE0000000U, RC_MEMORY_TYPE_SAVE_RAM, "Internal Backup Memory" }, + { 0x208000U, 0x20FFFFU, 0xE8000000U, RC_MEMORY_TYPE_SAVE_RAM, "External Backup Memory" }, +}; +static const rc_memory_regions_t rc_memory_regions_pcfx = { _rc_memory_regions_pcfx, 3 }; + +/* ===== PlayStation ===== */ +/* http://www.raphnet.net/electronique/psx_adaptor/Playstation.txt */ +static const rc_memory_region_t _rc_memory_regions_playstation[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x010000U, 0x1FFFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_playstation = { _rc_memory_regions_playstation, 2 }; + +/* ===== PlayStation 2 ===== */ +/* https://psi-rockin.github.io/ps2tek/ */ +static const rc_memory_region_t _rc_memory_regions_playstation2[] = { + { 0x00000000U, 0x000FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x00100000U, 0x01FFFFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x02000000U, 0x02003FFFU, 0x70000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Scratchpad RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_playstation2 = { _rc_memory_regions_playstation2, 3 }; + +/* ===== PlayStation Portable ===== */ +/* https://github.com/uofw/upspd/wiki/Memory-map */ +static const rc_memory_region_t _rc_memory_regions_psp[] = { + { 0x00000000U, 0x007FFFFFU, 0x08000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x00800000U, 0x01FFFFFFU, 0x08800000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_psp = { _rc_memory_regions_psp, 2 }; + +/* ===== Pokemon Mini ===== */ +/* https://www.pokemon-mini.net/documentation/memory-map/ */ +static const rc_memory_region_t _rc_memory_regions_pokemini[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "BIOS RAM" }, + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_pokemini = { _rc_memory_regions_pokemini, 2 }; + +/* ===== Sega CD ===== */ +/* https://en.wikibooks.org/wiki/Genesis_Programming#MegaCD_Changes */ +static const rc_memory_region_t _rc_memory_regions_segacd[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "68000 RAM" }, + { 0x010000U, 0x08FFFFU, 0x80020000U, RC_MEMORY_TYPE_SAVE_RAM, "CD PRG RAM" } /* normally banked into $020000-$03FFFF */ +}; +static const rc_memory_regions_t rc_memory_regions_segacd = { _rc_memory_regions_segacd, 2 }; + +/* ===== Sega Saturn ===== */ +/* https://segaretro.org/Sega_Saturn_hardware_notes_(2004-04-27) */ +static const rc_memory_region_t _rc_memory_regions_saturn[] = { + { 0x000000U, 0x0FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Work RAM Low" }, + { 0x100000U, 0x1FFFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Work RAM High" } +}; +static const rc_memory_regions_t rc_memory_regions_saturn = { _rc_memory_regions_saturn, 2 }; + +/* ===== SG-1000 ===== */ +/* http://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_sg1000[] = { + { 0x000000U, 0x0003FFU, 0xC000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* https://github.com/libretro/FBNeo/blob/697801c6262be6ca91615cf905444d3e039bc06f/src/burn/drv/sg1000/d_sg1000.cpp#L210-L237 */ + /* Expansion mode B exposes 8KB at $C000. The first 2KB hides the System RAM, but since the address matches, + we'll leverage that definition and expand it another 6KB */ + { 0x000400U, 0x001FFFU, 0xC400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, + /* Expansion mode A exposes 8KB at $2000 */ + { 0x002000U, 0x003FFFU, 0x2000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, + /* Othello exposes 2KB at $8000, and The Castle exposes 8KB at $8000 */ + { 0x004000U, 0x005FFFU, 0x8000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_sg1000 = { _rc_memory_regions_sg1000, 4 }; + +/* ===== Super Cassette Vision ===== */ +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 }; + +/* ===== Thomson TO8 ===== */ +/* https://github.com/mamedev/mame/blob/master/src/mame/drivers/thomson.cpp#L1617 */ +static const rc_memory_region_t _rc_memory_regions_thomson_to8[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_thomson_to8 = { _rc_memory_regions_thomson_to8, 1 }; + +/* ===== TI-83 ===== */ +/* https://tutorials.eeems.ca/ASMin28Days/lesson/day03.html#mem */ +static const rc_memory_region_t _rc_memory_regions_ti83[] = { + { 0x000000U, 0x007FFFU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_ti83 = { _rc_memory_regions_ti83, 1 }; + +/* ===== TIC-80 ===== */ +/* https://github.com/nesbox/TIC-80/wiki/RAM */ +static const rc_memory_region_t _rc_memory_regions_tic80[] = { + { 0x000000U, 0x003FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Video RAM" }, /* have to classify this as system RAM because the core exposes it as part of the RETRO_MEMORY_SYSTEM_RAM */ + { 0x004000U, 0x005FFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Tile RAM" }, + { 0x006000U, 0x007FFFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Sprite RAM" }, + { 0x008000U, 0x00FF7FU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "MAP RAM" }, + { 0x00FF80U, 0x00FF8BU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Input State" }, + { 0x00FF8CU, 0x014003U, 0x00FF8CU, RC_MEMORY_TYPE_SYSTEM_RAM, "Sound RAM" }, + { 0x014004U, 0x014403U, 0x014004U, RC_MEMORY_TYPE_SAVE_RAM, "Persistent Memory" }, /* this is also returned as part of RETRO_MEMORY_SYSTEM_RAM, but can be extrapolated correctly because the pointer starts at the first SYSTEM_RAM region */ + { 0x014404U, 0x014603U, 0x014404U, RC_MEMORY_TYPE_SYSTEM_RAM, "Sprite Flags" }, + { 0x014604U, 0x014E03U, 0x014604U, RC_MEMORY_TYPE_SYSTEM_RAM, "System Font" }, + { 0x014E04U, 0x017FFFU, 0x014E04U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM"} +}; +static const rc_memory_regions_t rc_memory_regions_tic80 = { _rc_memory_regions_tic80, 10 }; + +/* ===== Uzebox ===== */ +/* https://uzebox.org/index.php */ +static const rc_memory_region_t _rc_memory_regions_uzebox[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_uzebox = { _rc_memory_regions_uzebox, 1 }; + +/* ===== Vectrex ===== */ +/* https://roadsidethoughts.com/vectrex/vectrex-memory-map.htm */ +static const rc_memory_region_t _rc_memory_regions_vectrex[] = { + { 0x000000U, 0x0003FFU, 0x00C800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_vectrex = { _rc_memory_regions_vectrex, 1 }; + +/* ===== Virtual Boy ===== */ +static const rc_memory_region_t _rc_memory_regions_virtualboy[] = { + { 0x000000U, 0x00FFFFU, 0x05000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x010000U, 0x01FFFFU, 0x06000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_virtualboy = { _rc_memory_regions_virtualboy, 2 }; + +/* ===== Watara Supervision ===== */ +/* https://github.com/libretro/potator/blob/b5e5ba02914fcdf4a8128072dbc709da28e08832/common/memorymap.c#L231-L259 */ +static const rc_memory_region_t _rc_memory_regions_watara_supervision[] = { + { 0x0000U, 0x001FFFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x2000U, 0x003FFFU, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Registers" }, + { 0x4000U, 0x005FFFU, 0x4000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_watara_supervision = { _rc_memory_regions_watara_supervision, 3 }; + +/* ===== WASM-4 ===== */ +/* fantasy console that runs specifically designed WebAssembly games */ +/* https://github.com/aduros/wasm4/blob/main/site/docs/intro.md#hardware-specs */ +static const rc_memory_region_t _rc_memory_regions_wasm4[] = { + { 0x000000U, 0x00FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Persistent storage is not directly accessible from the game. It has to be loaded into System RAM first + { 0x010000U, 0x0103FFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "Disk Storage"} + */ +}; +static const rc_memory_regions_t rc_memory_regions_wasm4 = { _rc_memory_regions_wasm4, 1 }; + +/* ===== Wii ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_wii[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x01800000U, 0x057FFFFF, 0x90000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wii = { _rc_memory_regions_wii, 2 }; + +/* ===== WonderSwan ===== */ +/* http://daifukkat.su/docs/wsman/#ovr_memmap */ +static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { + /* RAM ends at 0x3FFF for WonderSwan, WonderSwan color uses all 64KB */ + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Only 64KB of SRAM is accessible via the addressing scheme, but the cartridge + * may have up to 512KB of SRAM. http://daifukkat.su/docs/wsman/#cart_meta + * Since beetle_wswan exposes it as a contiguous block, assume its contiguous + * even though the documentation says $20000-$FFFFF is ROM data. If this causes + * a conflict in the future, we can revisit. A new region with a virtual address + * could be added to pick up the additional SRAM data. As long as it immediately + * follows the 64KB at $10000, all existing achievements should be unaffected. + */ + { 0x010000U, 0x08FFFFU, 0x010000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_regions_wonderswan, 2 }; + +/* ===== default ===== */ +static const rc_memory_regions_t rc_memory_regions_none = { 0, 0 }; + +const rc_memory_regions_t* rc_console_memory_regions(int console_id) +{ + switch (console_id) + { + case RC_CONSOLE_3DO: + return &rc_memory_regions_3do; + + case RC_CONSOLE_AMIGA: + return &rc_memory_regions_amiga; + + case RC_CONSOLE_AMSTRAD_PC: + return &rc_memory_regions_amstrad_pc; + + case RC_CONSOLE_APPLE_II: + return &rc_memory_regions_appleii; + + case RC_CONSOLE_ARCADIA_2001: + return &rc_memory_regions_arcadia_2001; + + case RC_CONSOLE_ARDUBOY: + return &rc_memory_regions_arduboy; + + case RC_CONSOLE_ATARI_2600: + return &rc_memory_regions_atari2600; + + case RC_CONSOLE_ATARI_7800: + return &rc_memory_regions_atari7800; + + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_ATARI_JAGUAR_CD: + return &rc_memory_regions_atari_jaguar; + + case RC_CONSOLE_ATARI_LYNX: + return &rc_memory_regions_atari_lynx; + + case RC_CONSOLE_COLECOVISION: + return &rc_memory_regions_colecovision; + + case RC_CONSOLE_COMMODORE_64: + return &rc_memory_regions_c64; + + case RC_CONSOLE_DREAMCAST: + return &rc_memory_regions_dreamcast; + + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + return &rc_memory_regions_elektor_tv_games; + + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return &rc_memory_regions_fairchild_channel_f; + + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_GAMEBOY: + return &rc_memory_regions_gameboy; + + case RC_CONSOLE_GAMEBOY_COLOR: + return &rc_memory_regions_gameboy_color; + + case RC_CONSOLE_GAMEBOY_ADVANCE: + return &rc_memory_regions_gameboy_advance; + + case RC_CONSOLE_GAMECUBE: + return &rc_memory_regions_gamecube; + + case RC_CONSOLE_GAME_GEAR: + return &rc_memory_regions_game_gear; + + case RC_CONSOLE_INTELLIVISION: + return &rc_memory_regions_intellivision; + + case RC_CONSOLE_INTERTON_VC_4000: + return &rc_memory_regions_interton_vc_4000; + + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + return &rc_memory_regions_magnavox_odyssey_2; + + case RC_CONSOLE_MASTER_SYSTEM: + return &rc_memory_regions_master_system; + + case RC_CONSOLE_MEGA_DRIVE: + return &rc_memory_regions_megadrive; + + case RC_CONSOLE_SEGA_32X: + return &rc_memory_regions_megadrive_32x; + + case RC_CONSOLE_MSX: + return &rc_memory_regions_msx; + + case RC_CONSOLE_NEOGEO_POCKET: + return &rc_memory_regions_neo_geo_pocket; + + case RC_CONSOLE_NEO_GEO_CD: + return &rc_memory_regions_neo_geo_cd; + + case RC_CONSOLE_NINTENDO: + return &rc_memory_regions_nes; + + case RC_CONSOLE_NINTENDO_64: + return &rc_memory_regions_n64; + + case RC_CONSOLE_NINTENDO_DS: + return &rc_memory_regions_nintendo_ds; + + case RC_CONSOLE_NINTENDO_DSI: + return &rc_memory_regions_nintendo_dsi; + + case RC_CONSOLE_ORIC: + return &rc_memory_regions_oric; + + case RC_CONSOLE_PC8800: + return &rc_memory_regions_pc8800; + + case RC_CONSOLE_PC_ENGINE: + return &rc_memory_regions_pc_engine; + + case RC_CONSOLE_PC_ENGINE_CD: + return &rc_memory_regions_pc_engine_cd; + + case RC_CONSOLE_PCFX: + return &rc_memory_regions_pcfx; + + case RC_CONSOLE_PLAYSTATION: + return &rc_memory_regions_playstation; + + case RC_CONSOLE_PLAYSTATION_2: + return &rc_memory_regions_playstation2; + + case RC_CONSOLE_PSP: + return &rc_memory_regions_psp; + + case RC_CONSOLE_POKEMON_MINI: + return &rc_memory_regions_pokemini; + + case RC_CONSOLE_SATURN: + return &rc_memory_regions_saturn; + + case RC_CONSOLE_SEGA_CD: + return &rc_memory_regions_segacd; + + case RC_CONSOLE_SG1000: + return &rc_memory_regions_sg1000; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return &rc_memory_regions_scv; + + case RC_CONSOLE_SUPER_NINTENDO: + return &rc_memory_regions_snes; + + case RC_CONSOLE_SUPERVISION: + return &rc_memory_regions_watara_supervision; + + case RC_CONSOLE_THOMSONTO8: + return &rc_memory_regions_thomson_to8; + + case RC_CONSOLE_TI83: + return &rc_memory_regions_ti83; + + case RC_CONSOLE_TIC80: + return &rc_memory_regions_tic80; + + case RC_CONSOLE_UZEBOX: + return &rc_memory_regions_uzebox; + + case RC_CONSOLE_VECTREX: + return &rc_memory_regions_vectrex; + + case RC_CONSOLE_VIRTUAL_BOY: + return &rc_memory_regions_virtualboy; + + case RC_CONSOLE_WASM4: + return &rc_memory_regions_wasm4; + + case RC_CONSOLE_WII: + return &rc_memory_regions_wii; + + case RC_CONSOLE_WONDERSWAN: + return &rc_memory_regions_wonderswan; + + default: + return &rc_memory_regions_none; + } +} diff --git a/src/rcheevos/src/rcheevos/format.c b/src/rcheevos/src/rcheevos/format.c new file mode 100644 index 000000000..1129f950b --- /dev/null +++ b/src/rcheevos/src/rcheevos/format.c @@ -0,0 +1,206 @@ +#include "rc_internal.h" + +#include "rc_compat.h" + +#include +#include + +int rc_parse_format(const char* format_str) { + switch (*format_str++) { + case 'F': + if (!strcmp(format_str, "RAMES")) { + return RC_FORMAT_FRAMES; + } + if (!strncmp(format_str, "LOAT", 4) && format_str[4] >= '1' && format_str[4] <= '6' && format_str[5] == '\0') { + return RC_FORMAT_FLOAT1 + (format_str[4] - '1'); + } + + break; + + case 'T': + if (!strcmp(format_str, "IME")) { + return RC_FORMAT_FRAMES; + } + 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_typed_value(char* buffer, int size, const rc_typed_value_t* value, int format) { + int chars; + rc_typed_value_t converted_value; + + memcpy(&converted_value, value, sizeof(converted_value)); + + switch (format) { + default: + case RC_FORMAT_VALUE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = snprintf(buffer, size, "%d", converted_value.value.i32); + break; + + case RC_FORMAT_FRAMES: + /* 60 frames per second = 100 centiseconds / 60 frames; multiply frames by 100 / 60 */ + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_centiseconds(buffer, size, converted_value.value.u32 * 10 / 6); + break; + + case RC_FORMAT_CENTISECS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_centiseconds(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SECONDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_seconds(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SECONDS_AS_MINUTES: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_minutes(buffer, size, converted_value.value.u32 / 60); + break; + + case RC_FORMAT_MINUTES: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_minutes(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SCORE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = snprintf(buffer, size, "%06d", converted_value.value.i32); + break; + + case RC_FORMAT_FLOAT1: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.1f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT2: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.2f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT3: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.3f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT4: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.4f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT5: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.5f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT6: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.6f", converted_value.value.f32); + break; + } + + return chars; +} + +int rc_format_value(char* buffer, int size, int value, int format) { + rc_typed_value_t typed_value; + + typed_value.value.i32 = value; + typed_value.type = RC_VALUE_TYPE_SIGNED; + return rc_format_typed_value(buffer, size, &typed_value, format); +} diff --git a/src/rcheevos/src/rcheevos/lboard.c b/src/rcheevos/src/rcheevos/lboard.c new file mode 100644 index 000000000..f7b1ec5a7 --- /dev/null +++ b/src/rcheevos/src/rcheevos/lboard.c @@ -0,0 +1,275 @@ +#include "rc_internal.h" + +enum { + RC_LBOARD_START = 1 << 0, + RC_LBOARD_CANCEL = 1 << 1, + RC_LBOARD_SUBMIT = 1 << 2, + RC_LBOARD_VALUE = 1 << 3, + RC_LBOARD_PROGRESS = 1 << 4, + RC_LBOARD_COMPLETE = RC_LBOARD_START | RC_LBOARD_CANCEL | RC_LBOARD_SUBMIT | RC_LBOARD_VALUE +}; + +void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse) { + int found; + + self->progress = 0; + found = 0; + + for (;;) + { + if ((memaddr[0] == 's' || memaddr[0] == 'S') && + (memaddr[1] == 't' || memaddr[1] == 'T') && + (memaddr[2] == 'a' || memaddr[2] == 'A') && memaddr[3] == ':') { + if ((found & RC_LBOARD_START) != 0) { + parse->offset = RC_DUPLICATED_START; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_START; + rc_parse_trigger_internal(&self->start, &memaddr, parse); + self->start.memrefs = 0; + } + } + else if ((memaddr[0] == 'c' || memaddr[0] == 'C') && + (memaddr[1] == 'a' || memaddr[1] == 'A') && + (memaddr[2] == 'n' || memaddr[2] == 'N') && memaddr[3] == ':') { + if ((found & RC_LBOARD_CANCEL) != 0) { + parse->offset = RC_DUPLICATED_CANCEL; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_CANCEL; + rc_parse_trigger_internal(&self->cancel, &memaddr, parse); + self->cancel.memrefs = 0; + } + } + else if ((memaddr[0] == 's' || memaddr[0] == 'S') && + (memaddr[1] == 'u' || memaddr[1] == 'U') && + (memaddr[2] == 'b' || memaddr[2] == 'B') && memaddr[3] == ':') { + if ((found & RC_LBOARD_SUBMIT) != 0) { + parse->offset = RC_DUPLICATED_SUBMIT; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_SUBMIT; + rc_parse_trigger_internal(&self->submit, &memaddr, parse); + self->submit.memrefs = 0; + } + } + else if ((memaddr[0] == 'v' || memaddr[0] == 'V') && + (memaddr[1] == 'a' || memaddr[1] == 'A') && + (memaddr[2] == 'l' || memaddr[2] == 'L') && memaddr[3] == ':') { + if ((found & RC_LBOARD_VALUE) != 0) { + parse->offset = RC_DUPLICATED_VALUE; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_VALUE; + rc_parse_value_internal(&self->value, &memaddr, parse); + self->value.memrefs = 0; + } + } + else if ((memaddr[0] == 'p' || memaddr[0] == 'P') && + (memaddr[1] == 'r' || memaddr[1] == 'R') && + (memaddr[2] == 'o' || memaddr[2] == 'O') && memaddr[3] == ':') { + if ((found & RC_LBOARD_PROGRESS) != 0) { + parse->offset = RC_DUPLICATED_PROGRESS; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_PROGRESS; + + self->progress = RC_ALLOC(rc_value_t, parse); + rc_parse_value_internal(self->progress, &memaddr, parse); + self->progress->memrefs = 0; + } + } + + /* encountered an error parsing one of the parts */ + if (parse->offset < 0) + return; + + /* end of string, or end of quoted string - stop processing */ + if (memaddr[0] == '\0' || memaddr[0] == '\"') + break; + + /* expect two colons between fields */ + if (memaddr[0] != ':' || memaddr[1] != ':') { + parse->offset = RC_INVALID_LBOARD_FIELD; + return; + } + + memaddr += 2; + } + + if ((found & RC_LBOARD_COMPLETE) != RC_LBOARD_COMPLETE) { + if ((found & RC_LBOARD_START) == 0) { + parse->offset = RC_MISSING_START; + } + else if ((found & RC_LBOARD_CANCEL) == 0) { + parse->offset = RC_MISSING_CANCEL; + } + else if ((found & RC_LBOARD_SUBMIT) == 0) { + parse->offset = RC_MISSING_SUBMIT; + } + else if ((found & RC_LBOARD_VALUE) == 0) { + parse->offset = RC_MISSING_VALUE; + } + + return; + } + + self->state = RC_LBOARD_STATE_WAITING; +} + +int rc_lboard_size(const char* memaddr) { + rc_lboard_t* self; + rc_parse_state_t parse; + rc_memref_t* first_memref; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &first_memref); + + self = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(self, memaddr, &parse); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { + rc_lboard_t* self; + rc_parse_state_t parse; + + if (!buffer || !memaddr) + return 0; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_lboard_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + + rc_parse_lboard_internal(self, memaddr, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : 0; +} + +int rc_evaluate_lboard(rc_lboard_t* self, 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; +} + +int rc_lboard_state_active(int state) { + switch (state) + { + case RC_LBOARD_STATE_DISABLED: + case RC_LBOARD_STATE_INACTIVE: + return 0; + + default: + return 1; + } +} + +void rc_reset_lboard(rc_lboard_t* self) { + 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/src/rcheevos/src/rcheevos/memref.c b/src/rcheevos/src/rcheevos/memref.c new file mode 100644 index 000000000..ae0f99a8e --- /dev/null +++ b/src/rcheevos/src/rcheevos/memref.c @@ -0,0 +1,494 @@ +#include "rc_internal.h" + +#include /* malloc/realloc */ +#include /* memcpy */ +#include /* INFINITY/NAN */ + +#define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF + +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, 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] == '0') { + if (aux[1] != 'x' && aux[1] != 'X') + return RC_INVALID_MEMORY_OPERAND; + + aux += 2; + switch (*aux++) { + /* ordered by estimated frequency in case compiler doesn't build a jump table */ + case 'h': case 'H': *size = RC_MEMSIZE_8_BITS; break; + case ' ': *size = RC_MEMSIZE_16_BITS; break; + case 'x': case 'X': *size = RC_MEMSIZE_32_BITS; break; + + case 'm': case 'M': *size = RC_MEMSIZE_BIT_0; break; + case 'n': case 'N': *size = RC_MEMSIZE_BIT_1; break; + case 'o': case 'O': *size = RC_MEMSIZE_BIT_2; break; + case 'p': case 'P': *size = RC_MEMSIZE_BIT_3; break; + case 'q': case 'Q': *size = RC_MEMSIZE_BIT_4; break; + case 'r': case 'R': *size = RC_MEMSIZE_BIT_5; break; + case 's': case 'S': *size = RC_MEMSIZE_BIT_6; break; + case 't': case 'T': *size = RC_MEMSIZE_BIT_7; break; + case 'l': case 'L': *size = RC_MEMSIZE_LOW; break; + case 'u': case 'U': *size = RC_MEMSIZE_HIGH; break; + case 'k': case 'K': *size = RC_MEMSIZE_BITCOUNT; break; + case 'w': case 'W': *size = RC_MEMSIZE_24_BITS; break; + case 'g': case 'G': *size = RC_MEMSIZE_32_BITS_BE; break; + case 'i': case 'I': *size = RC_MEMSIZE_16_BITS_BE; break; + case 'j': case 'J': *size = RC_MEMSIZE_24_BITS_BE; break; + + /* case 'v': case 'V': */ + /* case 'y': case 'Y': 64 bit? */ + /* case 'z': case 'Z': 128 bit? */ + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + /* legacy support - addresses without a size prefix are assumed to be 16-bit */ + aux--; + *size = RC_MEMSIZE_16_BITS; + break; + + default: + return RC_INVALID_MEMORY_OPERAND; + } + } + else if (aux[0] == 'f' || aux[0] == 'F') { + ++aux; + switch (*aux++) { + case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; + case 'b': case 'B': *size = RC_MEMSIZE_FLOAT_BE; break; + case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; + case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; + + default: + return RC_INVALID_FP_OPERAND; + } + } + else { + return RC_INVALID_MEMORY_OPERAND; + } + + value = strtoul(aux, &end, 16); + + if (end == aux) + return RC_INVALID_MEMORY_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + *address = (unsigned)value; + *memaddr = end; + return RC_OK; +} + +static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { + /* 32-bit float has a 23-bit mantissa and 8-bit exponent */ + const unsigned implied_bit = 1 << 23; + const unsigned mantissa = mantissa_bits | implied_bit; + double dbl = ((double)mantissa) / ((double)implied_bit); + + if (exponent > 127) { + /* exponent above 127 is a special number */ + if (mantissa_bits == 0) { + /* infinity */ +#ifdef INFINITY /* INFINITY and NAN #defines require C99 */ + dbl = INFINITY; +#else + dbl = -log(0.0); +#endif + } + else { + /* NaN */ +#ifdef NAN + dbl = NAN; +#else + dbl = -sqrt(-1); +#endif + } + } + else if (exponent > 0) { + /* exponent from 1 to 127 is a number greater than 1 */ + while (exponent > 30) { + dbl *= (double)(1 << 30); + exponent -= 30; + } + dbl *= (double)((long long)1 << exponent); + } + else if (exponent < 0) { + /* exponent from -1 to -127 is a number less than 1 */ + + if (exponent == -127) { + /* exponent -127 (all exponent bits were zero) is a denormalized value + * (no implied leading bit) with exponent -126 */ + dbl = ((double)mantissa_bits) / ((double)implied_bit); + exponent = 126; + } else { + exponent = -exponent; + } + + while (exponent > 30) { + dbl /= (double)(1 << 30); + exponent -= 30; + } + dbl /= (double)((long long)1 << exponent); + } + else { + /* exponent of 0 requires no adjustment */ + } + + return (sign) ? (float)-dbl : (float)dbl; +} + +static void rc_transform_memref_float(rc_typed_value_t* value) { + /* decodes an IEEE 754 float */ + const unsigned mantissa = (value->value.u32 & 0x7FFFFF); + const int exponent = (int)((value->value.u32 >> 23) & 0xFF) - 127; + const int sign = (value->value.u32 & 0x80000000); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_float_be(rc_typed_value_t* value) { + /* decodes an IEEE 754 float in big endian format */ + const unsigned mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00007F00) << 8); + const int exponent = (int)(((value->value.u32 & 0x0000007F) << 1) | + ((value->value.u32 & 0x00008000) >> 15)) - 127; + const int sign = (value->value.u32 & 0x00000080); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_mbf32(rc_typed_value_t* value) { + /* decodes a Microsoft Binary Format float */ + /* NOTE: 32-bit MBF is stored in memory as big endian (at least for Apple II) */ + const unsigned mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00007F00) << 8); + const int exponent = (int)(value->value.u32 & 0xFF) - 129; + const int sign = (value->value.u32 & 0x00008000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { + /* decodes a Microsoft Binary Format float */ + /* Locomotive BASIC (CPC) uses MBF40, but in little endian format */ + const unsigned mantissa = value->value.u32 & 0x007FFFFF; + const int exponent = (int)(value->value.u32 >> 24) - 129; + const int sign = (value->value.u32 & 0x00800000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + +static const unsigned char rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; + +void rc_transform_memref_value(rc_typed_value_t* value, char size) { + /* ASSERT: value->type == RC_VALUE_TYPE_UNSIGNED */ + switch (size) + { + case RC_MEMSIZE_8_BITS: + value->value.u32 = (value->value.u32 & 0x000000ff); + break; + + case RC_MEMSIZE_16_BITS: + value->value.u32 = (value->value.u32 & 0x0000ffff); + break; + + case RC_MEMSIZE_24_BITS: + value->value.u32 = (value->value.u32 & 0x00ffffff); + break; + + case RC_MEMSIZE_32_BITS: + break; + + case RC_MEMSIZE_BIT_0: + value->value.u32 = (value->value.u32 >> 0) & 1; + break; + + case RC_MEMSIZE_BIT_1: + value->value.u32 = (value->value.u32 >> 1) & 1; + break; + + case RC_MEMSIZE_BIT_2: + value->value.u32 = (value->value.u32 >> 2) & 1; + break; + + case RC_MEMSIZE_BIT_3: + value->value.u32 = (value->value.u32 >> 3) & 1; + break; + + case RC_MEMSIZE_BIT_4: + value->value.u32 = (value->value.u32 >> 4) & 1; + break; + + case RC_MEMSIZE_BIT_5: + value->value.u32 = (value->value.u32 >> 5) & 1; + break; + + case RC_MEMSIZE_BIT_6: + value->value.u32 = (value->value.u32 >> 6) & 1; + break; + + case RC_MEMSIZE_BIT_7: + value->value.u32 = (value->value.u32 >> 7) & 1; + break; + + case RC_MEMSIZE_LOW: + value->value.u32 = value->value.u32 & 0x0f; + break; + + case RC_MEMSIZE_HIGH: + value->value.u32 = (value->value.u32 >> 4) & 0x0f; + break; + + case RC_MEMSIZE_BITCOUNT: + value->value.u32 = rc_bits_set[(value->value.u32 & 0x0F)] + + rc_bits_set[((value->value.u32 >> 4) & 0x0F)]; + break; + + case RC_MEMSIZE_16_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF00) >> 8) | + ((value->value.u32 & 0x00FF) << 8); + break; + + case RC_MEMSIZE_24_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF0000) >> 16) | + (value->value.u32 & 0x00FF00) | + ((value->value.u32 & 0x0000FF) << 16); + break; + + case RC_MEMSIZE_32_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x0000FF00) << 8) | + ((value->value.u32 & 0x000000FF) << 24); + break; + + case RC_MEMSIZE_FLOAT: + rc_transform_memref_float(value); + break; + + case RC_MEMSIZE_FLOAT_BE: + rc_transform_memref_float_be(value); + break; + + case RC_MEMSIZE_MBF32: + rc_transform_memref_mbf32(value); + break; + + case RC_MEMSIZE_MBF32_LE: + rc_transform_memref_mbf32_le(value); + break; + + default: + break; + } +} + +static const unsigned rc_memref_masks[] = { + 0x000000ff, /* RC_MEMSIZE_8_BITS */ + 0x0000ffff, /* RC_MEMSIZE_16_BITS */ + 0x00ffffff, /* RC_MEMSIZE_24_BITS */ + 0xffffffff, /* RC_MEMSIZE_32_BITS */ + 0x0000000f, /* RC_MEMSIZE_LOW */ + 0x000000f0, /* RC_MEMSIZE_HIGH */ + 0x00000001, /* RC_MEMSIZE_BIT_0 */ + 0x00000002, /* RC_MEMSIZE_BIT_1 */ + 0x00000004, /* RC_MEMSIZE_BIT_2 */ + 0x00000008, /* RC_MEMSIZE_BIT_3 */ + 0x00000010, /* RC_MEMSIZE_BIT_4 */ + 0x00000020, /* RC_MEMSIZE_BIT_5 */ + 0x00000040, /* RC_MEMSIZE_BIT_6 */ + 0x00000080, /* RC_MEMSIZE_BIT_7 */ + 0x000000ff, /* RC_MEMSIZE_BITCOUNT */ + 0x0000ffff, /* RC_MEMSIZE_16_BITS_BE */ + 0x00ffffff, /* RC_MEMSIZE_24_BITS_BE */ + 0xffffffff, /* RC_MEMSIZE_32_BITS_BE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT */ + 0xffffffff, /* RC_MEMSIZE_MBF32 */ + 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT_BE */ + 0xffffffff /* RC_MEMSIZE_VARIABLE */ +}; + +unsigned rc_memref_mask(char size) { + const size_t index = (size_t)size; + if (index >= sizeof(rc_memref_masks) / sizeof(rc_memref_masks[0])) + return 0xffffffff; + + return rc_memref_masks[index]; +} + +/* all sizes less than 8-bits (1 byte) are mapped to 8-bits. 24-bit is mapped to 32-bit + * as we don't expect the client to understand a request for 3 bytes. all other reads are + * mapped to the little-endian read of the same size. */ +static const char rc_memref_shared_sizes[] = { + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_8_BITS */ + RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_LOW */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_HIGH */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_0 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_1 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_2 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_3 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_4 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_5 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_6 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_7 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BITCOUNT */ + RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT_BE */ + RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ +}; + +char rc_memref_shared_size(char size) { + const size_t index = (size_t)size; + if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) + return size; + + return rc_memref_shared_sizes[index]; +} + +unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { + if (!peek) + return 0; + + switch (size) + { + case RC_MEMSIZE_8_BITS: + return peek(address, 1, ud); + + case RC_MEMSIZE_16_BITS: + return peek(address, 2, ud); + + case RC_MEMSIZE_32_BITS: + return peek(address, 4, ud); + + default: + { + unsigned value; + const size_t index = (size_t)size; + if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) + return 0; + + /* fetch the larger value and mask off the bits associated to the specified size + * for correct deduction of prior value. non-prior memrefs should already be using + * shared size memrefs to minimize the total number of memory reads required. */ + value = rc_peek_value(address, rc_memref_shared_sizes[index], peek, ud); + return value & rc_memref_masks[index]; + } + } +} + +void rc_update_memref_value(rc_memref_value_t* memref, 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; +} + +static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { + switch (operand_type) + { + /* most common case explicitly first, even though it could be handled by default case. + * this helps the compiler to optimize if it turns the switch into a series of if/elses */ + case RC_OPERAND_ADDRESS: + return memref->value; + + case RC_OPERAND_DELTA: + if (!memref->changed) { + /* fallthrough */ + default: + return memref->value; + } + /* fallthrough */ + case RC_OPERAND_PRIOR: + return memref->prior; + } +} + +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); +} diff --git a/src/rcheevos/src/rcheevos/operand.c b/src/rcheevos/src/rcheevos/operand.c new file mode 100644 index 000000000..f8b46ed70 --- /dev/null +++ b/src/rcheevos/src/rcheevos/operand.c @@ -0,0 +1,480 @@ +#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((unsigned char)*aux)) { + return RC_INVALID_LUA_OPERAND; + } + +#ifndef RC_DISABLE_LUA + id = aux; +#endif + + while (isalnum((unsigned char)*aux) || *aux == '_') { + aux++; + } + +#ifndef RC_DISABLE_LUA + + if (parse->L != 0) { + if (!lua_istable(parse->L, parse->funcs_ndx)) { + return RC_INVALID_LUA_OPERAND; + } + + lua_pushlstring(parse->L, id, aux - id); + lua_gettable(parse->L, parse->funcs_ndx); + + if (!lua_isfunction(parse->L, -1)) { + lua_pop(parse->L, 1); + return RC_INVALID_LUA_OPERAND; + } + + self->value.luafunc = luaL_ref(parse->L, LUA_REGISTRYINDEX); + } + +#else + (void)parse; +#endif /* RC_DISABLE_LUA */ + + self->type = RC_OPERAND_LUA; + *memaddr = aux; + return RC_OK; +} + +static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse, 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; + + size = rc_memref_shared_size(self->size); + if (size != self->size && self->type == RC_OPERAND_PRIOR) { + /* if the shared size differs from the requested size and it's a prior operation, we + * have to check to make sure both sizes use the same mask, or the prior value may be + * updated when bits outside the mask are modified, which would make it look like the + * current value once the mask is applied. if the mask differs, create a new + * non-shared record for tracking the prior data. */ + if (rc_memref_mask(size) != rc_memref_mask(self->size)) + size = self->size; + } + + self->value.memref = rc_alloc_memref(parse, address, size, (char)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_indirect, rc_parse_state_t* parse) { + const char* aux = *memaddr; + char* end; + int ret; + unsigned long value; + int negative; + int allow_decimal = 0; + + self->size = RC_MEMSIZE_32_BITS; + + switch (*aux) { + case 'h': case 'H': /* hex constant */ + if (aux[2] == 'x' || aux[2] == 'X') { + /* H0x1234 is a typo - either H1234 or 0xH1234 was probably meant */ + return RC_INVALID_CONST_OPERAND; + } + + value = strtoul(++aux, &end, 16); + if (end == aux) + return RC_INVALID_CONST_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + self->type = RC_OPERAND_CONST; + self->value.num = (unsigned)value; + + aux = end; + break; + + case 'f': case 'F': /* floating point constant */ + if (isalpha((unsigned char)aux[1])) { + ret = rc_parse_operand_memory(self, &aux, parse, is_indirect); + + if (ret < 0) + return ret; + + break; + } + allow_decimal = 1; + /* fall through */ + case 'v': case 'V': /* signed integer constant */ + ++aux; + /* fall through */ + case '+': case '-': /* signed integer constant */ + negative = 0; + if (*aux == '-') { + negative = 1; + ++aux; + } + else if (*aux == '+') { + ++aux; + } + + value = strtoul(aux, &end, 10); + + if (*end == '.' && allow_decimal) { + /* custom parser for decimal values to ignore locale */ + unsigned long shift = 1; + unsigned long fraction = 0; + + aux = end + 1; + if (*aux < '0' || *aux > '9') + return RC_INVALID_FP_OPERAND; + + do { + /* only keep as many digits as will fit in a 32-bit value to prevent overflow. + * float only has around 7 digits of precision anyway. */ + if (shift < 1000000000) { + fraction *= 10; + fraction += (*aux - '0'); + shift *= 10; + } + ++aux; + } while (*aux >= '0' && *aux <= '9'); + + if (fraction != 0) { + /* non-zero fractional part, convert to double and merge in integer portion */ + const double dbl_fraction = ((double)fraction) / ((double)shift); + if (negative) + self->value.dbl = ((double)(-((long)value))) - dbl_fraction; + else + self->value.dbl = (double)value + dbl_fraction; + } + else { + /* fractional part is 0, just convert the integer portion */ + if (negative) + self->value.dbl = (double)(-((long)value)); + else + self->value.dbl = (double)value; + } + + self->type = RC_OPERAND_FP; + } + else { + /* not a floating point value, make sure something was read and advance the read pointer */ + if (end == aux) + return allow_decimal ? RC_INVALID_FP_OPERAND : RC_INVALID_CONST_OPERAND; + + aux = end; + + if (value > 0x7fffffffU) + value = 0x7fffffffU; + + self->type = RC_OPERAND_CONST; + + if (negative) + self->value.num = (unsigned)(-((long)value)); + else + self->value.num = (unsigned)value; + } + break; + + case '0': + if (aux[1] == 'x' || aux[1] == 'X') { /* hex integer constant */ + /* 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 */ + +int rc_operand_is_float_memref(const rc_operand_t* self) { + switch (self->size) { + case RC_MEMSIZE_FLOAT: + case RC_MEMSIZE_FLOAT_BE: + case RC_MEMSIZE_MBF32: + case RC_MEMSIZE_MBF32_LE: + return 1; + + default: + return 0; + } +} + +int rc_operand_is_memref(const rc_operand_t* self) { + switch (self->type) { + case RC_OPERAND_CONST: + case RC_OPERAND_FP: + case RC_OPERAND_LUA: + return 0; + + default: + return 1; + } +} + +int rc_operand_is_float(const rc_operand_t* self) { + if (self->type == RC_OPERAND_FP) + return 1; + + return rc_operand_is_float_memref(self); +} + +unsigned rc_transform_operand_value(unsigned value, const rc_operand_t* self) { + switch (self->type) + { + case RC_OPERAND_BCD: + switch (self->size) + { + case RC_MEMSIZE_8_BITS: + value = ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + value = ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + value = ((value >> 20) & 0x0f) * 100000 + + ((value >> 16) & 0x0f) * 10000 + + ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_32_BITS_BE: + case RC_MEMSIZE_VARIABLE: + value = ((value >> 28) & 0x0f) * 10000000 + + ((value >> 24) & 0x0f) * 1000000 + + ((value >> 20) & 0x0f) * 100000 + + ((value >> 16) & 0x0f) * 10000 + + ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + default: + break; + } + break; + + case RC_OPERAND_INVERTED: + switch (self->size) + { + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + value ^= 0x0f; + break; + + case RC_MEMSIZE_8_BITS: + value ^= 0xff; + break; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + value ^= 0xffff; + break; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + value ^= 0xffffff; + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_32_BITS_BE: + case RC_MEMSIZE_VARIABLE: + value ^= 0xffffffff; + break; + + default: + value ^= 0x01; + break; + } + break; + + default: + break; + } + + return value; +} + +void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_state_t* eval_state) { +#ifndef RC_DISABLE_LUA + rc_luapeek_t luapeek; +#endif /* RC_DISABLE_LUA */ + + /* step 1: read memory */ + switch (self->type) { + case RC_OPERAND_CONST: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = self->value.num; + return; + + case RC_OPERAND_FP: + result->type = RC_VALUE_TYPE_FLOAT; + result->value.f32 = (float)self->value.dbl; + return; + + case RC_OPERAND_LUA: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = 0; + +#ifndef RC_DISABLE_LUA + if (eval_state->L != 0) { + lua_rawgeti(eval_state->L, LUA_REGISTRYINDEX, self->value.luafunc); + lua_pushcfunction(eval_state->L, rc_luapeek); + + luapeek.peek = eval_state->peek; + luapeek.ud = eval_state->peek_userdata; + + lua_pushlightuserdata(eval_state->L, &luapeek); + + if (lua_pcall(eval_state->L, 2, 1, 0) == LUA_OK) { + if (lua_isboolean(eval_state->L, -1)) { + result->value.u32 = (unsigned)lua_toboolean(eval_state->L, -1); + } + else { + result->value.u32 = (unsigned)lua_tonumber(eval_state->L, -1); + } + } + + lua_pop(eval_state->L, 1); + } + +#endif /* RC_DISABLE_LUA */ + + break; + + default: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = rc_get_memref_value(self->value.memref, self->type, eval_state); + break; + } + + /* step 2: convert read memory to desired format */ + rc_transform_memref_value(result, self->size); + + /* step 3: apply logic (BCD/invert) */ + if (result->type == RC_VALUE_TYPE_UNSIGNED) + result->value.u32 = rc_transform_operand_value(result->value.u32, self); +} diff --git a/src/rcheevos/src/rcheevos/rc_client.c b/src/rcheevos/src/rcheevos/rc_client.c new file mode 100644 index 000000000..2452dfdac --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_client.c @@ -0,0 +1,4697 @@ +#include "rc_client_internal.h" + +#include "rc_api_info.h" +#include "rc_api_runtime.h" +#include "rc_api_user.h" +#include "rc_consoles.h" +#include "rc_internal.h" +#include "rc_hash.h" + +#include "../rapi/rc_api_common.h" + +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif + +#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 +#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +typedef struct rc_client_generic_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_generic_callback_data_t; + +typedef struct rc_client_pending_media_t +{ + const char* file_path; + uint8_t* data; + size_t data_size; + rc_client_callback_t callback; + void* callback_userdata; +} rc_client_pending_media_t; + +typedef struct rc_client_load_state_t +{ + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + + rc_client_game_info_t* game; + rc_client_subset_info_t* subset; + rc_client_game_hash_t* hash; + + rc_hash_iterator_t hash_iterator; + rc_client_pending_media_t* pending_media; + + rc_api_start_session_response_t *start_session_response; + + rc_client_async_handle_t async_handle; + + uint8_t progress; + uint8_t outstanding_requests; + uint8_t hash_console_id; +} rc_client_load_state_t; + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); +static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when); + +/* ===== Construction/Destruction ===== */ + +static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ +} + +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function) +{ + rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t)); + if (!client) + return NULL; + + client->state.hardcore = 1; + + client->callbacks.read_memory = read_memory_function; + client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_dummy_event_handler; + rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); + rc_client_set_get_time_millisecs_function(client, NULL); + + rc_mutex_init(&client->state.mutex); + + rc_buf_init(&client->state.buffer); + + return client; +} + +void rc_client_destroy(rc_client_t* client) +{ + if (!client) + return; + + rc_client_unload_game(client); + + rc_buf_destroy(&client->state.buffer); + + rc_mutex_destroy(&client->state.mutex); + + free(client); +} + +/* ===== Logging ===== */ + +static rc_client_t* g_hash_client = NULL; + +static void rc_client_log_hash_message(const char* message) { + rc_client_log_message(g_hash_client, message); +} + +void rc_client_log_message(const rc_client_t* client, const char* message) +{ + if (client->callbacks.log_call) + client->callbacks.log_call(message, client); +} + +static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args) +{ + if (client->callbacks.log_call) { + char buffer[2048]; + +#ifdef __STDC_WANT_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + vsprintf(buffer, format, args); +#endif + + client->callbacks.log_call(buffer, client); + } +} + +#ifdef RC_NO_VARIADIC_MACROS + +void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +#else + +void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...) +{ + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); +} + +#endif /* RC_NO_VARIADIC_MACROS */ + +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + client->callbacks.log_call = callback; + client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE; +} + +/* ===== Common ===== */ + +static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + + /* round nanoseconds to nearest millisecond and add to seconds */ + return ((rc_clock_t)now.tv_sec * 1000 + ((rc_clock_t)now.tv_nsec / 1000000)); +#elif defined(_WIN32) + static LARGE_INTEGER freq; + LARGE_INTEGER ticks; + + /* Frequency is the number of ticks per second and is guaranteed to not change. */ + if (!freq.QuadPart) { + if (!QueryPerformanceFrequency(&freq)) + return 0; + + /* convert to number of ticks per millisecond to simplify later calculations */ + freq.QuadPart /= 1000; + } + + if (!QueryPerformanceCounter(&ticks)) + return 0; + + return (rc_clock_t)(ticks.QuadPart / freq.QuadPart); +#else + const clock_t clock_now = clock(); + if (sizeof(clock_t) == 4) { + static uint32_t clock_wraps = 0; + static clock_t last_clock = 0; + static time_t last_timet = 0; + const time_t time_now = time(NULL); + + if (last_timet != 0) { + const time_t seconds_per_clock_t = (time_t)(((uint64_t)1 << 32) / CLOCKS_PER_SEC); + if (clock_now < last_clock) { + /* clock() has wrapped */ + ++clock_wraps; + } + else if (time_now - last_timet > seconds_per_clock_t) { + /* it's been long enough that clock() has wrapped and is higher than the last time it was read */ + ++clock_wraps; + } + } + + last_timet = time_now; + last_clock = clock_now; + + return (rc_clock_t)((((uint64_t)clock_wraps << 32) | clock_now) / (CLOCKS_PER_SEC / 1000)); + } + else { + return (rc_clock_t)(clock_now / (CLOCKS_PER_SEC / 1000)); + } +#endif +} + +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler) +{ + client->callbacks.get_time_millisecs = handler ? handler : rc_client_clock_get_now_millisecs; +} + +static int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted; + + rc_mutex_lock(&client->state.mutex); + aborted = async_handle->aborted; + rc_mutex_unlock(&client->state.mutex); + + return aborted; +} + +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + if (async_handle && client) { + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = 1; + rc_mutex_unlock(&client->state.mutex); + } +} + +static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) +{ + if (!response->succeeded) { + if (*result == RC_OK) { + *result = RC_API_FAILURE; + if (!response->error_message) + return "Unexpected API failure with no error message"; + } + + if (response->error_message) + return response->error_message; + } + + if (*result != RC_OK) + return rc_error_str(*result); + + return NULL; +} + +static void rc_client_raise_server_error_event(rc_client_t* client, const char* api, const char* error_message) +{ + rc_client_server_error_t server_error; + rc_client_event_t client_event; + + server_error.api = api; + server_error.error_message = error_message; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_SERVER_ERROR; + client_event.server_error = &server_error; + + client->callbacks.event_handler(&client_event, client); +} + +static int rc_client_should_retry(const rc_api_server_response_t* server_response) +{ + switch (server_response->http_status_code) { + case 502: /* 502 Bad Gateway */ + /* nginx connection pool full. retry */ + return 1; + + case 503: /* 503 Service Temporarily Unavailable */ + /* site is in maintenance mode. retry */ + return 1; + + case 429: /* 429 Too Many Requests */ + /* too many unlocks occurred at the same time */ + return 1; + + case 521: /* 521 Web Server is Down */ + /* cloudfare could not find the server */ + return 1; + + case 522: /* 522 Connection Timed Out */ + /* timeout connecting to server from cloudfare */ + return 1; + + case 523: /* 523 Origin is Unreachable */ + /* cloudfare cannot find server */ + return 1; + + case 524: /* 524 A Timeout Occurred */ + /* connection to server from cloudfare was dropped before request was completed */ + return 1; + + default: + return 0; + } +} + +static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + if (!buffer) + return RC_INVALID_STATE; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result == RC_OK) + snprintf(buffer, buffer_size, "%s", request.url); + + rc_api_destroy_request(&request); + return result; +} + +/* ===== User ===== */ + +static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data; + rc_client_t* client = login_callback_data->client; + rc_api_login_response_t login_response; + rc_client_load_state_t* load_state; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &login_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Login aborted"); + free(login_callback_data); + return; + } + + if (client->state.user == RC_CLIENT_USER_STATE_NONE) { + /* logout was called */ + if (login_callback_data->callback) + login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata); + + free(login_callback_data); + return; + } + + result = rc_api_process_login_server_response(&login_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response); + if (error_message) { + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_NONE; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message); + if (login_callback_data->callback) + login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + } + else { + client->user.username = rc_buf_strcpy(&client->state.buffer, login_response.username); + + if (strcmp(login_response.username, login_response.display_name) == 0) + client->user.display_name = client->user.username; + else + client->user.display_name = rc_buf_strcpy(&client->state.buffer, login_response.display_name); + + client->user.token = rc_buf_strcpy(&client->state.buffer, login_response.api_token); + client->user.score = login_response.score; + client->user.score_softcore = login_response.score_softcore; + client->user.num_unread_messages = login_response.num_unread_messages; + + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + + if (login_callback_data->callback) + login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); + } + + rc_api_destroy_login_response(&login_response); + free(login_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, + const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_generic_callback_data_t* callback_data; + rc_api_request_t request; + int result = rc_api_init_login_request(&request, login_request); + const char* error_message = rc_error_str(result); + + if (result == RC_OK) { + rc_mutex_lock(&client->state.mutex); + + if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) { + error_message = "Login already in progress"; + result = RC_INVALID_STATE; + } + client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + rc_mutex_unlock(&client->state.mutex); + } + + if (result != RC_OK) { + callback(result, error_message, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!password || !password[0]) { + callback(RC_INVALID_STATE, "password is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.password = password; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!token || !token[0]) { + callback(RC_INVALID_STATE, "token is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.api_token = token; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +void rc_client_logout(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + if (!client) + return; + + switch (client->state.user) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name); + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + RC_CLIENT_LOG_INFO(client, "Aborting login"); + break; + } + + rc_mutex_lock(&client->state.mutex); + + client->state.user = RC_CLIENT_USER_STATE_NONE; + memset(&client->user, 0, sizeof(client->user)); + + load_state = client->state.load; + + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_load_error(load_state, RC_ABORTED, "Login aborted"); +} + +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) +{ + return (client && client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; +} + +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size) +{ + if (!user) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); +} + +static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset, + rc_client_user_game_summary_t* summary, const uint8_t unlock_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + switch (achievement->public_.category) { + case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + ++summary->num_core_achievements; + summary->points_core += achievement->public_.points; + + if (achievement->public_.unlocked & unlock_bit) { + ++summary->num_unlocked_achievements; + summary->points_unlocked += achievement->public_.points; + } + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { + ++summary->num_unsupported_achievements; + } + + break; + + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + ++summary->num_unofficial_achievements; + break; + + default: + continue; + } + } +} + +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) +{ + const uint8_t unlock_bit = (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + if (!summary) + return; + + memset(summary, 0, sizeof(*summary)); + if (!client || !client->game) + return; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit); + + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ +} + +/* ===== Game ===== */ + +static void rc_client_free_game(rc_client_game_info_t* game) +{ + rc_runtime_destroy(&game->runtime); + + rc_buf_destroy(&game->buffer); + + free(game); +} + +static void rc_client_free_load_state(rc_client_load_state_t* load_state) +{ + if (load_state->game) + rc_client_free_game(load_state->game); + + if (load_state->start_session_response) { + rc_api_destroy_start_session_response(load_state->start_session_response); + free(load_state->start_session_response); + } + + free(load_state); +} + +static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests) +{ + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = state; + load_state->outstanding_requests += num_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); +} + +static int rc_client_end_load_state(rc_client_load_state_t* load_state) +{ + int remaining_requests = 0; + int aborted = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + if (load_state->outstanding_requests > 0) + --load_state->outstanding_requests; + remaining_requests = load_state->outstanding_requests; + + if (load_state->client->state.load != load_state) + aborted = 1; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (aborted) { + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. As they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) { + /* if one of the callbacks called rc_client_load_error, progress will be set to + * RC_CLIENT_LOAD_STATE_UNKNOWN. There's no need to call the callback with RC_ABORTED + * in that case, as it will have already been called with something more appropriate. */ + if (load_state->progress != RC_CLIENT_LOAD_STATE_UNKNOWN_GAME && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + } + + return -1; + } + + return remaining_requests; +} + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message) +{ + int remaining_requests = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + if (load_state->client->state.load == load_state) + load_state->client->state.load = NULL; + + remaining_requests = load_state->outstanding_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (load_state->callback) + load_state->callback(result, error_message, load_state->client, load_state->callback_userdata); + + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. as they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) + rc_client_free_load_state(load_state); +} + +static void rc_client_load_aborted(rc_client_load_state_t* load_state) +{ + /* prevent callback from being called when manually aborted */ + load_state->callback = NULL; + + /* mark the game as no longer being loaded */ + rc_client_load_error(load_state, RC_ABORTED, NULL); + + /* decrement the async counter and potentially free the load_state object */ + rc_client_end_load_state(load_state); +} + +static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(achievement->trigger, memref)) { + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address); + } + } + } +} + +static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_value_contains_memref(&leaderboard->lboard->value, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else + continue; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address); + } + } +} + +static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id); + const uint32_t max_address = (regions && regions->num_regions > 0) ? + regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF; + uint8_t buffer[8]; + uint32_t total_count = 0; + uint32_t invalid_count = 0; + + rc_memref_t** last_memref = &game->runtime.memrefs; + rc_memref_t* memref = game->runtime.memrefs; + for (; memref; memref = memref->next) { + if (!memref->value.is_indirect) { + total_count++; + + if (memref->address > max_address || + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + /* invalid address, remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch 0. */ + *last_memref = memref->next; + + rc_client_invalidate_memref_achievements(game, client, memref); + rc_client_invalidate_memref_leaderboards(game, client, memref); + + invalid_count++; + continue; + } + } + + last_memref = &memref->next; + } + + game->max_valid_address = max_address; + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count); +} + +static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_runtime_trigger_t* trigger; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + + if (active_count <= game->runtime.trigger_capacity) { + if (active_count != 0) + memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t)); + } + else { + if (game->runtime.triggers) + free(game->runtime.triggers); + + game->runtime.trigger_capacity = active_count; + game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t)); + if (!game->runtime.triggers) { + /* Unexpected, no callback available, just fail */ + break; + } + } + + trigger = game->runtime.triggers; + achievement = subset->achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + trigger->id = achievement->public_.id; + memcpy(trigger->md5, achievement->md5, 16); + trigger->trigger = achievement->trigger; + ++trigger; + } + } + } + } + + game->runtime.trigger_count = active_count; +} + +static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + ++active_count; + } + + return active_count; +} + +static void rc_client_update_active_achievements(rc_client_game_info_t* game) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_count_active_achievements(subset); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if ((achievement->public_.unlocked & active_bit) == 0) { + switch (achievement->public_.state) { + case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: + rc_reset_trigger(achievement->trigger); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + ++active_count; + break; + + case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: + ++active_count; + break; + } + } + else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + + /* if it's active despite being unlocked, and we're in encore mode, leave it active */ + if (client->state.encore_mode) { + ++active_count; + continue; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? + achievement->unlock_time_hardcore : achievement->unlock_time_softcore; + + if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client_event.achievement = &achievement->public_; + client->callbacks.event_handler(&client_event, client); + } + } + } + + return active_count; +} + +static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client) +{ + const uint8_t active_bit = (client->state.encore_mode) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_client_toggle_hardcore_achievements(game, client, active_bit); +} + +static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + unsigned active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + if (client->state.hardcore) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (client->state.hardcore) + ++active_count; + else + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + if (active_count > 0) { + rc_runtime_lboard_t* lboard; + + if (active_count <= game->runtime.lboard_capacity) { + if (active_count != 0) + memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t)); + } + else { + if (game->runtime.lboards) + free(game->runtime.lboards); + + game->runtime.lboard_capacity = active_count; + game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t)); + } + + lboard = game->runtime.lboards; + + subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + lboard->id = leaderboard->public_.id; + memcpy(lboard->md5, leaderboard->md5, 16); + lboard->lboard = leaderboard->lboard; + ++lboard; + } + } + } + } + + game->runtime.lboard_count = active_count; +} + +static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(client->game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + game->runtime.lboard_count = 0; +} + +static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, rc_api_unlock_entry_t* unlocks, uint32_t num_unlocks, uint8_t mode) +{ + rc_client_achievement_info_t* start = subset->achievements; + rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; + rc_client_achievement_info_t* scan; + rc_api_unlock_entry_t* unlock = unlocks; + rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks; + + for (; unlock < unlock_stop; ++unlock) { + for (scan = start; scan < stop; ++scan) { + if (scan->public_.id == unlock->achievement_id) { + scan->public_.unlocked |= mode; + + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) + scan->unlock_time_hardcore = unlock->when; + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE) + scan->unlock_time_softcore = unlock->when; + + if (scan == start) + ++start; + else if (scan + 1 == stop) + --stop; + break; + } + } + } +} + +static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response) +{ + rc_client_t* client = load_state->client; + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_STATE_DONE : RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + /* unlocks not available - assume malloc failed */ + if (load_state->callback) + load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); + } + else { + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks, + start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks, + start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load == NULL) + client->game = load_state->game; + rc_mutex_unlock(&client->state.mutex); + + if (client->game != load_state->game) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* if a change media request is pending, kick it off */ + rc_client_pending_media_t* pending_media; + + rc_mutex_lock(&load_state->client->state.mutex); + pending_media = load_state->pending_media; + load_state->pending_media = NULL; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (pending_media) { + rc_client_begin_change_media(client, pending_media->file_path, + pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + /* client->game must be set before calling this function so it can query the console_id */ + rc_client_validate_addresses(load_state->game, client); + + rc_client_activate_achievements(load_state->game, client); + rc_client_activate_leaderboards(load_state->game, client); + + if (load_state->hash->hash[0] != '[') { + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { + /* schedule the periodic ping */ + rc_client_scheduled_callback_data_t* callback_data = rc_buf_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(callback_data, 0, sizeof(*callback_data)); + callback_data->callback = rc_client_ping; + callback_data->related_id = load_state->game->public_.id; + callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000; + rc_client_schedule_callback(client, callback_data); + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcode %s%s", load_state->game->public_.id, + client->state.hardcore ? "enabled" : "disabled", + (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id); + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + + /* detach the game object so it doesn't get freed by free_load_state */ + load_state->game = NULL; + } + } + + rc_client_free_load_state(load_state); +} + +static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_start_session_response_t start_session_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session"); + return; + } + + result = rc_api_process_start_session_server_response(&start_session_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else if (outstanding_requests == 0) { + rc_client_activate_game(load_state, &start_session_response); + } + else { + load_state->start_session_response = + (rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t)); + + if (!load_state->start_session_response) { + rc_client_load_error(callback_data, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + } + else { + /* safer to parse the response again than to try to copy it */ + rc_api_process_start_session_response(load_state->start_session_response, server_response->body); + } + } + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void rc_client_begin_start_session(rc_client_load_state_t* load_state) +{ + rc_api_start_session_request_t start_session_params; + rc_client_t* client = load_state->client; + rc_api_request_t start_session_request; + int result; + + memset(&start_session_params, 0, sizeof(start_session_params)); + start_session_params.username = client->user.username; + start_session_params.api_token = client->user.token; + start_session_params.game_id = load_state->hash->game_id; + + result = rc_api_init_start_session_request(&start_session_request, &start_session_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); + client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); + rc_api_destroy_request(&start_session_request); + } +} + +static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements) +{ + const rc_api_achievement_definition_t* read; + const rc_api_achievement_definition_t* stop; + rc_client_achievement_info_t* achievements; + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* scan; + rc_api_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + size_t size; + int trigger_size; + + subset->achievements = NULL; + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + + stop = achievement_definitions + num_achievements; + + /* if not testing unofficial, filter them out */ + if (!load_state->client->state.unofficial_enabled) { + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) + --num_achievements; + } + + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + } + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */ + + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */ + + sizeof(rc_client_achievement_info_t); + rc_buf_reserve(&load_state->game->buffer, size * num_achievements); + + /* allocate the achievement array */ + size = sizeof(rc_client_achievement_info_t) * num_achievements; + buffer = &load_state->game->buffer; + achievement = achievements = rc_buf_alloc(buffer, size); + memset(achievements, 0, size); + + /* copy the achievement data */ + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled) + continue; + + achievement->public_.title = rc_buf_strcpy(buffer, read->title); + achievement->public_.description = rc_buf_strcpy(buffer, read->description); + snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name); + achievement->public_.id = read->id; + achievement->public_.points = read->points; + achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ? + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, achievement->md5); + + trigger_size = rc_trigger_size(memaddr); + if (trigger_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buf_reserve(buffer, trigger_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + achievement->trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + rc_buf_consume(buffer, parse.buffer, (char*)parse.buffer + parse.offset); + achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + achievement->created_time = read->created; + achievement->updated_time = read->updated; + + scan = achievement; + while (scan > achievements) { + --scan; + if (strcmp(scan->author, read->author) == 0) { + achievement->author = scan->author; + break; + } + } + if (!achievement->author) + achievement->author = rc_buf_strcpy(buffer, read->author); + + ++achievement; + } + + subset->achievements = achievements; +} + +static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards) +{ + const rc_api_leaderboard_definition_t* read; + const rc_api_leaderboard_definition_t* stop; + rc_client_leaderboard_info_t* leaderboards; + rc_client_leaderboard_info_t* leaderboard; + rc_api_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + const char* ptr; + size_t size; + int lboard_size; + + subset->leaderboards = NULL; + subset->public_.num_leaderboards = num_leaderboards; + + if (num_leaderboards == 0) + return; + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_lboard_t) /* lboard container */ + + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */ + + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */ + + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */ + + sizeof(rc_client_leaderboard_info_t); + rc_buf_reserve(&load_state->game->buffer, size * num_leaderboards); + + /* allocate the achievement array */ + size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; + buffer = &load_state->game->buffer; + leaderboard = leaderboards = rc_buf_alloc(buffer, size); + memset(leaderboards, 0, size); + + /* copy the achievement data */ + read = leaderboard_definitions; + stop = read + num_leaderboards; + do { + leaderboard->public_.title = rc_buf_strcpy(buffer, read->title); + leaderboard->public_.description = rc_buf_strcpy(buffer, read->description); + leaderboard->public_.id = read->id; + leaderboard->public_.lower_is_better = read->lower_is_better; + leaderboard->format = (uint8_t)read->format; + leaderboard->hidden = (uint8_t)read->hidden; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, leaderboard->md5); + + ptr = strstr(memaddr, "VAL:"); + if (ptr != NULL) { + /* calculate the DJB2 hash of the VAL portion of the string*/ + uint32_t hash = 5381; + ptr += 4; /* skip 'VAL:' */ + while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) + hash = (hash << 5) + hash + *ptr++; + leaderboard->value_djb2 = hash; + } + + lboard_size = rc_lboard_size(memaddr); + if (lboard_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buf_reserve(buffer, lboard_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + rc_buf_consume(buffer, parse.buffer, (char*)parse.buffer + parse.offset); + leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + ++leaderboard; + ++read; + } while (read < stop); + + subset->leaderboards = leaderboards; +} + +static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title) +{ + const char* subset_prefix = strstr(title, "[Subset - "); + if (subset_prefix) { + const char* start = subset_prefix + 10; + const char* stop = strstr(start, "]"); + const size_t len = stop - start; + char* result = (char*)rc_buf_alloc(&game->buffer, len + 1); + + memcpy(result, start, len); + result[len] = '\0'; + return result; + } + + return NULL; +} + +static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_game_data_response_t fetch_game_data_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data"); + return; + } + + result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(load_state, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buf_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = fetch_game_data_response.id; + subset->active = 1; + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", fetch_game_data_response.image_name); + load_state->subset = subset; + + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && + fetch_game_data_response.console_id != load_state->game->public_.console_id) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", + fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + } + + /* kick off the start session request while we process the game data */ + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + /* we can't unlock achievements without a session, lock spectator mode for the game */ + load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED; + } + else { + rc_client_begin_start_session(load_state); + } + + /* process the game data */ + rc_client_copy_achievements(load_state, subset, + fetch_game_data_response.achievements, fetch_game_data_response.num_achievements); + rc_client_copy_leaderboards(load_state, subset, + fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards); + + if (!load_state->game->subsets) { + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public_.title = rc_buf_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + load_state->game->subsets = subset; + load_state->game->public_.badge_name = subset->public_.badge_name; + load_state->game->public_.console_id = fetch_game_data_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); + + subset->public_.title = load_state->game->public_.title; + + if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); + } + } + } + else { + rc_client_subset_info_t* scan; + + /* subset - extract subset title */ + subset->public_.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title); + if (!subset->public_.title) { + const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public_.title); + if (core_subset_title) { + rc_client_subset_info_t* scan = load_state->game->subsets; + for (; scan; scan = scan->next) { + if (scan->public_.title == load_state->game->public_.title) { + scan->public_.title = core_subset_title; + break; + } + } + } + + subset->public_.title = rc_buf_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + } + + /* append to subset list */ + scan = load_state->game->subsets; + while (scan->next) + scan = scan->next; + scan->next = subset; + } + + if (load_state->client->callbacks.post_process_game_data_response) { + load_state->client->callbacks.post_process_game_data_response(server_response, + &fetch_game_data_response, load_state->client, load_state->callback_userdata); + } + + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (outstanding_requests == 0) + rc_client_activate_game(load_state, load_state->start_session_response); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + + if (load_state->hash->game_id == 0) { + char hash[33]; + + if (rc_hash_iterate(hash, &load_state->hash_iterator)) { + /* found another hash to try */ + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + rc_client_load_game(load_state, hash, NULL); + return; + } + + if (load_state->game->media_hash && + load_state->game->media_hash->game_hash && + load_state->game->media_hash->game_hash->next) { + /* multiple hashes were tried, create a CSV */ + struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash; + int count = 1; + char* ptr; + size_t size; + + size = strlen(game_hash->hash) + 1; + while (game_hash->next) { + game_hash = game_hash->next; + size += strlen(game_hash->hash) + 1; + count++; + } + + ptr = (char*)rc_buf_alloc(&load_state->game->buffer, size); + ptr += size - 1; + *ptr = '\0'; + game_hash = load_state->game->media_hash->game_hash; + do { + const size_t hash_len = strlen(game_hash->hash); + ptr -= hash_len; + memcpy(ptr, game_hash->hash, hash_len); + + game_hash = game_hash->next; + if (!game_hash) + break; + + ptr--; + *ptr = ','; + } while (1); + + load_state->game->public_.hash = ptr; + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + } else { + /* only a single hash was tried, capture it */ + load_state->game->public_.console_id = load_state->hash_console_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + load_state->game->public_.title = "Unknown Game"; + load_state->game->public_.badge_name = ""; + client->game = load_state->game; + load_state->game = NULL; + + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + + if (load_state->hash->hash[0] != '[') { + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + /* done with the hashing code, release the global pointer */ + g_hash_client = NULL; + + rc_mutex_lock(&client->state.mutex); + result = client->state.user; + if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + load_state->progress = RC_CLIENT_LOAD_STATE_AWAIT_LOGIN; + rc_mutex_unlock(&client->state.mutex); + + switch (result) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + /* do nothing, this function will be called again after login completes */ + return; + + default: + rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED)); + return; + } + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = client->user.username; + fetch_game_data_request.api_token = client->user.token; + fetch_game_data_request.game_id = load_state->hash->game_id; + + result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, 1); + + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id); + client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + rc_api_destroy_request(&request); +} + +static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &load_state->async_handle)) { + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification"); + return; + } + + result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (error_message) { + rc_client_end_load_state(load_state); + rc_client_load_error(load_state, result, error_message); + } + else { + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = resolve_hash_response.game_id; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + /* have to call end_load_state after updating hash in case the load_state gets free'd */ + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_begin_fetch_game_data(load_state); + } + } + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) +{ + rc_client_game_hash_t* game_hash; + + rc_mutex_lock(&client->state.mutex); + game_hash = client->hashes; + while (game_hash) { + if (strcasecmp(game_hash->hash, hash) == 0) + break; + + game_hash = game_hash->next; + } + + if (!game_hash) { + game_hash = rc_buf_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); + memset(game_hash, 0, sizeof(*game_hash)); + snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash); + game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID; + game_hash->next = client->hashes; + client->hashes = game_hash; + } + rc_mutex_unlock(&client->state.mutex); + + return game_hash; +} + +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, + const char* hash, const char* file_path) +{ + rc_client_t* client = load_state->client; + rc_client_game_hash_t* old_hash; + + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + rc_buf_init(&load_state->game->buffer); + rc_runtime_init(&load_state->game->runtime); + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + old_hash = load_state->hash; + load_state->hash = rc_client_find_game_hash(client, hash); + + if (file_path) { + rc_client_media_hash_t* media_hash = + (rc_client_media_hash_t*)rc_buf_alloc(&load_state->game->buffer, sizeof(*media_hash)); + media_hash->game_hash = load_state->hash; + media_hash->path_djb2 = rc_djb2(file_path); + media_hash->next = load_state->game->media_hash; + load_state->game->media_hash = media_hash; + } + else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) { + load_state->game->media_hash->game_hash = load_state->hash; + } + + if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return NULL; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, 1); + + client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client); + + rc_api_destroy_request(&request); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + rc_client_begin_fetch_game_data(load_state); + } + + return &load_state->async_handle; +} + +rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) +{ + if (client && client->state.load) + return &client->state.load->hash_iterator; + + return NULL; +} + +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + return rc_client_load_game(load_state, hash, NULL); +} + +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + char hash[33]; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (data) { + if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data); + } + } + else if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path); + } + else { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (!file_path) + file_path = "?"; + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + if (console_id == RC_CONSOLE_UNKNOWN) { + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + + if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + } + else { + /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ + load_state->hash_console_id = console_id; + + if (data != NULL) { + if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + else { + if (!rc_hash_generate_from_file(hash, console_id, file_path)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + rc_client_subset_info_t* subset; + + for (subset = game->subsets; subset; subset = subset->next) { + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE && + achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } + + rc_client_hide_progress_tracker(client, game); +} + +void rc_client_unload_game(rc_client_t* client) +{ + rc_client_game_info_t* game; + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + game = client->game; + client->game = NULL; + client->state.load = NULL; + + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) + client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; + + if (game != NULL) + rc_client_game_mark_ui_to_be_hidden(client, game); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; + + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) { + *last = next->next; + continue; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); + + if (game != NULL) { + rc_client_raise_pending_events(client, game); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id); + rc_client_free_game(game); + } +} + +static void rc_client_change_media(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + if (game_hash->game_id == client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash); + } + else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + RC_CLIENT_LOG_INFO(client, "Switching to unknown media"); + } + else if (game_hash->game_id == 0) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash); + } + + client->game->public_.hash = game_hash->hash; + callback(RC_OK, NULL, client, callback_userdata); +} + +static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + + int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (rc_client_async_handle_aborted(client, &load_state->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Media change aborted"); + /* if lookup succeeded, still capture the new hash */ + if (result == RC_OK) + load_state->hash->game_id = resolve_hash_response.game_id; + } + else if (client->game != load_state->game) { + /* loaded game changed. return success regardless of result */ + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (error_message) { + load_state->callback(result, error_message, client, load_state->callback_userdata); + } + else { + load_state->hash->game_id = resolve_hash_response.game_id; + + if (resolve_hash_response.game_id == 0 && client->state.hardcore) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", load_state->hash->hash); + rc_client_set_hardcore_enabled(client, 0); + client->game->public_.hash = load_state->hash->hash; /* do still update the loaded hash */ + load_state->callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, load_state->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + rc_client_change_media(client, load_state->hash, load_state->callback, load_state->callback_userdata); + } + } + + free(load_state); + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_game_hash_t* game_hash = NULL; + rc_client_media_hash_t* media_hash; + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + uint32_t path_djb2; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!data && !file_path) { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) { + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + pending_media->file_path = strdup(file_path); + pending_media->callback = callback; + pending_media->callback_userdata = callback_userdata; + if (data && data_size) { + pending_media->data_size = data_size; + pending_media->data = (uint8_t*)malloc(data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + memcpy(pending_media->data, data, data_size); + } + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return NULL; + } + + /* still waiting for game data */ + if (pending_media) + return NULL; + + /* check to see if we've already hashed this file */ + path_djb2 = rc_djb2(file_path); + rc_mutex_lock(&client->state.mutex); + for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) { + if (media_hash->path_djb2 == path_djb2) { + game_hash = media_hash->game_hash; + break; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!game_hash) { + char hash[33]; + int result; + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (data != NULL) + result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); + else + result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); + + g_hash_client = NULL; + + if (!result) { + /* when changing discs, if the disc is not supported by the system, allow it. this is + * primarily for games that support user-provided audio CDs, but does allow using discs + * from other systems for games that leverage user-provided discs. */ + strcpy_s(hash, sizeof(hash), "[NO HASH]"); + } + + game_hash = rc_client_find_game_hash(client, hash); + + media_hash = (rc_client_media_hash_t*)rc_buf_alloc(&game->buffer, sizeof(*media_hash)); + media_hash->game_hash = game_hash; + media_hash->path_djb2 = path_djb2; + + rc_mutex_lock(&client->state.mutex); + media_hash->next = game->media_hash; + game->media_hash = media_hash; + rc_mutex_unlock(&client->state.mutex); + + if (!result) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + } + + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + else { + /* call the server to make sure the hash is valid for the loaded game */ + rc_client_load_state_t* callback_data; + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = game_hash->hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + callback(result, rc_error_str(result), client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client = client; + callback_data->hash = game_hash; + callback_data->game = game; + + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; + } +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + return (client && client->game) ? &client->game->public_ : NULL; +} + +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size) +{ + if (!game) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); +} + +/* ===== Subsets ===== */ + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata) +{ + char buffer[32]; + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return; + } + + if (!client->game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return; + } + + snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)subset_id); + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + load_state->game = client->game; + load_state->hash = rc_client_find_game_hash(client, buffer); + load_state->hash->game_id = subset_id; + client->state.load = load_state; + + rc_client_begin_fetch_game_data(load_state); +} + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->public_.id == subset_id) + return &subset->public_; + } + + return NULL; +} + +/* ===== Achievements ===== */ + +static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) +{ + uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN; + uint32_t new_measured_value = 0; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + return; + + achievement->public_.measured_progress[0] = '\0'; + + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { + /* achievement unlocked */ + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + } + else { + /* active achievement */ + new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + if (achievement->trigger) { + if (achievement->trigger->measured_target) { + if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) { + /* value hasn't been initialized yet, leave progress string empty */ + } + else if (achievement->trigger->measured_value == 0) { + /* value is 0, leave progress string empty. update progress to 0.0 */ + achievement->public_.measured_percent = 0.0; + } + else { + /* clamp measured value at target (can't get more than 100%) */ + new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ? + achievement->trigger->measured_target : achievement->trigger->measured_value; + + achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target; + + if (!achievement->trigger->measured_as_percent) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target); + } + else if (achievement->public_.measured_percent >= 1.0) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu%%", (unsigned long)achievement->public_.measured_percent); + } + } + } + + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE; + else if (achievement->public_.measured_percent >= 80.0) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE; + } + } + + if (new_bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED && achievement->public_.unlock_time >= recent_unlock_time) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + + achievement->public_.bucket = new_bucket; +} + +static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_achievement_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buf_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static int rc_client_compare_achievement_unlock_times(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + return (int)(unlock_b->unlock_time - unlock_a->unlock_time); +} + +static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) +{ + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { + switch (bucket) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + default: + return bucket; + } + } + + return bucket; +} + +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_client_achievement_t** bucket_achievements; + rc_client_achievement_t** achievement_ptr; + rc_client_achievement_bucket_t* bucket_ptr; + rc_client_achievement_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[16]; + uint32_t num_buckets; + uint32_t num_achievements; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED + }; + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_achievement_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + } + } + } + + num_buckets = 0; + num_achievements = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_achievements += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) { + ++num_buckets; + break; + } + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); + + list = (rc_client_achievement_list_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + bucket_ptr = list->buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + + if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) + qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + if (list) + free(list); +} + +static const rc_client_achievement_t* rc_client_subset_get_achievement_info( + rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.id == id) { + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_mutex_lock((rc_mutex_t*)(&client->state.mutex)); + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex)); + return &achievement->public_; + } + } + + return NULL; +} + +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id); + if (achievement != NULL) + return achievement; + } + + return NULL; +} + +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) +{ + const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? + RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + if (!achievement || !achievement->badge_name[0]) + return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); +} + +typedef struct rc_client_award_achievement_callback_data_t +{ + uint32_t id; + uint32_t retry_count; + uint8_t hardcore; + const char* game_hash; + time_t unlock_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_award_achievement_callback_data_t; + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); + +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data->data; + + rc_client_award_achievement_server_call(ach_data); +} + +static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data; + rc_api_award_achievement_response_t award_achievement_response; + + int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response); + + if (error_message) { + if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", award_achievement_response.response.error_message); + } + else if (ach_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message); + rc_client_award_achievement_server_call(ach_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (ach_data->retry_count > 8) ? 120 : (1 << (ach_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay); + + if (!ach_data->scheduled_callback_data) { + ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data)); + if (!ach_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry; + ach_data->scheduled_callback_data->data = ach_data; + ach_data->scheduled_callback_data->related_id = ach_data->id; + } + + ach_data->scheduled_callback_data->when = + ach_data->client->callbacks.get_time_millisecs(ach_data->client) + delay * 1000; + + rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data); + return; + } + } + else { + ach_data->client->user.score = award_achievement_response.new_player_score; + ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore; + + if (award_achievement_response.awarded_achievement_id != ach_data->id) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message); + } + else { + if (award_achievement_response.response.error_message) { + /* previously unlocked achievements are returned as a success with an error message */ + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u", + ach_data->id, ach_data->retry_count + 1, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u", + ach_data->id, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + + if (award_achievement_response.achievements_remaining == 0) { + rc_client_subset_info_t* subset; + for (subset = ach_data->client->game->subsets; subset; subset = subset->next) { + if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE && + rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) { + if (subset->public_.id == ach_data->client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + + /* TODO: subset mastery notification */ + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + } + } + } + } + } + } + + if (ach_data->scheduled_callback_data) + free(ach_data->scheduled_callback_data); + free(ach_data); +} + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data) +{ + rc_api_award_achievement_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = ach_data->client->user.username; + api_params.api_token = ach_data->client->user.token; + api_params.achievement_id = ach_data->id; + api_params.hardcore = ach_data->hardcore; + api_params.game_hash = ach_data->game_hash; + + result = rc_api_init_award_achievement_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); + free(ach_data); + return; + } + + ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement) +{ + rc_client_award_achievement_callback_data_t* callback_data; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.hardcore) { + achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL); + if (achievement->unlock_time_softcore == 0) + achievement->unlock_time_softcore = achievement->unlock_time_hardcore; + + /* adjust score now - will get accurate score back from server */ + client->user.score += achievement->public_.points; + } + else { + achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL); + + /* adjust score now - will get accurate score back from server */ + client->user.score_softcore += achievement->public_.points; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlocked |= (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_unlock(&client->state.mutex); + + if (client->callbacks.can_submit_achievement_unlock && + !client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id); + return; + } + + /* can't unlock unofficial achievements on the server */ + if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + /* don't actually unlock achievements when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id); + rc_client_raise_server_error_event(client, "award_achievement", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = achievement->public_.id; + callback_data->hardcore = client->state.hardcore; + callback_data->game_hash = client->game->public_.hash; + callback_data->unlock_time = achievement->public_.unlock_time; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); + rc_client_award_achievement_server_call(callback_data); +} + +static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + + rc_reset_trigger(trigger); + } +} + +static void rc_client_reset_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_achievements(subset); +} + +/* ===== Leaderboards ===== */ + +static const rc_client_leaderboard_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.id == id) + return &leaderboard->public_; + } + + return NULL; +} + +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_leaderboard_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id); + if (leaderboard != NULL) + return leaderboard; + } + + return NULL; +} + +static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive"; + case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active"; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_leaderboard_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buf_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping) +{ + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED; + + default: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + } +} + +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_leaderboard_t** bucket_leaderboards; + rc_client_leaderboard_t** leaderboard_ptr; + rc_client_leaderboard_bucket_t* bucket_ptr; + rc_client_leaderboard_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[8]; + uint32_t num_buckets; + uint32_t num_leaderboards; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ALL, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED + }; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_leaderboard_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->hidden) + continue; + + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + bucket_counts[leaderboard->bucket]++; + } + } + + num_buckets = 0; + num_leaderboards = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_leaderboards += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); + + list = (rc_client_leaderboard_list_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + bucket_ptr = list->buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + if (list) + free(list); +} + +static void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker; + rc_client_leaderboard_tracker_info_t* available_tracker = NULL; + + for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) { + if (tracker->reference_count == 0) { + if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + available_tracker = tracker; + + continue; + } + + if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format) + continue; + + if (tracker->raw_value != leaderboard->value) { + /* if the value comes from tracking hits, we can't assume the trackers started in the + * same frame, so we can't share the tracker */ + if (tracker->value_from_hits) + continue; + + /* value has changed. prepare an update event */ + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } + + /* attach to the existing tracker */ + ++tracker->reference_count; + tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + leaderboard->tracker = tracker; + leaderboard->public_.tracker_value = tracker->public_.display; + return; + } + + if (!available_tracker) { + rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers; + + available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buf_alloc(&game->buffer, sizeof(*available_tracker)); + memset(available_tracker, 0, sizeof(*available_tracker)); + available_tracker->public_.id = 1; + + for (tracker = *next; tracker; next = &tracker->next, tracker = *next) + available_tracker->public_.id++; + + *next = available_tracker; + } + + /* update the claimed tracker */ + available_tracker->reference_count = 1; + available_tracker->value_djb2 = leaderboard->value_djb2; + available_tracker->format = leaderboard->format; + available_tracker->raw_value = leaderboard->value; + available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW; + available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value); + leaderboard->tracker = available_tracker; + leaderboard->public_.tracker_value = available_tracker->public_.display; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; +} + +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + leaderboard->tracker = NULL; + + if (tracker && --tracker->reference_count == 0) { + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + if (tracker && tracker->raw_value != leaderboard->value) { + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +typedef struct rc_client_submit_leaderboard_entry_callback_data_t +{ + uint32_t id; + int32_t score; + uint32_t retry_count; + const char* game_hash; + time_t submit_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_submit_leaderboard_entry_callback_data_t; + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data); + +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; + + rc_client_submit_leaderboard_entry_server_call(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data; + rc_api_submit_lboard_entry_response_t submit_lboard_entry_response; + + int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response); + + if (error_message) { + if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", submit_lboard_entry_response.response.error_message); + } + else if (lboard_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message); + rc_client_submit_leaderboard_entry_server_call(lboard_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (lboard_data->retry_count > 8) ? 120 : (1 << (lboard_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay); + + if (!lboard_data->scheduled_callback_data) { + lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data)); + if (!lboard_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry; + lboard_data->scheduled_callback_data->data = lboard_data; + lboard_data->scheduled_callback_data->related_id = lboard_data->id; + } + + lboard_data->scheduled_callback_data->when = + lboard_data->client->callbacks.get_time_millisecs(lboard_data->client) + delay * 1000; + + rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data); + return; + } + } + else { + /* TODO: raise event for scoreboard (if retry_count < 2) */ + + /* not currently doing anything with the response */ + if (lboard_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts", + lboard_data->id, lboard_data->score, lboard_data->retry_count); + } + } + + if (lboard_data->scheduled_callback_data) + free(lboard_data->scheduled_callback_data); + free(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data) +{ + rc_api_submit_lboard_entry_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = lboard_data->client->user.username; + api_params.api_token = lboard_data->client->user.token; + api_params.leaderboard_id = lboard_data->id; + api_params.score = lboard_data->score; + api_params.game_hash = lboard_data->game_hash; + + result = rc_api_init_submit_lboard_entry_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); + return; + } + + lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_submit_leaderboard_entry_callback_data_t* callback_data; + + if (client->callbacks.can_submit_leaderboard_entry && + !client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id); + return; + } + + /* don't actually submit leaderboard entries when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + return; + } + + callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id); + rc_client_raise_server_error_event(client, "submit_lboard_entry", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = leaderboard->public_.id; + callback_data->score = leaderboard->value; + callback_data->game_hash = client->game->public_.hash; + callback_data->submit_time = time(NULL); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + rc_client_submit_leaderboard_entry_server_call(callback_data); +} + +static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard) + continue; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_reset_lboard(lboard); + break; + } + } +} + +static void rc_client_reset_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_leaderboards(client->game, subset); +} + +typedef struct rc_client_fetch_leaderboard_entries_callback_data_t { + rc_client_t* client; + rc_client_fetch_leaderboard_entries_callback_t callback; + void* callback_userdata; + uint32_t leaderboard_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_leaderboard_entries_callback_data_t; + +static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data; + rc_client_t* client = lbinfo_callback_data->client; + rc_api_fetch_leaderboard_info_response_t lbinfo_response; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &lbinfo_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted"); + free(lbinfo_callback_data); + return; + } + + result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message); + lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries; + size_t needed_size = list_size; + unsigned i; + + for (i = 0; i < lbinfo_response.num_entries; i++) + needed_size += strlen(lbinfo_response.entries[i].username) + 1; + + list = (rc_client_leaderboard_entry_list_t*)malloc(needed_size); + if (!list) { + lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)list + sizeof(*list)); + char* user = (char*)((uint8_t*)list + list_size); + const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries; + const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries; + const size_t logged_in_user_len = strlen(client->user.display_name) + 1; + list->user_index = -1; + + for (; lbentry < stop; ++lbentry, ++entry) { + const size_t len = strlen(lbentry->username) + 1; + entry->user = user; + memcpy(user, lbentry->username, len); + user += len; + + if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0) + list->user_index = (int)(entry - list->entries); + + entry->index = lbentry->index; + entry->rank = lbentry->rank; + entry->submitted = lbentry->submitted; + + rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format); + } + + list->num_entries = lbinfo_response.num_entries; + + lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response); + free(lbinfo_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client, + const rc_api_fetch_leaderboard_info_request_t* lbinfo_request, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_api_request_t request; + int result; + const char* error_message; + + result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->leaderboard_id = lbinfo_request->leaderboard_id; + + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.first_entry = first_entry; + lbinfo_request.count = count; + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.username = client->user.username; + lbinfo_request.count = count; + + if (!lbinfo_request.username) { + callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata); + return NULL; + } + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size) +{ + if (!entry) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user); +} + +/* ===== Rich Presence ===== */ + +static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_t* client = (rc_client_t*)callback_data; + rc_api_ping_response_t response; + + int result = rc_api_process_ping_server_response(&response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response); + if (error_message) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message); + } + + rc_api_destroy_ping_response(&response); +} + +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_api_ping_request_t api_params; + rc_api_request_t request; + char buffer[256]; + int result; + + rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer), + client->state.legacy_peek, client, NULL); + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public_.id; + api_params.rich_presence = buffer; + + result = rc_api_init_ping_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); + } + else { + client->callbacks.server_call(&request, rc_client_ping_callback, client, client); + } + + callback_data->when = now + 120 * 1000; + rc_client_schedule_callback(client, callback_data); +} + +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size) +{ + int result; + + if (!client || !client->game || !buffer) + return 0; + + result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size, + client->state.legacy_peek, client, NULL); + + if (result == 0) + result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title); + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (client) + client->callbacks.event_handler = handler; +} + +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + if (client) + client->callbacks.read_memory = handler; +} + +static void rc_client_invalidate_processing_memref(rc_client_t* client) +{ + rc_memref_t** next_memref = &client->game->runtime.memrefs; + rc_memref_t* memref; + + /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ + if (!client->state.processing_memref) + return; + + /* invalid memref. remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch the current value. */ + while ((memref = *next_memref) != NULL) { + if (memref == client->state.processing_memref) { + *next_memref = memref->next; + break; + } + next_memref = &memref->next; + } + + rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); + rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); + + client->state.processing_memref = NULL; +} + +static unsigned rc_client_peek_le(unsigned address, unsigned num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + unsigned value = 0; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + if (num_bytes <= sizeof(value)) { + num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client); + if (num_read == num_bytes) + return value; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +static unsigned rc_client_peek(unsigned address, unsigned num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint8_t buffer[4]; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + switch (num_bytes) { + case 1: + num_read = client->callbacks.read_memory(address, buffer, 1, client); + if (num_read == 1) + return buffer[0]; + break; + case 2: + num_read = client->callbacks.read_memory(address, buffer, 2, client); + if (num_read == 2) + return buffer[0] | (buffer[1] << 8); + break; + case 3: + num_read = client->callbacks.read_memory(address, buffer, 3, client); + if (num_read == 3) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16); + break; + case 4: + num_read = client->callbacks.read_memory(address, buffer, 4, client); + if (num_read == 4) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + break; + default: + break; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +void rc_client_set_legacy_peek(rc_client_t* client, int method) +{ + if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { + union { + uint32_t whole; + uint8_t parts[4]; + } u; + u.whole = 1; + method = (u.parts[0] == 1) ? + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED; + } + + client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ? + rc_client_peek_le : rc_client_peek; +} + +int rc_client_is_processing_required(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (client->game->runtime.trigger_count || client->game->runtime.lboard_count) + return 1; + + return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence); +} + +static void rc_client_update_memref_values(rc_client_t* client) +{ + rc_memref_t* memref = client->game->runtime.memrefs; + unsigned value; + int invalidated_memref = 0; + + for (; memref; memref = memref->next) { + if (memref->value.is_indirect) + continue; + + client->state.processing_memref = memref; + + value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + + if (client->state.processing_memref) { + rc_update_memref_value(&memref->value, value); + } + else { + /* if the peek function cleared the processing_memref, the memref was invalidated */ + invalidated_memref = 1; + } + } + + client->state.processing_memref = NULL; + + if (invalidated_memref) + rc_client_update_active_achievements(client->game); +} + +static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + int old_state, new_state; + unsigned old_measured_value; + + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL); + + /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_value <= trigger->measured_target && + rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { + + /* only show a popup for the achievement closest to triggering */ + float progress = (float)trigger->measured_value / (float)trigger->measured_target; + + if (trigger->measured_as_percent) { + /* if reporting the measured value as a percentage, only show the popup if the percentage changes */ + const unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent == new_percent) + progress = -1.0; + } + + if (progress > client->game->progress_tracker.progress) { + client->game->progress_tracker.progress = progress; + client->game->progress_tracker.achievement = achievement; + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE; + } + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + + /* raise events for each of the possible new states */ + if (new_state == RC_TRIGGER_STATE_TRIGGERED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED; + else if (new_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } +} + +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (game->progress_tracker.hide_callback && + game->progress_tracker.hide_callback->when && + game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0); + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + } +} + +static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + + rc_mutex_lock(&client->state.mutex); + if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + client->game->progress_tracker.hide_callback->when = 0; + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + } + rc_mutex_unlock(&client->state.mutex); + + if (client_event.type) + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + if (!game->progress_tracker.hide_callback) { + game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) + rc_buf_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t)); + game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed; + } + + if (game->progress_tracker.hide_callback->when == 0) + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW; + else + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; + + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, + client->callbacks.get_time_millisecs(client) + 2 * 1000); +} + +static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + switch (game->progress_tracker.action) { + case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + break; + case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + break; + default: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + break; + } + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE; + + client_event.achievement = &game->progress_tracker.achievement->public_; + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + rc_client_event_t client_event; + time_t recent_unlock_time = 0; + + memset(&client_event, 0, sizeof(client_event)); + + for (; achievement < stop; ++achievement) { + if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE) + continue; + + /* kick off award achievement request first */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + rc_client_award_achievement(client, achievement); + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS; + } + + /* update display state */ + if (recent_unlock_time == 0) + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + + /* raise events */ + client_event.achievement = &achievement->public_; + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + client->callbacks.event_handler(&client_event, client); + } + + /* clear pending flags */ + achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + int old_state, new_state; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + default: + if (!lboard) + continue; + + break; + } + + old_state = lboard->state; + new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL); + + switch (new_state) { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (old_state != RC_LBOARD_STATE_STARTED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED; + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + } + else { + rc_client_update_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (old_state != RC_LBOARD_STATE_CANCELED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED; + + if (old_state != RC_LBOARD_STATE_STARTED) + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + else + rc_client_update_leaderboard_tracker(client->game, leaderboard); + + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + } + + if (leaderboard->pending_events) + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } +} + +static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = game->leaderboard_trackers; + for (; tracker; tracker = tracker->next) { + if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard_tracker = &tracker->public_; + + /* update display text for new trackers or updated trackers */ + if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE)) + rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format); + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) { + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + /* request to show and hide in the same frame - ignore the event */ + } + else { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + client->callbacks.event_handler(&client_event, client); + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + client->callbacks.event_handler(&client_event, client); + } + + tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard = &leaderboard->public_; + + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) { + /* kick off submission request before raising event */ + rc_client_submit_leaderboard_entry(client, leaderboard); + + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + client->callbacks.event_handler(&client_event, client); + } + + leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE; + } +} + +static void rc_client_reset_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; + + for (subset = client->game->subsets; subset; subset = subset->next) + subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE; +} + +static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + /* raise any pending achievement events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT) + rc_client_raise_achievement_events(client, subset); + + /* raise any pending leaderboard events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD) + rc_client_raise_leaderboard_events(client, subset); + + /* raise mastery event if pending */ + if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING) + rc_client_raise_mastery_event(client, subset); +} + +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client, game); + + for (subset = game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* raise progress tracker events after achievement events so formatted values are updated for tracker event */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_raise_progress_tracker_events(client, game); + + /* if any achievements were unlocked, resync the active achievements list */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(game); + rc_mutex_unlock(&client->state.mutex); + } + + game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; +} + +void rc_client_do_frame(rc_client_t* client) +{ + if (!client) + return; + + if (client->game && !client->game->waiting_for_reset) { + rc_runtime_richpresence_t* richpresence; + rc_client_subset_info_t* subset; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + rc_client_update_memref_values(client); + rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL); + + client->game->progress_tracker.progress = 0.0; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_do_frame_update_progress_tracker(client, client->game); + + if (client->state.hardcore) { + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_leaderboards(client, subset); + } + } + + richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + const rc_clock_t now = client->callbacks.get_time_millisecs(client); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (scheduled_callback->when > now) { + /* not time for next callback yet, ignore it */ + scheduled_callback = NULL; + } + else { + /* remove the callback from the queue while we process it. callback can requeue if desired */ + client->state.scheduled_callbacks = scheduled_callback->next; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!scheduled_callback) + break; + + scheduled_callback->callback(scheduled_callback, client, now); + } while (1); + } +} + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_mutex_lock(&client->state.mutex); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next || scheduled_callback->when < next->when) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reschedule_callback(rc_client_t* client, + rc_client_scheduled_callback_data_t* callback, rc_clock_t when) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + /* ASSERT: this should only be called if the mutex is held */ + + callback->when = when; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + + if (next == callback) { + if (when == 0) { + /* request to unschedule the callback */ + *last = next->next; + next->next = NULL; + break; + } + + if (!next->next) { + /* end of list, just append it */ + break; + } + + if (when < next->next->when) { + /* already in the correct place */ + break; + } + + /* remove from current position - will insert later */ + *last = next->next; + next->next = NULL; + continue; + } + + if (!next || when < next->when) { + /* insert here */ + callback->next = next; + *last = callback; + break; + } + + last = &next->next; + } while (1); +} + +static void rc_client_reset_richpresence(rc_client_t* client) +{ + rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_reset_richpresence(richpresence->richpresence); +} + +static void rc_client_reset_variables(rc_client_t* client) +{ + rc_value_t* variable = client->game->runtime.variables; + for (; variable; variable = variable->next) + rc_reset_value(variable); +} + +static void rc_client_reset_all(rc_client_t* client) +{ + rc_client_reset_achievements(client); + rc_client_reset_leaderboards(client); + rc_client_reset_richpresence(client); + rc_client_reset_variables(client); +} + +void rc_client_reset(rc_client_t* client) +{ + rc_client_game_hash_t* game_hash; + if (!client || !client->game) + return; + + game_hash = rc_client_find_game_hash(client, client->game->public_.hash); + if (game_hash && game_hash->game_id != client->game->public_.id) { + /* current media is not for loaded game. unload game */ + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)", + (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash); + rc_client_unload_game(client); + return; + } + + RC_CLIENT_LOG_INFO(client, "Resetting runtime"); + + rc_mutex_lock(&client->state.mutex); + + client->game->waiting_for_reset = 0; + rc_client_reset_pending_events(client); + + rc_client_hide_progress_tracker(client, client->game); + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_progress_size(&client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer) +{ + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + if (!buffer) + return RC_INVALID_STATE; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_serialize_progress(buffer, &client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any visible challenge indicators to be hidden */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED && + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + /* flag any visible trackers to be hidden */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (lboard && lboard->state == RC_LBOARD_STATE_STARTED && + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } + } +} + +static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any challenge indicators that should be shown */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + /* if it's already shown, just keep it. otherwise flag it to be shown */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + } + else { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + /* ASSERT: only active achievements are serialized, so we don't have to worry about + * deserialization deactiving them. */ + } + + /* flag any trackers that need to be shown */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (lboard->state == RC_LBOARD_STATE_STARTED) { + leaderboard->value = (int)lboard->value.value.value; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + + /* if it's already being tracked, just update tracker. otherwise, allocate one */ + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_update_leaderboard_tracker(game, leaderboard); + } + else { + rc_client_allocate_leaderboard_tracker(game, leaderboard); + } + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + /* deallocate the tracker (don't actually raise the failed event) */ + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } +} + +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_before_deserialize_progress(subset); + + rc_client_hide_progress_tracker(client, client->game); + + if (!serialized) { + rc_client_reset_all(client); + result = RC_OK; + } + else { + result = rc_runtime_deserialize_progress(&client->game->runtime, serialized, NULL); + } + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_after_deserialize_progress(client->game, subset); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + + return result; +} + +/* ===== Toggles ===== */ + +static void rc_client_enable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 1; + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); + rc_client_activate_leaderboards(client->game, client); + + /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */ + RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset"); + client->game->waiting_for_reset = 1; + } + else { + RC_CLIENT_LOG_INFO(client, "Hardcore enabled"); + } +} + +static void rc_client_disable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 0; + RC_CLIENT_LOG_INFO(client, "Hardcore disabled"); + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_deactivate_leaderboards(client->game, client); + } +} + +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled) +{ + int changed = 0; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + enabled = enabled ? 1 : 0; + if (client->state.hardcore != enabled) { + if (enabled) + rc_client_enable_hardcore(client); + else + rc_client_disable_hardcore(client); + + changed = 1; + } + + rc_mutex_unlock(&client->state.mutex); + + /* events must be raised outside of lock */ + if (changed && client->game) { + if (enabled) { + /* if enabling hardcore, notify client that a reset is requested */ + if (client->game->waiting_for_reset) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + client->callbacks.event_handler(&client_event, client); + } + } + else { + /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */ + rc_client_raise_pending_events(client, client->game); + } + } +} + +int rc_client_get_hardcore_enabled(const rc_client_t* client) +{ + return client && client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled"); + client->state.unofficial_enabled = enabled ? 1 : 0; + } +} + +int rc_client_get_unofficial_enabled(const rc_client_t* client) +{ + return client && client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled"); + client->state.encore_mode = enabled ? 1 : 0; + } +} + +int rc_client_get_encore_mode_enabled(const rc_client_t* client) +{ + return client && client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) { + RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game."); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled"); + client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF; + } +} + +int rc_client_get_spectator_mode_enabled(const rc_client_t* client) +{ + return client && (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1; +} + +void rc_client_set_userdata(rc_client_t* client, void* userdata) +{ + if (client) + client->callbacks.client_data = userdata; +} + +void* rc_client_get_userdata(const rc_client_t* client) +{ + return client ? client->callbacks.client_data : NULL; +} + +void rc_client_set_host(const rc_client_t* client, const char* hostname) +{ + /* if empty, just pass NULL */ + if (hostname && !hostname[0]) + hostname = NULL; + + /* clear the image host so it'll use the custom host for images too */ + rc_api_set_image_host(NULL); + + /* set the custom host */ + if (hostname && client) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + } + rc_api_set_host(hostname); +} diff --git a/src/rcheevos/src/rcheevos/rc_client_internal.h b/src/rcheevos/src/rcheevos/rc_client_internal.h new file mode 100644 index 000000000..efa0bc9ea --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_client_internal.h @@ -0,0 +1,300 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_client.h" + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +struct rc_api_fetch_game_data_response_t; +typedef void (*rc_client_post_process_game_data_response_t)(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata); +typedef int (*rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); +typedef int (*rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); + +typedef struct rc_client_callbacks_t { + rc_client_read_memory_func_t read_memory; + rc_client_event_handler_t event_handler; + rc_client_server_call_t server_call; + rc_client_message_callback_t log_call; + rc_get_time_millisecs_func_t get_time_millisecs; + rc_client_post_process_game_data_response_t post_process_game_data_response; + rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock; + rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry; + + void* client_data; +} rc_client_callbacks_t; + +struct rc_client_scheduled_callback_data_t; +typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + rc_clock_t when; + unsigned related_id; + rc_client_scheduled_callback_t callback; + void* data; + struct rc_client_scheduled_callback_data_t* next; +} rc_client_scheduled_callback_data_t; + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback); + +enum { + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE = 0, + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED = (1 << 1), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW = (1 << 2), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE = (1 << 3), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE = (1 << 4) /* not a real event, just triggers update */ +}; + +typedef struct rc_client_achievement_info_t { + rc_client_achievement_t public_; + + rc_trigger_t* trigger; + uint8_t md5[16]; + + time_t unlock_time_hardcore; + time_t unlock_time_softcore; + + uint8_t pending_events; + + const char* author; + time_t created_time; + time_t updated_time; +} rc_client_achievement_info_t; + +enum { + RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW, + RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE +}; + +typedef struct rc_client_progress_tracker_t { + rc_client_achievement_info_t* achievement; + float progress; + + rc_client_scheduled_callback_data_t* hide_callback; + uint8_t action; +} rc_client_progress_tracker_t; + +enum { + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE = (1 << 1), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW = (1 << 2), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE = (1 << 3) +}; + +typedef struct rc_client_leaderboard_tracker_info_t { + rc_client_leaderboard_tracker_t public_; + struct rc_client_leaderboard_tracker_info_t* next; + int raw_value; + + uint32_t value_djb2; + + uint8_t format; + uint8_t pending_events; + uint8_t reference_count; + uint8_t value_from_hits; +} rc_client_leaderboard_tracker_info_t; + +enum { + RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED = (1 << 1), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED = (1 << 2), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED = (1 << 3) +}; + +typedef struct rc_client_leaderboard_info_t { + rc_client_leaderboard_t public_; + + rc_lboard_t* lboard; + uint8_t md5[16]; + + rc_client_leaderboard_tracker_info_t* tracker; + + uint32_t value_djb2; + int value; + + uint8_t format; + uint8_t pending_events; + uint8_t bucket; + uint8_t hidden; +} rc_client_leaderboard_info_t; + +enum { + RC_CLIENT_SUBSET_PENDING_EVENT_NONE = 0, + RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT = (1 << 1), + RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD = (1 << 2) +}; + +enum { + RC_CLIENT_GAME_PENDING_EVENT_NONE = 0, + RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER = (1 << 1), + RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS = (1 << 2), + RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER = (1 << 3) +}; + +typedef struct rc_client_subset_info_t { + rc_client_subset_t public_; + + rc_client_achievement_info_t* achievements; + rc_client_leaderboard_info_t* leaderboards; + + struct rc_client_subset_info_t* next; + + const char* all_label; + const char* inactive_label; + const char* locked_label; + const char* unlocked_label; + const char* unofficial_label; + const char* unsupported_label; + + uint8_t active; + uint8_t mastery; + uint8_t pending_events; +} rc_client_subset_info_t; + +typedef struct rc_client_game_hash_t { + char hash[33]; + uint32_t game_id; + struct rc_client_game_hash_t* next; +} rc_client_game_hash_t; + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash); + +typedef struct rc_client_media_hash_t { + rc_client_game_hash_t* game_hash; + struct rc_client_media_hash_t* next; + uint32_t path_djb2; +} rc_client_media_hash_t; + +typedef struct rc_client_game_info_t { + rc_client_game_t public_; + rc_client_leaderboard_tracker_info_t* leaderboard_trackers; + rc_client_progress_tracker_t progress_tracker; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + + uint32_t max_valid_address; + + uint8_t waiting_for_reset; + uint8_t pending_events; + + rc_api_buffer_t buffer; +} rc_client_game_info_t; + +enum { + RC_CLIENT_LOAD_STATE_NONE, + RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_STATE_AWAIT_LOGIN, + RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_STATE_STARTING_SESSION, + RC_CLIENT_LOAD_STATE_DONE, + RC_CLIENT_LOAD_STATE_UNKNOWN_GAME +}; + +enum { + RC_CLIENT_USER_STATE_NONE, + RC_CLIENT_USER_STATE_LOGIN_REQUESTED, + RC_CLIENT_USER_STATE_LOGGED_IN +}; + +enum { + RC_CLIENT_MASTERY_STATE_NONE, + RC_CLIENT_MASTERY_STATE_PENDING, + RC_CLIENT_MASTERY_STATE_SHOWN +}; + +enum { + RC_CLIENT_SPECTATOR_MODE_OFF, + RC_CLIENT_SPECTATOR_MODE_ON, + RC_CLIENT_SPECTATOR_MODE_LOCKED +}; + +struct rc_client_load_state_t; + +typedef struct rc_client_state_t { + rc_mutex_t mutex; + rc_api_buffer_t buffer; + + rc_client_scheduled_callback_data_t* scheduled_callbacks; + + uint8_t hardcore; + uint8_t encore_mode; + uint8_t spectator_mode; + uint8_t unofficial_enabled; + uint8_t log_level; + uint8_t user; + + struct rc_client_load_state_t* load; + rc_memref_t* processing_memref; + + rc_peek_t legacy_peek; +} rc_client_state_t; + +struct rc_client_t { + rc_client_game_info_t* game; + rc_client_game_hash_t* hashes; + + rc_client_user_t user; + + rc_client_callbacks_t callbacks; + + rc_client_state_t state; +}; + +#ifdef RC_NO_VARIADIC_MACROS + void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...); +#else + void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...); + #define RC_CLIENT_LOG_ERR_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_WARN_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_INFO_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_VERBOSE_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message_formatted(client, format, __VA_ARGS__); } +#endif + +void rc_client_log_message(const rc_client_t* client, const char* message); +#define RC_CLIENT_LOG_ERR(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_WARN(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_INFO(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_VERBOSE(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message(client, message); } + +/* internals pulled from runtime.c */ +void rc_runtime_checksum(const char* memaddr, unsigned char* md5); +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref); +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref); +/* end runtime.c internals */ + +/* helper functions for unit tests */ +struct rc_hash_iterator; +struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +/* end helper functions for unit tests */ + +enum { + RC_CLIENT_LEGACY_PEEK_AUTO, + RC_CLIENT_LEGACY_PEEK_CONSTRUCTED, + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS +}; + +void rc_client_set_legacy_peek(rc_client_t* client, int method); + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_CLIENT_INTERNAL_H */ diff --git a/src/rcheevos/src/rcheevos/rc_compat.h b/src/rcheevos/src/rcheevos/rc_compat.h new file mode 100644 index 000000000..e22f9b84e --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_compat.h @@ -0,0 +1,99 @@ +#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 */ + +#define RC_NO_VARIADIC_MACROS 1 + +#elif defined(_MSC_VER) + +/* Visual Studio redefinitions */ + +#ifndef strcasecmp + #define strcasecmp _stricmp +#endif +#ifndef strncasecmp + #define strncasecmp _strnicmp +#endif +#ifndef strdup + #define strdup _strdup +#endif + +#elif __STDC_VERSION__ < 199901L + +/* C89 redefinitions */ +#define RC_C89_HELPERS 1 + +#define RC_NO_VARIADIC_MACROS 1 + +#ifndef snprintf + extern int rc_snprintf(char* buffer, size_t size, const char* format, ...); + #define snprintf rc_snprintf +#endif + +#ifndef strncasecmp + extern int rc_strncasecmp(const char* left, const char* right, size_t length); + #define strncasecmp rc_strncasecmp +#endif + +#ifndef strcasecmp + extern int rc_strcasecmp(const char* left, const char* right); + #define strcasecmp rc_strcasecmp +#endif + +#ifndef strdup + extern char* rc_strdup(const char* str); + #define strdup rc_strdup +#endif + +#endif /* __STDC_VERSION__ < 199901L */ + +#ifndef __STDC_WANT_SECURE_LIB__ + /* _CRT_SECURE_NO_WARNINGS redefinitions */ + #define strcpy_s(dest, sz, src) strcpy(dest, src) + #define sscanf_s sscanf + + /* NOTE: Microsoft secure gmtime_s parameter order differs from C11 standard */ + #include + extern struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer); + #define gmtime_s rc_gmtime_s +#endif + +#ifdef RC_NO_THREADS + typedef int rc_mutex_t; + + #define rc_mutex_init(mutex) + #define rc_mutex_destroy(mutex) + #define rc_mutex_lock(mutex) + #define rc_mutex_unlock(mutex) +#else + #ifdef _WIN32 + typedef struct rc_mutex_t { + void* handle; /* HANDLE is defined as "void*" */ + } rc_mutex_t; + #else + #include + typedef pthread_mutex_t rc_mutex_t; + #endif + + void rc_mutex_init(rc_mutex_t* mutex); + void rc_mutex_destroy(rc_mutex_t* mutex); + void rc_mutex_lock(rc_mutex_t* mutex); + void rc_mutex_unlock(rc_mutex_t* mutex); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RC_COMPAT_H */ diff --git a/src/rcheevos/src/rcheevos/rc_internal.h b/src/rcheevos/src/rcheevos/rc_internal.h new file mode 100644 index 000000000..e58ca53de --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_internal.h @@ -0,0 +1,209 @@ +#ifndef INTERNAL_H +#define INTERNAL_H + +#include "rc_runtime_types.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))) + +/* force alignment to 4 bytes on 32-bit systems, or 8 bytes on 64-bit systems */ +#define RC_ALIGN(n) (((n) + (sizeof(void*)-1)) & ~(sizeof(void*)-1)) + +typedef struct rc_scratch_buffer { + struct rc_scratch_buffer* next; + int offset; + unsigned char buffer[512 - 16]; +} +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; + +enum { + RC_VALUE_TYPE_NONE, + RC_VALUE_TYPE_UNSIGNED, + RC_VALUE_TYPE_SIGNED, + RC_VALUE_TYPE_FLOAT +}; + +typedef struct { + union { + unsigned u32; + int i32; + float f32; + } value; + + char type; +} +rc_typed_value_t; + +#define RC_MEASURED_UNKNOWN 0xFFFFFFFF + +typedef struct { + rc_typed_value_t add_value;/* AddSource/SubSource */ + int add_hits; /* AddHits */ + unsigned add_address; /* AddAddress */ + + rc_peek_t peek; + void* peek_userdata; + lua_State* L; + + rc_typed_value_t measured_value; /* Measured */ + char was_reset; /* ResetIf triggered */ + char has_hits; /* one of more hit counts is non-zero */ + char primed; /* true if all non-Trigger conditions are true */ + char measured_from_hits; /* true if the measured_value came from a condition's hit count */ + char was_cond_reset; /* ResetNextIf triggered */ +} +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; + int lines_read; + + char has_required_hits; + char measured_as_percent; +} +rc_parse_state_t; + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer, lua_State* L, int funcs_ndx); +void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs); +void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables); +void rc_destroy_parse_state(rc_parse_state_t* parse); + +void* rc_alloc(void* pointer, 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); + +unsigned rc_djb2(const char* input); + +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); +char rc_memref_shared_size(char size); +unsigned rc_memref_mask(char size); +void rc_transform_memref_value(rc_typed_value_t* value, char size); +unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud); + +void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); +int rc_trigger_state_active(int state); + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, int is_value); +int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state); +void rc_reset_condset(rc_condset_t* self); + +enum { + RC_PROCESSING_COMPARE_DEFAULT = 0, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_ALWAYS_TRUE, + RC_PROCESSING_COMPARE_ALWAYS_FALSE +}; + +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect); +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); +void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state); +int rc_condition_is_combining(const rc_condition_t* self); + +int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse); +void rc_evaluate_operand(rc_typed_value_t* value, rc_operand_t* self, rc_eval_state_t* eval_state); +int rc_operand_is_float_memref(const rc_operand_t* self); +int rc_operand_is_float(const rc_operand_t* self); + +void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L); +void rc_reset_value(rc_value_t* self); +int rc_value_from_hits(rc_value_t* self); +rc_value_t* rc_alloc_helper_variable(const char* memaddr, 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_typed_value_convert(rc_typed_value_t* value, char new_type); +void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_negate(rc_typed_value_t* value); +int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper); +void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); + +int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, int format); + +void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse); +int rc_lboard_state_active(int state); + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse); + +#ifdef __cplusplus +} +#endif + +#endif /* INTERNAL_H */ diff --git a/src/rcheevos/src/rcheevos/rc_libretro.c b/src/rcheevos/src/rcheevos/rc_libretro.c new file mode 100644 index 000000000..eab44ef1b --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_libretro.c @@ -0,0 +1,816 @@ +/* This file provides a series of functions for integrating RetroAchievements with libretro. + * These functions will be called by a libretro frontend to validate certain expected behaviors + * and simplify mapping core data to the RAIntegration DLL. + * + * Originally designed to be shared between RALibretro and RetroArch, but will simplify + * integrating with any other frontends. + */ + +#include "rc_libretro.h" + +#include "rc_consoles.h" +#include "rc_compat.h" + +#include +#include + +/* internal helper functions in hash.c */ +extern void* rc_file_open(const char* path); +extern void rc_file_seek(void* file_handle, int64_t offset, int origin); +extern int64_t rc_file_tell(void* file_handle); +extern size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes); +extern void rc_file_close(void* file_handle); +extern int rc_path_compare_extension(const char* path, const char* ext); +extern int rc_hash_error(const char* message); + + +static rc_libretro_message_callback rc_libretro_verbose_message_callback = NULL; + +/* a value that starts with a comma is a CSV. + * if it starts with an exclamation point, it's everything but the provided value. + * if it starts with an exclamntion point followed by a comma, it's everything but the CSV values. + * values are case-insensitive */ +typedef struct rc_disallowed_core_settings_t +{ + const char* library_name; + const rc_disallowed_setting_t* disallowed_settings; +} rc_disallowed_core_settings_t; + +static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { + { "bsnes_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_cap32_settings[] = { + { "cap32_autorun", "disabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = { + { "dolphin_cheats_enabled", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = { + { "duckstation_CDROM.LoadImagePatches", "true" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { + { "ecwolf-invulnerability", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { + { "fbneo-allow-patched-romsets", "enabled" }, + { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, + { "fbneo-dipswitch-*", "Universe BIOS*" }, + { "fbneo-neogeo-mode", "UNIBIOS" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { + { "fceumm_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { + { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_wide_settings[] = { + { "genesis_plus_gx_wide_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_wide_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_settings[] = { + { "mesen_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_s_settings[] = { + { "mesen-s_region", "PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { + { "pcsx_rearmed_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_picodrive_settings[] = { + { "picodrive_region", ",Europe,Japan PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = { + { "ppsspp_cheats", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_quasi88_settings[] = { + { "q88_cpu_clock", ",1,2" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_smsplus_settings[] = { + { "smsplus_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { + { "snes9x_gfx_clip", "disabled" }, + { "snes9x_gfx_transp", "disabled" }, + { "snes9x_layer_*", "disabled" }, + { "snes9x_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { + { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ + { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = { + { "virtualjaguar_pal", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { + { "bsnes-mercury", _rc_disallowed_bsnes_settings }, + { "cap32", _rc_disallowed_cap32_settings }, + { "dolphin-emu", _rc_disallowed_dolphin_settings }, + { "DuckStation", _rc_disallowed_duckstation_settings }, + { "ecwolf", _rc_disallowed_ecwolf_settings }, + { "FCEUmm", _rc_disallowed_fceumm_settings }, + { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, + { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, + { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, + { "Mesen", _rc_disallowed_mesen_settings }, + { "Mesen-S", _rc_disallowed_mesen_s_settings }, + { "PPSSPP", _rc_disallowed_ppsspp_settings }, + { "PCSX-ReARMed", _rc_disallowed_pcsx_rearmed_settings }, + { "PicoDrive", _rc_disallowed_picodrive_settings }, + { "QUASI88", _rc_disallowed_quasi88_settings }, + { "SMS Plus GX", _rc_disallowed_smsplus_settings }, + { "Snes9x", _rc_disallowed_snes9x_settings }, + { "VICE x64", _rc_disallowed_vice_settings }, + { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, + { NULL, NULL } +}; + +static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* value) { + char c1, c2; + while ((c1 = *test++)) { + if (tolower(c1) != tolower(c2 = *value++)) + return (c2 == '*'); + } + + return (*value == '\0'); +} + +static int rc_libretro_match_value(const char* val, const char* match) { + /* if value starts with a comma, it's a CSV list of potential matches */ + if (*match == ',') { + do { + const char* ptr = ++match; + size_t size; + + while (*match && *match != ',') + ++match; + + size = match - ptr; + if (val[size] == '\0') { + if (memcmp(ptr, val, size) == 0) { + return 1; + } + else { + char buffer[128]; + memcpy(buffer, ptr, size); + buffer[size] = '\0'; + if (rc_libretro_string_equal_nocase_wildcard(buffer, val)) + return 1; + } + } + } while (*match == ','); + + return 0; + } + + /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ + if (*match == '!') + return !rc_libretro_match_value(val, &match[1]); + + /* just a single value, attempt to match it */ + return rc_libretro_string_equal_nocase_wildcard(val, match); +} + +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { + const char* key; + size_t key_len; + + for (; disallowed_settings->setting; ++disallowed_settings) { + key = disallowed_settings->setting; + key_len = strlen(key); + + if (key[key_len - 1] == '*') { + if (memcmp(setting, key, key_len - 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + else { + if (memcmp(setting, key, key_len + 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + } + + return 1; +} + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name) { + const rc_disallowed_core_settings_t* core_filter = rc_disallowed_core_settings; + size_t library_name_length; + + if (!library_name || !library_name[0]) + return NULL; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) + return core_filter->disallowed_settings; + + ++core_filter; + } + + return NULL; +} + +typedef struct rc_disallowed_core_systems_t +{ + const char* library_name; + const int disallowed_consoles[4]; +} rc_disallowed_core_systems_t; + +static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { + /* https://github.com/libretro/Mesen-S/issues/8 */ + { "Mesen-S", { RC_CONSOLE_GAMEBOY, RC_CONSOLE_GAMEBOY_COLOR, 0 }}, + { NULL, { 0 } } +}; + +int rc_libretro_is_system_allowed(const char* library_name, int console_id) { + const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; + size_t library_name_length; + size_t i; + + if (!library_name || !library_name[0]) + return 1; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) { + for (i = 0; i < sizeof(core_filter->disallowed_consoles) / sizeof(core_filter->disallowed_consoles[0]); ++i) { + if (core_filter->disallowed_consoles[i] == console_id) + return 0; + } + break; + } + + ++core_filter; + } + + return 1; +} + +unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail) { + unsigned i; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + if (avail) + *avail = (unsigned)(size - address); + + return ®ions->data[i][address]; + } + + address -= (unsigned)size; + } + + if (avail) + *avail = 0; + + return NULL; +} + +unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address) { + return rc_libretro_memory_find_avail(regions, address, NULL); +} + +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, unsigned address, + uint8_t* buffer, uint32_t num_bytes) { + unsigned i; + uint32_t avail; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + avail = (unsigned)(size - address); + if (avail < num_bytes) + return avail; + + memcpy(buffer, ®ions->data[i][address], num_bytes); + return num_bytes; + } + + address -= (unsigned)size; + } + + return 0; +} + +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { + rc_libretro_verbose_message_callback = callback; +} + +static void rc_libretro_verbose(const char* message) { + if (rc_libretro_verbose_message_callback) + rc_libretro_verbose_message_callback(message); +} + +static const char* rc_memory_type_str(int type) { + switch (type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return "SRAM"; + case RC_MEMORY_TYPE_VIDEO_RAM: + return "VRAM"; + case RC_MEMORY_TYPE_UNUSED: + return "UNUSED"; + default: + break; + } + + return "SYSTEM RAM"; +} + +static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, + unsigned char* data, size_t size, const char* description) { + if (size == 0) + return; + + if (regions->count == (sizeof(regions->size) / sizeof(regions->size[0]))) { + rc_libretro_verbose("Too many memory memory regions to register"); + return; + } + + if (!data && regions->count > 0 && !regions->data[regions->count - 1]) { + /* extend null region */ + regions->size[regions->count - 1] += size; + } + else if (data && regions->count > 0 && + data == (regions->data[regions->count - 1] + regions->size[regions->count - 1])) { + /* extend non-null region */ + regions->size[regions->count - 1] += size; + } + else { + /* create new region */ + regions->data[regions->count] = data; + regions->size[regions->count] = size; + ++regions->count; + } + + regions->total_size += size; + + if (rc_libretro_verbose_message_callback) { + char message[128]; + snprintf(message, sizeof(message), "Registered 0x%04X bytes of %s at $%06X (%s)", (unsigned)size, + rc_memory_type_str(type), (unsigned)(regions->total_size - size), description); + rc_libretro_verbose_message_callback(message); + } +} + +static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info) { + /* no regions specified, assume system RAM followed by save RAM */ + char description[64]; + rc_libretro_core_memory_info_t info; + + snprintf(description, sizeof(description), "offset 0x%06x", 0); + + get_core_memory_info(RETRO_MEMORY_SYSTEM_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SYSTEM_RAM, info.data, info.size, description); + + get_core_memory_info(RETRO_MEMORY_SAVE_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); +} + +static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, unsigned real_address, size_t* offset) +{ + const struct retro_memory_descriptor* desc = mmap->descriptors; + const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; + + for (; desc < end; desc++) { + if (desc->select == 0) { + /* if select is 0, attempt to explcitly match the address */ + if (real_address >= desc->start && real_address < desc->start + desc->len) { + *offset = real_address - desc->start; + return desc; + } + } + else { + /* otherwise, attempt to match the address by matching the select bits */ + /* address is in the block if (addr & select) == (start & select) */ + if (((desc->start ^ real_address) & desc->select) == 0) { + /* get the relative offset of the address from the start of the memory block */ + unsigned reduced_address = real_address - (unsigned)desc->start; + + /* remove any bits from the reduced_address that correspond to the bits in the disconnect + * mask and collapse the remaining bits. this code was copied from the mmap_reduce function + * in RetroArch. i'm not exactly sure how it works, but it does. */ + unsigned disconnect_mask = (unsigned)desc->disconnect; + while (disconnect_mask) { + const unsigned tmp = (disconnect_mask - 1) & ~disconnect_mask; + reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); + disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; + } + + /* calculate the offset within the descriptor */ + *offset = reduced_address; + + /* sanity check - make sure the descriptor is large enough to hold the target address */ + if (reduced_address < desc->len) + return desc; + } + } + } + + *offset = 0; + return NULL; +} + +static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + const rc_memory_regions_t* console_regions) { + char description[64]; + unsigned i; + unsigned char* region_start; + unsigned char* desc_start; + size_t desc_size; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + size_t console_region_size = console_region->end_address - console_region->start_address + 1; + unsigned real_address = console_region->real_address; + unsigned disconnect_size = 0; + + while (console_region_size > 0) { + const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); + if (!desc) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + real_address - console_region->real_address + console_region->start_address); + rc_libretro_verbose(description); + } + + if (disconnect_size && console_region_size > disconnect_size) { + rc_libretro_memory_register_region(regions, console_region->type, NULL, disconnect_size, "null filler"); + console_region_size -= disconnect_size; + real_address += disconnect_size; + disconnect_size = 0; + continue; + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + break; + } + + snprintf(description, sizeof(description), "descriptor %u, offset 0x%06X%s", + (unsigned)(desc - mmap->descriptors) + 1, (int)offset, desc->ptr ? "" : " [no pointer]"); + + if (desc->ptr) { + desc_start = (uint8_t*)desc->ptr + desc->offset; + region_start = desc_start + offset; + } + else { + region_start = NULL; + } + + desc_size = desc->len - offset; + if (desc->disconnect && desc_size > desc->disconnect) { + /* if we need to extract a disconnect bit, the largest block we can read is up to + * the next time that bit flips */ + /* https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit */ + disconnect_size = (desc->disconnect & -((int)desc->disconnect)); + desc_size = disconnect_size - (real_address & (disconnect_size - 1)); + } + + if (console_region_size > desc_size) { + if (desc_size == 0) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + real_address - console_region->real_address + console_region->start_address); + rc_libretro_verbose(description); + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + console_region_size = 0; + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, desc_size, description); + console_region_size -= desc_size; + real_address += (unsigned)desc_size; + } + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, console_region_size, description); + console_region_size = 0; + } + } + } +} + +static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { + switch (region_type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return RETRO_MEMORY_SAVE_RAM; + case RC_MEMORY_TYPE_VIDEO_RAM: + return RETRO_MEMORY_VIDEO_RAM; + default: + break; + } + + return RETRO_MEMORY_SYSTEM_RAM; +} + +static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { + char description[64]; + unsigned i, j; + rc_libretro_core_memory_info_t info; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + const size_t console_region_size = console_region->end_address - console_region->start_address + 1; + const unsigned type = rc_libretro_memory_console_region_to_ram_type(console_region->type); + unsigned base_address = 0; + + for (j = 0; j <= i; ++j) { + const rc_memory_region_t* console_region2 = &console_regions->region[j]; + if (rc_libretro_memory_console_region_to_ram_type(console_region2->type) == type) { + base_address = console_region2->start_address; + break; + } + } + offset = console_region->start_address - base_address; + + get_core_memory_info(type, &info); + + if (offset < info.size) { + info.size -= offset; + + if (info.data) { + snprintf(description, sizeof(description), "offset 0x%06X", (int)offset); + info.data += offset; + } + else { + snprintf(description, sizeof(description), "null filler"); + } + } + else { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", console_region->start_address); + rc_libretro_verbose(description); + } + + info.data = NULL; + info.size = 0; + } + + if (console_region_size > info.size) { + /* want more than what is available, take what we can and null fill the rest */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, info.size, description); + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size - info.size, "null filler"); + } + else { + /* only take as much as we need */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, console_region_size, description); + } + } +} + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id) { + const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); + rc_libretro_memory_regions_t new_regions; + int has_valid_region = 0; + unsigned i; + + if (!regions) + return 0; + + memset(&new_regions, 0, sizeof(new_regions)); + + if (console_regions == NULL || console_regions->num_regions == 0) + rc_libretro_memory_init_without_regions(&new_regions, get_core_memory_info); + else if (mmap && mmap->num_descriptors != 0) + rc_libretro_memory_init_from_memory_map(&new_regions, mmap, console_regions); + else + rc_libretro_memory_init_from_unmapped_memory(&new_regions, get_core_memory_info, console_regions); + + /* determine if any valid regions were found */ + for (i = 0; i < new_regions.count; i++) { + if (new_regions.data[i]) { + has_valid_region = 1; + break; + } + } + + memcpy(regions, &new_regions, sizeof(*regions)); + return has_valid_region; +} + +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions) { + memset(regions, 0, sizeof(*regions)); +} + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path) { + char image_path[1024]; + char* m3u_contents; + char* ptr; + int64_t file_len; + void* file_handle; + int index = 0; + + memset(hash_set, 0, sizeof(*hash_set)); + + if (!rc_path_compare_extension(m3u_path, "m3u")) + return; + + file_handle = rc_file_open(m3u_path); + if (!file_handle) + { + rc_hash_error("Could not open playlist"); + return; + } + + rc_file_seek(file_handle, 0, SEEK_END); + file_len = rc_file_tell(file_handle); + rc_file_seek(file_handle, 0, SEEK_SET); + + m3u_contents = (char*)malloc((size_t)file_len + 1); + rc_file_read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + rc_file_close(file_handle); + + ptr = m3u_contents; + do + { + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') + { + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) + { + /* get the path to the save disk from the frontend, assign it a bogus hash so + * it doesn't get hashed later */ + if (get_image_path(index, image_path, sizeof(image_path))) + { + const char save_disk_hash[33] = "[SAVE DISK]"; + rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); + ++index; + } + } + } + else + { + /* non-empty line, tally a file */ + ++index; + } + + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; + + } while (*ptr); + + free(m3u_contents); + + if (hash_set->entries_count > 0) + { + /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by + * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */ + if (!get_image_path(index - 1, image_path, sizeof(image_path))) + hash_set->entries_count = 0; + } +} + +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { + if (hash_set->entries) + free(hash_set->entries); + memset(hash_set, 0, sizeof(*hash_set)); +} + +static unsigned rc_libretro_djb2(const char* input) +{ + unsigned result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, int game_id, const char hash[33]) { + const unsigned path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; + struct rc_libretro_hash_entry_t* entry = NULL; + struct rc_libretro_hash_entry_t* scan; + struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; + + if (path_djb2) + { + /* attempt to match the path */ + for (scan = hash_set->entries; scan < stop; ++scan) + { + if (scan->path_djb2 == path_djb2) + { + entry = scan; + break; + } + } + } + + if (!entry) + { + /* entry not found, allocate a new one */ + if (hash_set->entries_size == 0) + { + hash_set->entries_size = 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*) + malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + else if (hash_set->entries_count == hash_set->entries_size) + { + hash_set->entries_size += 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries, + hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + + entry = hash_set->entries + hash_set->entries_count++; + } + + /* update the entry */ + entry->path_djb2 = path_djb2; + entry->game_id = game_id; + memcpy(entry->hash, hash, sizeof(entry->hash)); +} + +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) +{ + const unsigned path_djb2 = rc_libretro_djb2(path); + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) + { + if (scan->path_djb2 == path_djb2) + return scan->hash; + } + + return NULL; +} + +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash) +{ + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) + { + if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0) + return scan->game_id; + } + + return 0; +} diff --git a/src/rcheevos/src/rcheevos/rc_libretro.h b/src/rcheevos/src/rcheevos/rc_libretro.h new file mode 100644 index 000000000..089631bae --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_libretro.h @@ -0,0 +1,92 @@ +#ifndef RC_LIBRETRO_H +#define RC_LIBRETRO_H + +/* this file comes from the libretro repository, which is not an explicit submodule. + * the integration must set up paths appropriately to find it. */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*****************************************************************************\ +| Disallowed Settings | +\*****************************************************************************/ + +typedef struct rc_disallowed_setting_t +{ + const char* setting; + const char* value; +} rc_disallowed_setting_t; + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name); +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); +int rc_libretro_is_system_allowed(const char* library_name, int console_id); + +/*****************************************************************************\ +| Memory Mapping | +\*****************************************************************************/ + +/* specifies a function to call for verbose logging */ +typedef void (*rc_libretro_message_callback)(const char*); +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback); + +#define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 +typedef struct rc_libretro_memory_regions_t +{ + unsigned char* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t total_size; + unsigned count; +} rc_libretro_memory_regions_t; + +typedef struct rc_libretro_core_memory_info_t +{ + unsigned char* data; + size_t size; +} rc_libretro_core_memory_info_t; + +typedef void (*rc_libretro_get_core_memory_info_func)(unsigned id, rc_libretro_core_memory_info_t* info); + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id); +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); + +unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address); +unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail); +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, unsigned address, uint8_t* buffer, uint32_t num_bytes); + +/*****************************************************************************\ +| Disk Identification | +\*****************************************************************************/ + +typedef struct rc_libretro_hash_entry_t +{ + uint32_t path_djb2; + int game_id; + char hash[33]; +} rc_libretro_hash_entry_t; + +typedef struct rc_libretro_hash_set_t +{ + struct rc_libretro_hash_entry_t* entries; + uint16_t entries_count; + uint16_t entries_size; +} rc_libretro_hash_set_t; + +typedef int (*rc_libretro_get_image_path_func)(unsigned index, char* buffer, size_t buffer_size); + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path); +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, int game_id, const char hash[33]); +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_LIBRETRO_H */ diff --git a/src/rcheevos/src/rcheevos/rc_validate.c b/src/rcheevos/src/rcheevos/rc_validate.c new file mode 100644 index 000000000..1751b890e --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_validate.c @@ -0,0 +1,847 @@ +#include "rc_validate.h" + +#include "rc_compat.h" +#include "rc_consoles.h" +#include "rc_internal.h" + +#include +#include + +static int rc_validate_memref(const rc_memref_t* memref, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + if (memref->address > max_address) { + snprintf(result, result_size, "Address %04X out of range (max %04X)", memref->address, max_address); + return 0; + } + + switch (console_id) { + case RC_CONSOLE_NINTENDO: + if (memref->address >= 0x0800 && memref->address <= 0x1FFF) { + snprintf(result, result_size, "Mirror RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_COLOR: + if (memref->address >= 0xE000 && memref->address <= 0xFDFF) { + snprintf(result, result_size, "Echo RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_PLAYSTATION: + if (memref->address <= 0xFFFF) { + snprintf(result, result_size, "Kernel RAM may not be initialized without real BIOS (address %04X)", memref->address); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, unsigned max_address) +{ + while (memref) { + if (!rc_validate_memref(memref, result, result_size, 0, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static unsigned rc_console_max_address(int console_id) +{ + const rc_memory_regions_t* memory_regions; + memory_regions = rc_console_memory_regions(console_id); + if (memory_regions && memory_regions->num_regions > 0) + return memory_regions->region[memory_regions->num_regions - 1].end_address; + + return 0xFFFFFFFF; +} + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + while (memref) { + if (!rc_validate_memref(memref, result, result_size, console_id, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static unsigned rc_max_value(const rc_operand_t* operand) +{ + if (operand->type == RC_OPERAND_CONST) + return operand->value.num; + + if (!rc_operand_is_memref(operand)) + return 0xFFFFFFFF; + + switch (operand->size) { + case RC_MEMSIZE_BIT_0: + case RC_MEMSIZE_BIT_1: + case RC_MEMSIZE_BIT_2: + case RC_MEMSIZE_BIT_3: + case RC_MEMSIZE_BIT_4: + case RC_MEMSIZE_BIT_5: + case RC_MEMSIZE_BIT_6: + case RC_MEMSIZE_BIT_7: + return 1; + + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + return 0xF; + + case RC_MEMSIZE_BITCOUNT: + return 8; + + case RC_MEMSIZE_8_BITS: + return (operand->type == RC_OPERAND_BCD) ? 165 : 0xFF; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 16665 : 0xFFFF; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 1666665 : 0xFFFFFF; + + default: + return (operand->type == RC_OPERAND_BCD) ? 166666665 : 0xFFFFFFFF; + } +} + +static unsigned rc_scale_value(unsigned value, char oper, const rc_operand_t* operand) +{ + switch (oper) { + case RC_OPERATOR_MULT: + { + unsigned long long scaled = ((unsigned long long)value) * rc_max_value(operand); + if (scaled > 0xFFFFFFFF) + return 0xFFFFFFFF; + + return (unsigned)scaled; + } + + case RC_OPERATOR_DIV: + { + const unsigned min_val = (operand->type == RC_OPERAND_CONST) ? operand->value.num : 1; + return value / min_val; + } + + case RC_OPERATOR_AND: + return rc_max_value(operand); + + case RC_OPERATOR_XOR: + return value | rc_max_value(operand); + + default: + return value; + } +} + +static int rc_validate_get_condition_index(const rc_condset_t* condset, const rc_condition_t* condition) +{ + int index = 1; + const rc_condition_t* scan; + for (scan = condset->conditions; scan != NULL; scan = scan->next) + { + if (scan == condition) + return index; + + ++index; + } + + return 0; +} + +static int rc_validate_range(unsigned min_val, unsigned max_val, char oper, unsigned max, char result[], const size_t result_size) +{ + switch (oper) { + case RC_OPERATOR_AND: + if (min_val > max) { + snprintf(result, result_size, "Mask has more bits than source"); + return 0; + } + else if (min_val == 0 && max_val == 0) { + snprintf(result, result_size, "Result of mask always 0"); + return 0; + } + break; + + case RC_OPERATOR_EQ: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_NE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GT: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_LE: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_LT: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_condset_internal(const rc_condset_t* condset, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + const rc_condition_t* cond; + char buffer[128]; + unsigned max_val; + int index = 1; + unsigned long long add_source_max = 0; + int in_add_hits = 0; + int in_add_address = 0; + int is_combining = 0; + + if (!condset) { + *result = '\0'; + return 1; + } + + for (cond = condset->conditions; cond; cond = cond->next, ++index) { + unsigned max = rc_max_value(&cond->operand1); + const int is_memref1 = rc_operand_is_memref(&cond->operand1); + const int is_memref2 = rc_operand_is_memref(&cond->operand2); + + if (!in_add_address) { + if (is_memref1 && !rc_validate_memref(cond->operand1.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + if (is_memref2 && !rc_validate_memref(cond->operand2.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + } + else { + in_add_address = 0; + } + + switch (cond->type) { + case RC_CONDITION_ADD_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + add_source_max += max; + is_combining = 1; + continue; + + case RC_CONDITION_SUB_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + if (add_source_max < max) /* potential underflow - may be expected */ + add_source_max = 0xFFFFFFFF; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_ADDRESS: + if (cond->operand1.type == RC_OPERAND_DELTA || cond->operand1.type == RC_OPERAND_PRIOR) { + snprintf(result, result_size, "Condition %d: Using pointer from previous frame", index); + return 0; + } + in_add_address = 1; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_SUB_HITS: + in_add_hits = 1; + is_combining = 1; + break; + + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + is_combining = 1; + break; + + default: + if (in_add_hits) { + if (cond->required_hits == 0) { + snprintf(result, result_size, "Condition %d: Final condition in AddHits chain must have a hit target", index); + return 0; + } + + in_add_hits = 0; + } + + is_combining = 0; + break; + } + + /* if we're in an add source chain, check for overflow */ + if (add_source_max) { + const unsigned long long overflow = add_source_max + max; + if (overflow > 0xFFFFFFFFUL) + max = 0xFFFFFFFF; + else + max += (unsigned)add_source_max; + } + + /* check for comparing two differently sized memrefs */ + max_val = rc_max_value(&cond->operand2); + if (max_val != max && add_source_max == 0 && is_memref1 && is_memref2) { + snprintf(result, result_size, "Condition %d: Comparing different memory sizes", index); + return 0; + } + + /* if either side is a memref, or there's a running add source chain, check for impossible comparisons */ + if (is_memref1 || is_memref2 || add_source_max) { + const size_t prefix_length = snprintf(result, result_size, "Condition %d: ", index); + + unsigned min_val; + switch (cond->operand2.type) { + case RC_OPERAND_CONST: + min_val = cond->operand2.value.num; + break; + + case RC_OPERAND_FP: + min_val = (int)cond->operand2.value.dbl; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref1 (because operand2==FP means !is_memref2) */ + if (!add_source_max && !rc_operand_is_float_memref(&cond->operand1) && + (float)min_val != cond->operand2.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + + default: + min_val = 0; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref2 (because operand1==FP means !is_memref1) */ + if (cond->operand1.type == RC_OPERAND_FP && !add_source_max && !rc_operand_is_float_memref(&cond->operand2) && + (float)((int)cond->operand1.value.dbl) != cond->operand1.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + } + + if (rc_operand_is_float(&cond->operand2) && rc_operand_is_float(&cond->operand1)) { + /* both sides are floats, don't validate range*/ + } else if (!rc_validate_range(min_val, max_val, cond->oper, max, result + prefix_length, result_size - prefix_length)) { + return 0; + } + } + + add_source_max = 0; + } + + if (is_combining) { + snprintf(result, result_size, "Final condition type expects another condition to follow"); + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, unsigned max_address) +{ + return rc_validate_condset_internal(condset, result, result_size, 0, max_address); +} + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + return rc_validate_condset_internal(condset, result, result_size, console_id, max_address); +} + +static int rc_validate_is_combining_condition(const rc_condition_t* condition) +{ + switch (condition->type) + { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_SUB_SOURCE: + return 1; + + default: + return 0; + } +} + +static const rc_condition_t* rc_validate_next_non_combining_condition(const rc_condition_t* condition) +{ + int is_combining = rc_validate_is_combining_condition(condition); + for (condition = condition->next; condition != NULL; condition = condition->next) + { + if (rc_validate_is_combining_condition(condition)) + is_combining = 1; + else if (is_combining) + is_combining = 0; + else + return condition; + } + + return NULL; +} + +static int rc_validate_get_opposite_comparison(int oper) +{ + switch (oper) + { + case RC_OPERATOR_EQ: return RC_OPERATOR_NE; + case RC_OPERATOR_NE: return RC_OPERATOR_EQ; + case RC_OPERATOR_LT: return RC_OPERATOR_GE; + case RC_OPERATOR_LE: return RC_OPERATOR_GT; + case RC_OPERATOR_GT: return RC_OPERATOR_LE; + case RC_OPERATOR_GE: return RC_OPERATOR_LT; + default: return oper; + } +} + +static const rc_operand_t* rc_validate_get_comparison(const rc_condition_t* condition, int* comparison, unsigned* value) +{ + if (rc_operand_is_memref(&condition->operand1)) + { + if (condition->operand2.type != RC_OPERAND_CONST) + return NULL; + + *comparison = condition->oper; + *value = condition->operand2.value.num; + return &condition->operand1; + } + + if (condition->operand1.type != RC_OPERAND_CONST) + return NULL; + + if (!rc_operand_is_memref(&condition->operand2)) + return NULL; + + *comparison = rc_validate_get_opposite_comparison(condition->oper); + *value = condition->operand1.value.num; + return &condition->operand2; +} + +enum { + RC_OVERLAP_NONE = 0, + RC_OVERLAP_CONFLICTING, + RC_OVERLAP_REDUNDANT, + RC_OVERLAP_DEFER +}; + +static int rc_validate_comparison_overlap(int comparison1, unsigned value1, int comparison2, unsigned value2) +{ + /* NOTE: this only cares if comp2 conflicts with comp1. + * If comp1 conflicts with comp2, we'll catch that later (return RC_OVERLAP_NONE for now) */ + switch (comparison2) + { + case RC_OPERATOR_EQ: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a == 1 | a == 1 && a == 2 | a == 2 && a == 1 */ + /* redundant conflict conflict */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a == 1 | a <= 1 && a == 2 | a <= 2 && a == 1 */ + /* defer conflict defer */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a == 1 | a >= 1 && a == 2 | a >= 2 && a == 1 */ + /* defer defer conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a == 1 | a != 1 && a == 2 | a != 2 && a == 1 */ + /* conflict defer defer */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_LT: /* a < 1 && a == 1 | a < 1 && a == 2 | a < 2 && a == 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a == 1 | a > 1 && a == 2 | a > 2 && a == 1 */ + /* conflict defer conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_NE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a != 1 | a == 1 && a != 2 | a == 2 && a != 1 */ + /* conflict redundant redundant */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_REDUNDANT; + case RC_OPERATOR_LE: /* a <= 1 && a != 1 | a <= 1 && a != 2 | a <= 2 && a != 1 */ + /* none redundant none */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a != 1 | a >= 1 && a != 2 | a >= 2 && a != 1 */ + /* none none redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a != 1 | a != 1 && a != 2 | a != 2 && a != 1 */ + /* redundant none none */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a != 1 | a < 1 && a != 2 | a < 2 && a != 1 */ + /* redundant redundant none */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a != 1 | a > 1 && a != 2 | a > 2 && a != 1 */ + /* redundant none redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a < 1 | a == 1 && a < 2 | a == 2 && a < 1 */ + /* conflict redundant conflict */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a < 1 | a <= 1 && a < 2 | a <= 2 && a < 1 */ + /* defer redundant defer */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a < 1 | a >= 1 && a < 2 | a >= 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a < 1 | a != 1 && a < 2 | a != 2 && a < 1 */ + /* defer none defer */ + return (value1 >= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a < 1 | a < 1 && a < 2 | a < 2 && a < 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a < 1 | a > 1 && a < 2 | a > 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a <= 1 | a == 1 && a <= 2 | a == 2 && a <= 1 */ + /* redundant redundant conflict */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a <= 1 | a <= 1 && a <= 2 | a <= 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a <= 1 | a >= 1 && a <= 2 | a >= 2 && a <= 1 */ + /* none none conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a <= 1 | a != 1 && a <= 2 | a != 2 && a <= 1 */ + /* none none defer */ + return (value1 > value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a <= 1 | a < 1 && a <= 2 | a < 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a <= 1 | a > 1 && a <= 2 | a > 2 && a <= 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_GT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a > 1 | a == 1 && a > 2 | a == 2 && a > 1 */ + /* conflict conflict redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a > 1 | a <= 1 && a > 2 | a <= 2 && a > 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a > 1 | a >= 1 && a > 2 | a >= 2 && a > 1 */ + /* defer defer redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a > 1 | a != 1 && a > 2 | a != 2 && a > 1 */ + /* defer defer none */ + return (value1 <= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a > 1 | a < 1 && a > 2 | a < 2 && a > 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a > 1 | a > 1 && a > 2 | a > 2 && a > 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_GE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a >= 1 | a == 1 && a >= 2 | a == 2 && a >= 1 */ + /* redundant conflict redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a >= 1 | a <= 1 && a >= 2 | a <= 2 && a >= 1 */ + /* none conflict none */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a >= 1 | a >= 1 && a >= 2 | a >= 2 && a >= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a >= 1 | a != 1 && a >= 2 | a != 2 && a >= 1 */ + /* none defer none */ + return (value1 < value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a >= 1 | a < 1 && a >= 2 | a < 2 && a >= 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a >= 1 | a > 1 && a >= 2 | a > 2 && a >= 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + } + + return RC_OVERLAP_NONE; +} + +static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, const rc_condset_t* compare_conditions, + const char* prefix, const char* compare_prefix, char result[], const size_t result_size) +{ + int comparison1, comparison2; + unsigned value1, value2; + const rc_operand_t* operand1; + const rc_operand_t* operand2; + const rc_condition_t* compare_condition; + const rc_condition_t* condition; + int overlap; + + /* empty group */ + if (conditions == NULL || compare_conditions == NULL) + return 1; + + /* outer loop is the source conditions */ + for (condition = conditions->conditions; condition != NULL; + condition = rc_validate_next_non_combining_condition(condition)) + { + /* hits can be captured at any time, so any potential conflict will not be conflicting at another time */ + if (condition->required_hits) + continue; + + operand1 = rc_validate_get_comparison(condition, &comparison1, &value1); + if (!operand1) + continue; + + switch (condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison1 = rc_validate_get_opposite_comparison(comparison1); + break; + default: + if (rc_validate_is_combining_condition(condition)) + continue; + break; + } + + /* inner loop is the potentially conflicting conditions */ + for (compare_condition = compare_conditions->conditions; compare_condition != NULL; + compare_condition = rc_validate_next_non_combining_condition(compare_condition)) + { + if (compare_condition == condition) + continue; + + if (compare_condition->required_hits) + continue; + + operand2 = rc_validate_get_comparison(compare_condition, &comparison2, &value2); + if (!operand2 || operand2->value.memref->address != operand1->value.memref->address || + operand2->size != operand1->size || operand2->type != operand1->type) + continue; + + switch (compare_condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison2 = rc_validate_get_opposite_comparison(comparison2); + break; + default: + if (rc_validate_is_combining_condition(compare_condition)) + continue; + break; + } + + overlap = rc_validate_comparison_overlap(comparison1, value1, comparison2, value2); + switch (overlap) + { + case RC_OVERLAP_CONFLICTING: + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf conflicts between groups, unless both conditions are PauseIfs */ + if (conditions != compare_conditions && compare_condition->type != condition->type) + continue; + } + break; + + case RC_OVERLAP_REDUNDANT: + if (prefix != compare_prefix && strcmp(compare_prefix, "Core") == 0) + { + /* if the alt condition is more restrictive than the core condition, ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + } + + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + /* if the PauseIf is less restrictive than the other condition, it's just a guard. ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + + /* PauseIf redundant with ResetIf is a conflict (both are inverted comparisons) */ + if (compare_condition->type == RC_CONDITION_RESET_IF || condition->type == RC_CONDITION_RESET_IF) + overlap = RC_OVERLAP_CONFLICTING; + } + else if (compare_condition->type == RC_CONDITION_RESET_IF && condition->type != RC_CONDITION_RESET_IF) + { + /* only ever report the redundancy on the non-ResetIf condition. The ResetIf is allowed to + * fire when the non-ResetIf condition is not true. */ + continue; + } + else if (compare_condition->type == RC_CONDITION_MEASURED_IF || condition->type == RC_CONDITION_MEASURED_IF) + { + /* ignore MeasuredIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + if (compare_condition->type == RC_CONDITION_MEASURED_IF && condition->type != RC_CONDITION_MEASURED_IF) + { + /* only ever report the redundancy on the non-MeasuredIf condition. The MeasuredIf provides + * additional functionality. */ + continue; + } + } + else if (compare_condition->type == RC_CONDITION_TRIGGER || condition->type == RC_CONDITION_TRIGGER) + { + /* Trigger is allowed to be redundant with non-trigger conditions as there may be limits that start a + * challenge that are furhter reduced for the completion of the challenge */ + if (compare_condition->type != condition->type) + continue; + } + break; + + default: + continue; + } + + if (compare_prefix && *compare_prefix) + { + snprintf(result, result_size, "%s Condition %d: %s with %s Condition %d", + compare_prefix, rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + prefix, rc_validate_get_condition_index(conditions, condition)); + } + else + { + snprintf(result, result_size, "Condition %d: %s with Condition %d", + rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + rc_validate_get_condition_index(conditions, condition)); + } + return 0; + } + } + + return 1; +} + +static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + const rc_condset_t* alt; + int index; + + if (!trigger->alternative) + { + if (!rc_validate_condset_internal(trigger->requirement, result, result_size, console_id, max_address)) + return 0; + + return rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "", "", result, result_size); + } + + snprintf(result, result_size, "Core "); + if (!rc_validate_condset_internal(trigger->requirement, result + 5, result_size - 5, console_id, max_address)) + return 0; + + /* compare core to itself */ + if (!rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "Core", "Core", result, result_size)) + return 0; + + index = 1; + for (alt = trigger->alternative; alt; alt = alt->next, ++index) { + char altname[16]; + const size_t prefix_length = snprintf(result, result_size, "Alt%d ", index); + if (!rc_validate_condset_internal(alt, result + prefix_length, result_size - prefix_length, console_id, max_address)) + return 0; + + /* compare alt to itself */ + snprintf(altname, sizeof(altname), "Alt%d", index); + if (!rc_validate_conflicting_conditions(alt, alt, altname, altname, result, result_size)) + return 0; + + /* compare alt to core */ + if (!rc_validate_conflicting_conditions(trigger->requirement, alt, "Core", altname, result, result_size)) + return 0; + + /* compare core to alt */ + if (!rc_validate_conflicting_conditions(alt, trigger->requirement, altname, "Core", result, result_size)) + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, unsigned max_address) +{ + return rc_validate_trigger_internal(trigger, result, result_size, 0, max_address); +} + +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + return rc_validate_trigger_internal(trigger, result, result_size, console_id, max_address); +} diff --git a/src/rcheevos/src/rcheevos/rc_validate.h b/src/rcheevos/src/rcheevos/rc_validate.h new file mode 100644 index 000000000..ba7df325e --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_validate.h @@ -0,0 +1,26 @@ +#ifndef RC_VALIDATE_H +#define RC_VALIDATE_H + +#include "rc_runtime_types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, unsigned max_address); + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, unsigned max_address); +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, unsigned max_address); + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, int console_id); + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, int console_id); +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VALIDATE_H */ diff --git a/src/rcheevos/src/rcheevos/rc_version.h b/src/rcheevos/src/rcheevos/rc_version.h new file mode 100644 index 000000000..cdf857d5f --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_version.h @@ -0,0 +1,29 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define RCHEEVOS_VERSION_MAJOR 10 +#define RCHEEVOS_VERSION_MINOR 7 +#define RCHEEVOS_VERSION_PATCH 0 + +#define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) +#define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) + +#define RCHEEVOS_MAKE_STRING(num) #num +#define RCHEEVOS_MAKE_VERSION_STRING(major, minor, patch) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) "." RCHEEVOS_MAKE_STRING(patch) +#define RCHEEVOS_MAKE_VERSION_STRING_SHORT(major, minor) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) + +#if RCHEEVOS_VERSION_PATCH > 0 + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) +#else + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING_SHORT(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VERSION_H */ diff --git a/src/rcheevos/src/rcheevos/richpresence.c b/src/rcheevos/src/rcheevos/richpresence.c new file mode 100644 index 000000000..3ad6bd991 --- /dev/null +++ b/src/rcheevos/src/rcheevos/richpresence.c @@ -0,0 +1,845 @@ +#include "rc_internal.h" + +#include "rc_compat.h" + +#include + +/* special formats only used by rc_richpresence_display_part_t.display_type. must not overlap other RC_FORMAT values */ +enum { + RC_FORMAT_STRING = 101, + RC_FORMAT_LOOKUP = 102, + RC_FORMAT_UNKNOWN_MACRO = 103, + RC_FORMAT_ASCIICHAR = 104, + RC_FORMAT_UNICODECHAR = 105 +}; + +static rc_memref_value_t* rc_alloc_helper_variable_memref_value(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { + const char* end; + rc_value_t* variable; + 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]) { + /* if it's not a derived size, we can reference the memref directly */ + if (rc_memref_shared_size(size) == size) + return &rc_alloc_memref(parse, address, size, 0)->value; + } + } + + /* not a simple memory reference, need to create a variable */ + variable = rc_alloc_helper_variable(memaddr, memaddr_len, parse); + if (!variable) + return NULL; + + return &variable->value; +} + +static const char* rc_parse_line(const char* line, const char** end, rc_parse_state_t* parse) { + const char* nextline; + const char* endline; + + /* get a single line */ + nextline = line; + while (*nextline && *nextline != '\n') + ++nextline; + + /* if a trailing comment marker (//) exists, the line stops there */ + endline = line; + while (endline < nextline && (endline[0] != '/' || endline[1] != '/' || (endline > line && endline[-1] == '\\'))) + ++endline; + + if (endline == nextline) { + /* trailing whitespace on a line without a comment marker may be significant, just remove the line ending */ + if (endline > line && endline[-1] == '\r') + --endline; + } else { + /* remove trailing whitespace before the comment marker */ + while (endline > line && isspace((int)((unsigned char*)endline)[-1])) + --endline; + } + + /* point end at the first character to ignore, it makes subtraction for length easier */ + *end = endline; + + /* tally the line */ + ++parse->lines_read; + + /* skip the newline character so we're pointing at the next line */ + if (*nextline == '\n') + ++nextline; + + return nextline; +} + +typedef struct rc_richpresence_builtin_macro_t { + const char* name; + size_t name_len; + unsigned short display_type; +} rc_richpresence_builtin_macro_t; + +static rc_richpresence_display_t* rc_parse_richpresence_display_internal(const char* line, const char* endline, rc_parse_state_t* parse, rc_richpresence_lookup_t* first_lookup) { + rc_richpresence_display_t* self; + rc_richpresence_display_part_t* part; + rc_richpresence_display_part_t** next; + rc_richpresence_lookup_t* lookup; + const char* ptr; + const char* in; + char* out; + + if (endline - line < 1) { + parse->offset = RC_MISSING_DISPLAY_STRING; + return 0; + } + + { + self = RC_ALLOC(rc_richpresence_display_t, parse); + memset(self, 0, sizeof(rc_richpresence_display_t)); + next = &self->display; + } + + /* break the string up on macros: text @macro() moretext */ + do { + ptr = line; + while (ptr < endline) { + if (*ptr == '@' && (ptr == line || ptr[-1] != '\\')) /* ignore escaped @s */ + break; + + ++ptr; + } + + if (ptr > line) { + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + /* handle string part */ + part->display_type = RC_FORMAT_STRING; + part->text = rc_alloc_str(parse, line, (int)(ptr - line)); + if (part->text) { + /* remove backslashes used for escaping */ + in = part->text; + while (*in && *in != '\\') + ++in; + + if (*in == '\\') { + out = (char*)in++; + while (*in) { + *out++ = *in++; + if (*in == '\\') + ++in; + } + *out = '\0'; + } + } + } + + if (*ptr == '@') { + /* handle macro part */ + size_t macro_len; + + line = ++ptr; + while (ptr < endline && *ptr != '(') + ++ptr; + + if (ptr == endline) { + parse->offset = RC_MISSING_VALUE; + return 0; + } + + macro_len = ptr - line; + + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + part->display_type = RC_FORMAT_UNKNOWN_MACRO; + + /* find the lookup and hook it up */ + lookup = first_lookup; + while (lookup) { + if (strncmp(lookup->name, line, macro_len) == 0 && lookup->name[macro_len] == '\0') { + part->text = lookup->name; + part->lookup = lookup; + part->display_type = lookup->format; + break; + } + + lookup = lookup->next; + } + + if (!lookup) { + static const rc_richpresence_builtin_macro_t builtin_macros[] = { + {"Number", 6, RC_FORMAT_VALUE}, + {"Score", 5, RC_FORMAT_SCORE}, + {"Centiseconds", 12, RC_FORMAT_CENTISECS}, + {"Seconds", 7, RC_FORMAT_SECONDS}, + {"Minutes", 7, RC_FORMAT_MINUTES}, + {"SecondsAsMinutes", 16, RC_FORMAT_SECONDS_AS_MINUTES}, + {"ASCIIChar", 9, RC_FORMAT_ASCIICHAR}, + {"UnicodeChar", 11, RC_FORMAT_UNICODECHAR}, + {"Float1", 6, RC_FORMAT_FLOAT1}, + {"Float2", 6, RC_FORMAT_FLOAT2}, + {"Float3", 6, RC_FORMAT_FLOAT3}, + {"Float4", 6, RC_FORMAT_FLOAT4}, + {"Float5", 6, RC_FORMAT_FLOAT5}, + {"Float6", 6, RC_FORMAT_FLOAT6}, + }; + size_t i; + + for (i = 0; i < sizeof(builtin_macros) / sizeof(builtin_macros[0]); ++i) { + if (macro_len == builtin_macros[i].name_len && + memcmp(builtin_macros[i].name, line, builtin_macros[i].name_len) == 0) { + part->text = builtin_macros[i].name; + part->lookup = NULL; + part->display_type = builtin_macros[i].display_type; + break; + } + } + } + + /* find the closing parenthesis */ + in = line; + line = ++ptr; + while (ptr < endline && *ptr != ')') + ++ptr; + + if (*ptr != ')') { + /* non-terminated macro, dump the macro and the remaining portion of the line */ + --in; /* already skipped over @ */ + part->display_type = RC_FORMAT_STRING; + part->text = rc_alloc_str(parse, in, (int)(ptr - in)); + } + else if (part->display_type != RC_FORMAT_UNKNOWN_MACRO) { + part->value = rc_alloc_helper_variable_memref_value(line, (int)(ptr - line), parse); + if (parse->offset < 0) + return 0; + + ++ptr; + } + else { + /* assert: the allocated string is going to be smaller than the memory used for the parameter of the macro */ + ++ptr; + part->text = rc_alloc_str(parse, in, (int)(ptr - in)); + } + } + + line = ptr; + } while (line < endline); + + *next = 0; + + return self; +} + +static int rc_richpresence_lookup_item_count(rc_richpresence_lookup_item_t* item) +{ + if (item == NULL) + return 0; + + return (rc_richpresence_lookup_item_count(item->left) + rc_richpresence_lookup_item_count(item->right) + 1); +} + +static void rc_rebalance_richpresence_lookup_get_items(rc_richpresence_lookup_item_t* root, + rc_richpresence_lookup_item_t** items, int* index) +{ + if (root->left != NULL) + rc_rebalance_richpresence_lookup_get_items(root->left, items, index); + + items[*index] = root; + ++(*index); + + if (root->right != NULL) + rc_rebalance_richpresence_lookup_get_items(root->right, items, index); +} + +static void rc_rebalance_richpresence_lookup_rebuild(rc_richpresence_lookup_item_t** root, + rc_richpresence_lookup_item_t** items, int first, int last) +{ + int mid = (first + last) / 2; + rc_richpresence_lookup_item_t* item = items[mid]; + *root = item; + + if (mid == first) + item->left = NULL; + else + rc_rebalance_richpresence_lookup_rebuild(&item->left, items, first, mid - 1); + + if (mid == last) + item->right = NULL; + else + rc_rebalance_richpresence_lookup_rebuild(&item->right, items, mid + 1, last); +} + +static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** root, rc_parse_state_t* parse) +{ + rc_richpresence_lookup_item_t** items; + rc_scratch_buffer_t* buffer; + 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 = RC_ALIGN(buffer->offset); + 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, parse); + + if (endline - line < 2) { + /* ignore full line comments inside a lookup */ + if (line[0] == '/' && line[1] == '/') + continue; + + /* empty line indicates end of lookup */ + if (lookup->root) + rc_rebalance_richpresence_lookup(&lookup->root, parse); + break; + } + + /* "*=XXX" specifies default label if lookup does not provide a mapping for the value */ + if (line[0] == '*' && line[1] == '=') { + line += 2; + lookup->default_label = rc_alloc_str(parse, line, (int)(endline - line)); + continue; + } + + label = line; + while (label < endline && *label != '=') + ++label; + + if (label == endline) { + parse->offset = RC_MISSING_VALUE; + break; + } + ++label; + + do { + /* get the value for the mapping */ + if (line[0] == '0' && line[1] == 'x') { + line += 2; + base = 16; + } else { + base = 10; + } + + first = (unsigned)strtoul(line, &endptr, base); + + /* check for a range */ + if (*endptr != '-') { + /* no range, just set last to first */ + last = first; + } + else { + /* range, get last value */ + line = endptr + 1; + + if (line[0] == '0' && line[1] == 'x') { + line += 2; + base = 16; + } else { + base = 10; + } + + last = (unsigned)strtoul(line, &endptr, base); + } + + /* ignore spaces after the number - was previously ignored as string was split on equals */ + while (*endptr == ' ') + ++endptr; + + /* if we've found the equal sign, this is the last item */ + if (*endptr == '=') { + rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + break; + } + + /* otherwise, if it's not a comma, it's an error */ + if (*endptr != ',') { + parse->offset = RC_INVALID_CONST_OPERAND; + break; + } + + /* insert the current item and continue scanning the next one */ + rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + line = endptr + 1; + } while (line < endline); + + } while (parse->offset > 0); + + return nextline; +} + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse) { + rc_richpresence_display_t** nextdisplay; + rc_richpresence_lookup_t* firstlookup = NULL; + rc_richpresence_lookup_t** nextlookup = &firstlookup; + rc_richpresence_lookup_t* lookup; + rc_trigger_t* trigger; + char format[64]; + const char* display = 0; + const char* line; + const char* nextline; + const char* endline; + const char* ptr; + int hasdisplay = 0; + int display_line = 0; + int chars; + + /* special case for empty script to return 1 line read */ + if (!*script) { + parse->lines_read = 1; + parse->offset = RC_MISSING_DISPLAY_STRING; + return; + } + + /* first pass: process macro initializers */ + line = script; + while (*line) { + nextline = rc_parse_line(line, &endline, parse); + if (strncmp(line, "Lookup:", 7) == 0) { + line += 7; + + lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); + lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); + lookup->format = RC_FORMAT_LOOKUP; + lookup->root = NULL; + lookup->default_label = ""; + *nextlookup = lookup; + nextlookup = &lookup->next; + + nextline = rc_parse_richpresence_lookup(lookup, nextline, parse); + if (parse->offset < 0) + return; + + } else if (strncmp(line, "Format:", 7) == 0) { + line += 7; + + lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); + lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); + lookup->root = NULL; + lookup->default_label = ""; + *nextlookup = lookup; + nextlookup = &lookup->next; + + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + if (parse->buffer && strncmp(line, "FormatType=", 11) == 0) { + line += 11; + + chars = (int)(endline - line); + if (chars > 63) + chars = 63; + memcpy(format, line, chars); + format[chars] = '\0'; + + lookup->format = (unsigned short)rc_parse_format(format); + } else { + lookup->format = RC_FORMAT_VALUE; + } + } else if (strncmp(line, "Display:", 8) == 0) { + display = nextline; + display_line = parse->lines_read; + + /* scan as long as we find conditional lines or full line comments */ + do { + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + } while (*line == '?' || (line[0] == '/' && line[1] == '/')); + } + + line = nextline; + } + + *nextlookup = 0; + self->first_lookup = firstlookup; + + nextdisplay = &self->first_display; + + /* second pass, process display string*/ + if (display) { + /* point the parser back at the display strings */ + int lines_read = parse->lines_read; + parse->lines_read = display_line; + line = display; + + nextline = rc_parse_line(line, &endline, parse); + + do { + if (line[0] == '?') { + /* conditional display: ?trigger?string */ + ptr = ++line; + while (ptr < endline && *ptr != '?') + ++ptr; + + if (ptr < endline) { + *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); + if (parse->offset < 0) + return; + trigger = &((*nextdisplay)->trigger); + rc_parse_trigger_internal(trigger, &line, parse); + trigger->memrefs = 0; + if (parse->offset < 0) + return; + if (parse->buffer) + nextdisplay = &((*nextdisplay)->next); + } + } + else if (line[0] != '/' || line[1] != '/') { + break; + } + + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + } while (1); + + /* non-conditional display: string */ + *nextdisplay = rc_parse_richpresence_display_internal(line, endline, parse, firstlookup); + if (*nextdisplay) { + hasdisplay = 1; + nextdisplay = &((*nextdisplay)->next); + + /* restore the parser state */ + parse->lines_read = lines_read; + } + else { + /* this should only happen if the line is blank. + * expect parse->offset to be RC_MISSING_DISPLAY_STRING and leave parse->lines_read + * on the current line for error tracking. */ + } + } + + /* finalize */ + *nextdisplay = 0; + + if (!hasdisplay && parse->offset > 0) { + parse->offset = RC_MISSING_DISPLAY_STRING; + } +} + +int rc_richpresence_size_lines(const char* script, int* lines_read) { + rc_richpresence_t* self; + rc_parse_state_t parse; + rc_memref_t* first_memref; + rc_value_t* variables; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &first_memref); + rc_init_parse_state_variables(&parse, &variables); + + self = RC_ALLOC(rc_richpresence_t, &parse); + rc_parse_richpresence_internal(self, script, &parse); + + if (lines_read) + *lines_read = parse.lines_read; + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +int rc_richpresence_size(const char* script) { + return rc_richpresence_size_lines(script, NULL); +} + +rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx) { + rc_richpresence_t* self; + rc_parse_state_t parse; + + if (!buffer || !script) + return NULL; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_richpresence_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + rc_init_parse_state_variables(&parse, &self->variables); + + rc_parse_richpresence_internal(self, script, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : NULL; +} + +void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L) { + rc_richpresence_display_t* display; + + rc_update_memref_values(richpresence->memrefs, peek, peek_ud); + rc_update_variables(richpresence->variables, peek, peek_ud, L); + + for (display = richpresence->first_display; display; display = display->next) { + if (display->trigger.has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, L); + } +} + +static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part, char* buffer, unsigned buffersize) +{ + rc_richpresence_lookup_item_t* item; + rc_typed_value_t value; + char tmp[256]; + char* ptr = buffer; + const char* text; + size_t chars; + + *ptr = '\0'; + while (part) { + switch (part->display_type) { + case RC_FORMAT_STRING: + text = part->text; + chars = strlen(text); + break; + + case RC_FORMAT_LOOKUP: + rc_typed_value_from_memref_value(&value, part->value); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + + text = part->lookup->default_label; + item = part->lookup->root; + while (item) { + if (value.value.u32 > item->last) { + item = item->right; + } + else if (value.value.u32 < item->first) { + item = item->left; + } + else { + text = item->label; + break; + } + } + + chars = strlen(text); + break; + + case RC_FORMAT_ASCIICHAR: + chars = 0; + text = tmp; + value.type = RC_VALUE_TYPE_UNSIGNED; + + do { + value.value.u32 = part->value->value; + if (value.value.u32 == 0) { + /* null terminator - skip over remaining character macros */ + while (part->next && part->next->display_type == RC_FORMAT_ASCIICHAR) + part = part->next; + break; + } + + if (value.value.u32 < 32 || value.value.u32 >= 127) + value.value.u32 = '?'; + + tmp[chars++] = (char)value.value.u32; + if (chars == sizeof(tmp) || !part->next || part->next->display_type != RC_FORMAT_ASCIICHAR) + break; + + part = part->next; + } while (1); + + tmp[chars] = '\0'; + break; + + case RC_FORMAT_UNICODECHAR: + chars = 0; + text = tmp; + value.type = RC_VALUE_TYPE_UNSIGNED; + + do { + value.value.u32 = part->value->value; + if (value.value.u32 == 0) { + /* null terminator - skip over remaining character macros */ + while (part->next && part->next->display_type == RC_FORMAT_UNICODECHAR) + part = part->next; + break; + } + + if (value.value.u32 < 32 || value.value.u32 > 65535) + value.value.u32 = 0xFFFD; /* unicode replacement char */ + + if (value.value.u32 < 0x80) { + tmp[chars++] = (char)value.value.u32; + } + else if (value.value.u32 < 0x0800) { + tmp[chars + 1] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars] = (char)(0xC0 | (value.value.u32 & 0x1F)); + chars += 2; + } + else { + /* surrogate pair not supported, convert to replacement char */ + if (value.value.u32 >= 0xD800 && value.value.u32 < 0xE000) + value.value.u32 = 0xFFFD; + + tmp[chars + 2] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars + 1] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars] = (char)(0xE0 | (value.value.u32 & 0x1F)); + chars += 3; + } + + if (chars >= sizeof(tmp) - 3 || !part->next || part->next->display_type != RC_FORMAT_UNICODECHAR) + break; + + part = part->next; + } while (1); + + tmp[chars] = '\0'; + break; + + case RC_FORMAT_UNKNOWN_MACRO: + chars = snprintf(tmp, sizeof(tmp), "[Unknown macro]%s", part->text); + text = tmp; + break; + + default: + rc_typed_value_from_memref_value(&value, part->value); + chars = rc_format_typed_value(tmp, sizeof(tmp), &value, part->display_type); + text = tmp; + break; + } + + if (chars > 0 && buffersize > 0) { + if ((unsigned)chars >= buffersize) { + /* prevent write past end of buffer */ + memcpy(ptr, text, buffersize - 1); + ptr[buffersize - 1] = '\0'; + buffersize = 0; + } + else { + memcpy(ptr, text, chars); + ptr[chars] = '\0'; + buffersize -= (unsigned)chars; + } + } + + ptr += chars; + part = part->next; + } + + return (int)(ptr - buffer); +} + +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, 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); +} + +void rc_reset_richpresence(rc_richpresence_t* self) { + rc_richpresence_display_t* display; + rc_value_t* variable; + + for (display = self->first_display; display; display = display->next) + rc_reset_trigger(&display->trigger); + + for (variable = self->variables; variable; variable = variable->next) + rc_reset_value(variable); +} diff --git a/src/rcheevos/src/rcheevos/runtime.c b/src/rcheevos/src/rcheevos/runtime.c new file mode 100644 index 000000000..a5a66abd6 --- /dev/null +++ b/src/rcheevos/src/rcheevos/runtime.c @@ -0,0 +1,892 @@ +#include "rc_runtime.h" +#include "rc_internal.h" +#include "rc_compat.h" + +#include "../rhash/md5.h" + +#include +#include + +#define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 + +rc_runtime_t* rc_runtime_alloc(void) { + rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); + + if (self) { + rc_runtime_init(self); + self->owns_self = 1; + } + + return self; +} + +void rc_runtime_init(rc_runtime_t* self) { + memset(self, 0, sizeof(rc_runtime_t)); + self->next_memref = &self->memrefs; + self->next_variable = &self->variables; +} + +void rc_runtime_destroy(rc_runtime_t* self) { + 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; + + if (self->owns_self) { + free(self); + } +} + +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; +} + +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id, unsigned* measured_value, unsigned* measured_target) +{ + const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); + if (!measured_value || !measured_target) + return 0; + + if (!trigger) { + *measured_value = *measured_target = 0; + return 0; + } + + if (rc_trigger_state_active(trigger->state)) { + *measured_value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; + *measured_target = trigger->measured_target; + } + else { + /* don't report measured information for inactive triggers */ + *measured_value = *measured_target = 0; + } + + return 1; +} + +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned id, char* buffer, size_t buffer_size) +{ + const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); + unsigned value; + if (!buffer || !buffer_size) + return 0; + + if (!trigger || /* no trigger */ + trigger->measured_target == 0 || /* not measured */ + !rc_trigger_state_active(trigger->state)) { /* don't report measured value for inactive triggers */ + *buffer = '\0'; + return 0; + } + + /* cap the value at the target so we can count past the target: "107 >= 100" */ + value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; + if (value > trigger->measured_target) + value = trigger->measured_target; + + if (trigger->measured_as_percent) { + unsigned percent = (unsigned)(((unsigned long long)value * 100) / trigger->measured_target); + return snprintf(buffer, buffer_size, "%u%%", percent); + } + + return snprintf(buffer, buffer_size, "%u/%u", value, trigger->measured_target); +} + +static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, unsigned index) { + 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; + rc_runtime_lboard_t* runtime_lboard; + 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 */ + runtime_lboard = &self->lboards[self->lboard_count++]; + runtime_lboard->id = id; + runtime_lboard->value = 0; + runtime_lboard->lboard = lboard; + runtime_lboard->buffer = lboard_buffer; + runtime_lboard->invalid_memref = NULL; + memcpy(runtime_lboard->md5, md5, 16); + runtime_lboard->serialized_size = 0; + runtime_lboard->owns_memrefs = rc_runtime_allocated_memrefs(self); + + /* reset it, and return it */ + lboard->memrefs = NULL; + rc_reset_lboard(lboard); + return RC_OK; +} + +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, 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_format_lboard_value(char* buffer, int size, int value, int format) +{ + return rc_format_value(buffer, size, value, format); +} + +int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua_State* L, int funcs_idx) { + rc_richpresence_t* richpresence; + rc_runtime_richpresence_t* previous; + rc_runtime_richpresence_t** previous_ptr; + rc_parse_state_t parse; + unsigned char md5[16]; + int size; + + if (script == NULL) + return RC_MISSING_DISPLAY_STRING; + + rc_runtime_checksum(script, md5); + + /* look for existing match */ + previous_ptr = NULL; + previous = self->richpresence; + while (previous) { + if (previous && self->richpresence->richpresence && memcmp(self->richpresence->md5, md5, 16) == 0) { + /* unchanged. reset all of the conditions */ + rc_reset_richpresence(self->richpresence->richpresence); + + /* move to front of linked list*/ + if (previous_ptr) { + *previous_ptr = previous->previous; + if (!self->richpresence->owns_memrefs) { + free(self->richpresence->buffer); + previous->previous = self->richpresence->previous; + } + else { + previous->previous = self->richpresence; + } + + self->richpresence = previous; + } + + /* return success*/ + return RC_OK; + } + + previous_ptr = &previous->previous; + previous = previous->previous; + } + + /* no existing match found, parse script */ + size = rc_richpresence_size(script); + if (size < 0) + return size; + + /* if the previous script doesn't have any memrefs, free it */ + previous = self->richpresence; + if (previous) { + if (!previous->owns_memrefs) { + free(previous->buffer); + previous = previous->previous; + } + } + + /* allocate and process the new script */ + self->richpresence = (rc_runtime_richpresence_t*)malloc(sizeof(rc_runtime_richpresence_t)); + if (!self->richpresence) + return RC_OUT_OF_MEMORY; + + self->richpresence->previous = previous; + self->richpresence->owns_memrefs = 0; + memcpy(self->richpresence->md5, md5, sizeof(md5)); + self->richpresence->buffer = malloc(size); + + if (!self->richpresence->buffer) + return RC_OUT_OF_MEMORY; + + rc_init_parse_state(&parse, self->richpresence->buffer, L, funcs_idx); + self->richpresence->richpresence = richpresence = RC_ALLOC(rc_richpresence_t, &parse); + parse.first_memref = &self->memrefs; + parse.variables = &self->variables; + rc_parse_richpresence_internal(richpresence, script, &parse); + rc_destroy_parse_state(&parse); + + if (parse.offset < 0) { + free(self->richpresence->buffer); + free(self->richpresence); + self->richpresence = previous; + *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ + return parse.offset; + } + + self->richpresence->owns_memrefs = rc_runtime_allocated_memrefs(self); + + richpresence->memrefs = NULL; + richpresence->variables = NULL; + + if (!richpresence->first_display || !richpresence->first_display->display) { + /* non-existant rich presence */ + self->richpresence->richpresence = NULL; + } + else { + /* reset all of the conditions */ + rc_reset_richpresence(richpresence); + } + + return RC_OK; +} + +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, unsigned buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L) { + if (self->richpresence && self->richpresence->richpresence) + return rc_get_richpresence_display_string(self->richpresence->richpresence, buffer, buffersize, peek, peek_ud, L); + + *buffer = '\0'; + return 0; +} + +void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L) { + rc_runtime_event_t runtime_event; + int i; + + runtime_event.value = 0; + + rc_update_memref_values(self->memrefs, peek, ud); + rc_update_variables(self->variables, peek, ud, L); + + for (i = self->trigger_count - 1; i >= 0; --i) { + rc_trigger_t* trigger = self->triggers[i].trigger; + int old_state, new_state; + unsigned old_measured_value; + + if (!trigger) + continue; + + if (self->triggers[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED; + runtime_event.id = self->triggers[i].id; + runtime_event.value = self->triggers[i].invalid_memref->address; + + trigger->state = RC_TRIGGER_STATE_DISABLED; + self->triggers[i].invalid_memref = NULL; + + event_handler(&runtime_event); + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + continue; + } + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, peek, ud, L); + + /* trigger->state doesn't actually change to RESET, RESET just serves as a notification. + * handle the notification, then look at the actual state */ + if (new_state == RC_TRIGGER_STATE_RESET) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_RESET; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + + new_state = trigger->state; + } + + /* if the measured value changed and the achievement hasn't triggered, send a notification */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_target != 0 && trigger->measured_value <= trigger->measured_target && + new_state != RC_TRIGGER_STATE_TRIGGERED && + new_state != RC_TRIGGER_STATE_INACTIVE && new_state != RC_TRIGGER_STATE_WAITING) { + + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED; + runtime_event.id = self->triggers[i].id; + + if (trigger->measured_as_percent) { + /* if reporting measured value as a percentage, only send the notification if the percentage changes */ + unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent != new_percent) { + runtime_event.value = new_percent; + event_handler(&runtime_event); + } + } + else { + runtime_event.value = trigger->measured_value; + event_handler(&runtime_event); + } + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise an UNPRIMED event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + } + + /* raise events for each of the possible new states */ + switch (new_state) + { + case RC_TRIGGER_STATE_TRIGGERED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_PAUSED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_PRIMED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_ACTIVE: + /* only raise ACTIVATED event when transitioning from an inactive state. + * note that inactive in this case means active but cannot trigger. */ + if (old_state == RC_TRIGGER_STATE_WAITING || old_state == RC_TRIGGER_STATE_PAUSED) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + } + break; + } + } + + for (i = self->lboard_count - 1; i >= 0; --i) { + rc_lboard_t* lboard = self->lboards[i].lboard; + int lboard_state; + + if (!lboard) + continue; + + if (self->lboards[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_DISABLED; + runtime_event.id = self->lboards[i].id; + runtime_event.value = self->lboards[i].invalid_memref->address; + + lboard->state = RC_LBOARD_STATE_DISABLED; + self->lboards[i].invalid_memref = NULL; + + event_handler(&runtime_event); + continue; + } + + lboard_state = lboard->state; + switch (rc_evaluate_lboard(lboard, &runtime_event.value, peek, ud, L)) + { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (lboard_state != RC_LBOARD_STATE_STARTED) { + self->lboards[i].value = runtime_event.value; + + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_STARTED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + else if (runtime_event.value != self->lboards[i].value) { + self->lboards[i].value = runtime_event.value; + + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_UPDATED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (lboard_state != RC_LBOARD_STATE_CANCELED) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_CANCELED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (lboard_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_TRIGGERED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + } + } + + if (self->richpresence && self->richpresence->richpresence) + rc_update_richpresence(self->richpresence->richpresence, peek, ud, L); +} + +void rc_runtime_reset(rc_runtime_t* self) { + rc_value_t* variable; + 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 && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); + + for (variable = self->variables; variable; variable = variable->next) + rc_reset_value(variable); +} + +static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memref_t* memref) { + rc_condition_t* cond; + if (!condset) + return 0; + + for (cond = condset->conditions; cond; cond = cond->next) { + if (rc_operand_is_memref(&cond->operand1) && cond->operand1.value.memref == memref) + return 1; + if (rc_operand_is_memref(&cond->operand2) && cond->operand2.value.memref == memref) + return 1; + } + + return 0; +} + +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { + rc_condset_t* condset; + if (!value) + return 0; + + for (condset = value->conditions; condset; condset = condset->next) { + if (rc_condset_contains_memref(condset, memref)) + return 1; + } + + return 0; +} + +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { + rc_condset_t* condset; + if (!trigger) + return 0; + + if (rc_condset_contains_memref(trigger->requirement, memref)) + return 1; + + for (condset = trigger->alternative; condset; condset = condset->next) { + if (rc_condset_contains_memref(condset, memref)) + return 1; + } + + return 0; +} + +static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref) { + unsigned i; + + /* disable any achievements dependent on the address */ + for (i = 0; i < self->trigger_count; ++i) { + if (!self->triggers[i].invalid_memref && rc_trigger_contains_memref(self->triggers[i].trigger, memref)) + self->triggers[i].invalid_memref = memref; + } + + /* disable any leaderboards dependent on the address */ + for (i = 0; i < self->lboard_count; ++i) { + if (!self->lboards[i].invalid_memref) { + rc_lboard_t* lboard = self->lboards[i].lboard; + if (lboard) { + if (rc_trigger_contains_memref(&lboard->start, memref)) { + lboard->start.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_trigger_contains_memref(&lboard->cancel, memref)) { + lboard->cancel.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_trigger_contains_memref(&lboard->submit, memref)) { + lboard->submit.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_value_contains_memref(&lboard->value, memref)) + self->lboards[i].invalid_memref = memref; + } + } + } +} + +void rc_runtime_invalidate_address(rc_runtime_t* self, unsigned address) { + rc_memref_t** last_memref = &self->memrefs; + rc_memref_t* memref = self->memrefs; + + while (memref) { + if (memref->address == address && !memref->value.is_indirect) { + /* remove the invalid memref from the chain so we don't try to evaluate it in the future. + * it's still there, so anything referencing it will continue to fetch 0. + */ + *last_memref = memref->next; + + rc_runtime_invalidate_memref(self, memref); + break; + } + + last_memref = &memref->next; + memref = *last_memref; + } +} + +void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, + rc_runtime_validate_address_t validate_handler) { + rc_memref_t** last_memref = &self->memrefs; + rc_memref_t* memref = self->memrefs; + int num_invalid = 0; + + while (memref) { + if (!memref->value.is_indirect && !validate_handler(memref->address)) { + /* remove the invalid memref from the chain so we don't try to evaluate it in the future. + * it's still there, so anything referencing it will continue to fetch 0. + */ + *last_memref = memref->next; + + rc_runtime_invalidate_memref(self, memref); + ++num_invalid; + } + else { + last_memref = &memref->next; + } + + memref = *last_memref; + } + + if (num_invalid) { + rc_runtime_event_t runtime_event; + int i; + + for (i = self->trigger_count - 1; i >= 0; --i) { + rc_trigger_t* trigger = self->triggers[i].trigger; + if (trigger && self->triggers[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED; + runtime_event.id = self->triggers[i].id; + runtime_event.value = self->triggers[i].invalid_memref->address; + + trigger->state = RC_TRIGGER_STATE_DISABLED; + self->triggers[i].invalid_memref = NULL; + + event_handler(&runtime_event); + } + } + + for (i = self->lboard_count - 1; i >= 0; --i) { + rc_lboard_t* lboard = self->lboards[i].lboard; + if (lboard && self->lboards[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_DISABLED; + runtime_event.id = self->lboards[i].id; + runtime_event.value = self->lboards[i].invalid_memref->address; + + lboard->state = RC_LBOARD_STATE_DISABLED; + self->lboards[i].invalid_memref = NULL; + + event_handler(&runtime_event); + } + } + } +} diff --git a/src/rcheevos/src/rcheevos/runtime_progress.c b/src/rcheevos/src/rcheevos/runtime_progress.c new file mode 100644 index 000000000..32353faab --- /dev/null +++ b/src/rcheevos/src/rcheevos/runtime_progress.c @@ -0,0 +1,880 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "../rhash/md5.h" + +#include +#include + +#define RC_RUNTIME_MARKER 0x0A504152 /* RAP\n */ + +#define RC_RUNTIME_CHUNK_MEMREFS 0x4645524D /* MREF */ +#define RC_RUNTIME_CHUNK_VARIABLES 0x53524156 /* VARS */ +#define RC_RUNTIME_CHUNK_ACHIEVEMENT 0x56484341 /* ACHV */ +#define RC_RUNTIME_CHUNK_LEADERBOARD 0x4452424C /* LBRD */ +#define RC_RUNTIME_CHUNK_RICHPRESENCE 0x48434952 /* RICH */ + +#define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ + +typedef struct rc_runtime_progress_t { + const rc_runtime_t* runtime; + + 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_VAR_FLAG_HAS_COND_DATA 0x01000000 + +#define RC_COND_FLAG_IS_TRUE 0x00000001 +#define RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF 0x00010000 +#define RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME 0x00020000 +#define RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF 0x00100000 +#define RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME 0x00200000 + +static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, 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, const rc_runtime_t* runtime, lua_State* L) +{ + memset(progress, 0, sizeof(rc_runtime_progress_t)); + progress->runtime = runtime; + progress->L = L; +} + +static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) +{ + rc_memref_t* memref = progress->runtime->memrefs; + 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 = (char)rc_runtime_progress_read_uint(progress); + + cond = condset->conditions; + while (cond) { + cond->current_hits = rc_runtime_progress_read_uint(progress); + flags = rc_runtime_progress_read_uint(progress); + + cond->is_true = (flags & RC_COND_FLAG_IS_TRUE) ? 1 : 0; + + if (flags & RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF) { + if (!rc_operand_is_memref(&cond->operand1)) /* this should never happen, but better safe than sorry */ + return RC_INVALID_STATE; + + cond->operand1.value.memref->value.value = rc_runtime_progress_read_uint(progress); + cond->operand1.value.memref->value.prior = rc_runtime_progress_read_uint(progress); + cond->operand1.value.memref->value.changed = (flags & RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME) ? 1 : 0; + } + + if (flags & RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF) { + if (!rc_operand_is_memref(&cond->operand2)) /* this should never happen, but better safe than sorry */ + return RC_INVALID_STATE; + + cond->operand2.value.memref->value.value = rc_runtime_progress_read_uint(progress); + cond->operand2.value.memref->value.prior = rc_runtime_progress_read_uint(progress); + cond->operand2.value.memref->value.changed = (flags & RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME) ? 1 : 0; + } + + cond = cond->next; + } + + return RC_OK; +} + +static unsigned rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) +{ + const rc_condition_t* condition; + + /* predetermined presence of pause flag or indirect memrefs - must serialize */ + if (conditions->has_pause || conditions->has_indirect_memrefs) + return RC_VAR_FLAG_HAS_COND_DATA; + + /* if any conditions has required hits, must serialize */ + /* ASSERT: Measured with comparison and no explicit target will set hit target to 0xFFFFFFFF */ + for (condition = conditions->conditions; condition; condition = condition->next) { + if (condition->required_hits > 0) + return RC_VAR_FLAG_HAS_COND_DATA; + } + + /* can safely be reset without affecting behavior */ + return 0; +} + +static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, const rc_value_t* variable) +{ + unsigned flags; + + flags = rc_runtime_progress_should_serialize_variable_condset(variable->conditions); + if (variable->value.changed) + flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + + rc_runtime_progress_write_uint(progress, flags); + rc_runtime_progress_write_uint(progress, variable->value.value); + rc_runtime_progress_write_uint(progress, variable->value.prior); + + if (flags & RC_VAR_FLAG_HAS_COND_DATA) { + int result = rc_runtime_progress_write_condset(progress, variable->conditions); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) +{ + unsigned count = 0; + const rc_value_t* variable; + + for (variable = progress->runtime->variables; variable; variable = variable->next) + ++count; + if (count == 0) + return RC_OK; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_VARIABLES); + rc_runtime_progress_write_uint(progress, count); + + for (variable = progress->runtime->variables; variable; variable = variable->next) + { + unsigned djb2 = rc_djb2(variable->name); + rc_runtime_progress_write_uint(progress, djb2); + + rc_runtime_progress_write_variable(progress, variable); + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_variable(rc_runtime_progress_t* progress, rc_value_t* variable) +{ + unsigned flags = rc_runtime_progress_read_uint(progress); + variable->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + variable->value.value = rc_runtime_progress_read_uint(progress); + variable->value.prior = rc_runtime_progress_read_uint(progress); + + if (flags & RC_VAR_FLAG_HAS_COND_DATA) { + int result = rc_runtime_progress_read_condset(progress, variable->conditions); + if (result != RC_OK) + return result; + } + else { + rc_reset_condset(variable->conditions); + } + + return RC_OK; +} + +static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) +{ + struct rc_pending_value_t + { + rc_value_t* variable; + unsigned djb2; + }; + struct rc_pending_value_t local_pending_variables[32]; + struct rc_pending_value_t* pending_variables; + rc_value_t* variable; + unsigned count, serialized_count; + int result; + unsigned i; + + serialized_count = rc_runtime_progress_read_uint(progress); + if (serialized_count == 0) + return RC_OK; + + count = 0; + for (variable = progress->runtime->variables; variable; variable = variable->next) + ++count; + + if (count == 0) + return RC_OK; + + if (count <= sizeof(local_pending_variables) / sizeof(local_pending_variables[0])) { + pending_variables = local_pending_variables; + } + else { + pending_variables = (struct rc_pending_value_t*)malloc(count * sizeof(struct rc_pending_value_t)); + if (pending_variables == NULL) + return RC_OUT_OF_MEMORY; + } + + count = 0; + for (variable = progress->runtime->variables; variable; variable = variable->next) { + pending_variables[count].variable = variable; + pending_variables[count].djb2 = rc_djb2(variable->name); + ++count; + } + + result = RC_OK; + for (; serialized_count > 0 && result == RC_OK; --serialized_count) { + unsigned djb2 = rc_runtime_progress_read_uint(progress); + for (i = 0; i < count; ++i) { + if (pending_variables[i].djb2 == djb2) { + variable = pending_variables[i].variable; + result = rc_runtime_progress_read_variable(progress, variable); + if (result == RC_OK) { + if (i < count - 1) + memcpy(&pending_variables[i], &pending_variables[count - 1], sizeof(struct rc_pending_value_t)); + count--; + } + break; + } + } + } + + while (count > 0) + rc_reset_value(pending_variables[--count].variable); + + if (pending_variables != local_pending_variables) + free(pending_variables); + + return result; +} + +static int rc_runtime_progress_write_trigger(rc_runtime_progress_t* progress, const rc_trigger_t* trigger) +{ + rc_condset_t* condset; + int result; + + rc_runtime_progress_write_uint(progress, trigger->state); + rc_runtime_progress_write_uint(progress, trigger->measured_value); + + if (trigger->requirement) { + result = rc_runtime_progress_write_condset(progress, trigger->requirement); + if (result != RC_OK) + return result; + } + + condset = trigger->alternative; + while (condset) { + result = rc_runtime_progress_write_condset(progress, condset); + if (result != RC_OK) + return result; + + condset = condset->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_trigger(rc_runtime_progress_t* progress, rc_trigger_t* trigger) +{ + rc_condset_t* condset; + int result; + + trigger->state = (char)rc_runtime_progress_read_uint(progress); + trigger->measured_value = rc_runtime_progress_read_uint(progress); + + if (trigger->requirement) { + result = rc_runtime_progress_read_condset(progress, trigger->requirement); + if (result != RC_OK) + return result; + } + + condset = trigger->alternative; + while (condset) { + result = rc_runtime_progress_read_condset(progress, condset); + if (result != RC_OK) + return result; + + condset = condset->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progress) +{ + 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; + + /* don't store state for inactive or triggered achievements */ + if (!rc_trigger_state_active(runtime_trigger->trigger->state)) + continue; + + if (!progress->buffer) { + if (runtime_trigger->serialized_size) { + progress->offset += runtime_trigger->serialized_size; + continue; + } + + offset = progress->offset; + } + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_ACHIEVEMENT); + rc_runtime_progress_write_uint(progress, runtime_trigger->id); + rc_runtime_progress_write_md5(progress, runtime_trigger->md5); + + result = rc_runtime_progress_write_trigger(progress, runtime_trigger->trigger); + if (result != RC_OK) + return result; + + rc_runtime_progress_end_chunk(progress); + + if (!progress->buffer) + runtime_trigger->serialized_size = progress->offset - offset; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) +{ + 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_write_leaderboards(rc_runtime_progress_t* progress) +{ + unsigned i; + unsigned flags; + int offset = 0; + int result; + + for (i = 0; i < progress->runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &progress->runtime->lboards[i]; + if (!runtime_lboard->lboard) + continue; + + /* don't store state for inactive leaderboards */ + if (!rc_lboard_state_active(runtime_lboard->lboard->state)) + continue; + + if (!progress->buffer) { + if (runtime_lboard->serialized_size) { + progress->offset += runtime_lboard->serialized_size; + continue; + } + + offset = progress->offset; + } + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_LEADERBOARD); + rc_runtime_progress_write_uint(progress, runtime_lboard->id); + rc_runtime_progress_write_md5(progress, runtime_lboard->md5); + + flags = runtime_lboard->lboard->state; + rc_runtime_progress_write_uint(progress, flags); + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->start); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->submit); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->cancel); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_variable(progress, &runtime_lboard->lboard->value); + if (result != RC_OK) + return result; + + rc_runtime_progress_end_chunk(progress); + + if (!progress->buffer) + runtime_lboard->serialized_size = progress->offset - offset; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) +{ + unsigned id = rc_runtime_progress_read_uint(progress); + unsigned i; + int result; + + for (i = 0; i < progress->runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &progress->runtime->lboards[i]; + if (runtime_lboard->id == id && runtime_lboard->lboard != NULL) { + /* ignore triggered and waiting achievements */ + if (runtime_lboard->lboard->state == RC_TRIGGER_STATE_UNUPDATED) { + /* only update state if definition hasn't changed (md5 matches) */ + if (rc_runtime_progress_match_md5(progress, runtime_lboard->md5)) { + unsigned flags = rc_runtime_progress_read_uint(progress); + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->start); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->submit); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->cancel); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_variable(progress, &runtime_lboard->lboard->value); + if (result != RC_OK) + return result; + + runtime_lboard->lboard->state = (char)(flags & 0x7F); + } + break; + } + } + } + + return RC_OK; +} + +static int rc_runtime_progress_write_rich_presence(rc_runtime_progress_t* progress) +{ + const rc_richpresence_display_t* display; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + /* if there are no conditional display strings, there's nothing to capture */ + display = progress->runtime->richpresence->richpresence->first_display; + if (!display->next) + return RC_OK; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_RICHPRESENCE); + rc_runtime_progress_write_md5(progress, progress->runtime->richpresence->md5); + + for (; display->next; display = display->next) { + result = rc_runtime_progress_write_trigger(progress, &display->trigger); + if (result != RC_OK) + return result; + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_rich_presence(rc_runtime_progress_t* progress) +{ + rc_richpresence_display_t* display; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + if (!rc_runtime_progress_match_md5(progress, progress->runtime->richpresence->md5)) { + rc_reset_richpresence(progress->runtime->richpresence->richpresence); + return RC_OK; + } + + display = progress->runtime->richpresence->richpresence->first_display; + for (; display->next; display = display->next) { + result = rc_runtime_progress_read_trigger(progress, &display->trigger); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +static int rc_runtime_progress_serialize_internal(rc_runtime_progress_t* progress) +{ + md5_state_t state; + 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_variables(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_achievements(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_leaderboards(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_rich_presence(progress)) != RC_OK) + return result; + + rc_runtime_progress_write_uint(progress, RC_RUNTIME_CHUNK_DONE); + rc_runtime_progress_write_uint(progress, 16); + + if (progress->buffer) { + md5_init(&state); + md5_append(&state, progress->buffer, progress->offset); + md5_finish(&state, md5); + } + + rc_runtime_progress_write_md5(progress, md5); + + return RC_OK; +} + +int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) +{ + rc_runtime_progress_t progress; + int result; + + rc_runtime_progress_init(&progress, runtime, L); + + result = rc_runtime_progress_serialize_internal(&progress); + if (result != RC_OK) + return result; + + return progress.offset; +} + +int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L) +{ + rc_runtime_progress_t progress; + + if (!buffer) + return RC_INVALID_STATE; + + rc_runtime_progress_init(&progress, runtime, L); + progress.buffer = (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 seen_rich_presence = 0; + int result = RC_OK; + + if (!serialized) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + + rc_runtime_progress_init(&progress, runtime, L); + progress.buffer = (unsigned char*)serialized; + + if (rc_runtime_progress_read_uint(&progress) != RC_RUNTIME_MARKER) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + + for (i = 0; i < runtime->trigger_count; ++i) { + rc_runtime_trigger_t* runtime_trigger = &runtime->triggers[i]; + if (runtime_trigger->trigger) { + /* don't update state for inactive or triggered achievements */ + if (rc_trigger_state_active(runtime_trigger->trigger->state)) { + /* mark active achievements as unupdated. anything that's still unupdated + * after deserializing the progress will be reset to waiting */ + runtime_trigger->trigger->state = RC_TRIGGER_STATE_UNUPDATED; + } + } + } + + for (i = 0; i < runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &runtime->lboards[i]; + if (runtime_lboard->lboard) { + /* don't update state for inactive or triggered achievements */ + if (rc_lboard_state_active(runtime_lboard->lboard->state)) { + /* mark active achievements as unupdated. anything that's still unupdated + * after deserializing the progress will be reset to waiting */ + runtime_lboard->lboard->state = RC_TRIGGER_STATE_UNUPDATED; + } + } + } + + do { + chunk_id = rc_runtime_progress_read_uint(&progress); + chunk_size = rc_runtime_progress_read_uint(&progress); + next_chunk_offset = progress.offset + chunk_size; + + switch (chunk_id) + { + case RC_RUNTIME_CHUNK_MEMREFS: + result = rc_runtime_progress_read_memrefs(&progress); + break; + + case RC_RUNTIME_CHUNK_VARIABLES: + result = rc_runtime_progress_read_variables(&progress); + break; + + case RC_RUNTIME_CHUNK_ACHIEVEMENT: + result = rc_runtime_progress_read_achievement(&progress); + break; + + case RC_RUNTIME_CHUNK_LEADERBOARD: + result = rc_runtime_progress_read_leaderboard(&progress); + break; + + case RC_RUNTIME_CHUNK_RICHPRESENCE: + seen_rich_presence = 1; + result = rc_runtime_progress_read_rich_presence(&progress); + break; + + case RC_RUNTIME_CHUNK_DONE: + md5_init(&state); + md5_append(&state, progress.buffer, progress.offset); + md5_finish(&state, md5); + if (!rc_runtime_progress_match_md5(&progress, md5)) + result = RC_INVALID_STATE; + break; + + default: + if (chunk_size & 0xFFFF0000) + result = RC_INVALID_STATE; /* assume unknown chunk > 64KB is invalid */ + break; + } + + progress.offset = next_chunk_offset; + } while (result == RC_OK && chunk_id != RC_RUNTIME_CHUNK_DONE); + + if (result != RC_OK) { + rc_runtime_reset(runtime); + } + else { + for (i = 0; i < runtime->trigger_count; ++i) { + rc_trigger_t* trigger = runtime->triggers[i].trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_UNUPDATED) + rc_reset_trigger(trigger); + } + + for (i = 0; i < runtime->lboard_count; ++i) { + rc_lboard_t* lboard = runtime->lboards[i].lboard; + if (lboard && lboard->state == RC_TRIGGER_STATE_UNUPDATED) + rc_reset_lboard(lboard); + } + + if (!seen_rich_presence && runtime->richpresence && runtime->richpresence->richpresence) + rc_reset_richpresence(runtime->richpresence->richpresence); + } + + return result; +} diff --git a/src/rcheevos/src/rcheevos/trigger.c b/src/rcheevos/src/rcheevos/trigger.c new file mode 100644 index 000000000..6061ab8ee --- /dev/null +++ b/src/rcheevos/src/rcheevos/trigger.c @@ -0,0 +1,291 @@ +#include "rc_internal.h" + +#include +#include /* memset */ + +void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_t** next; + const char* aux; + + aux = *memaddr; + next = &self->alternative; + + /* reset in case multiple triggers are parsed by the same parse_state */ + parse->measured_target = 0; + parse->has_required_hits = 0; + parse->measured_as_percent = 0; + + if (*aux == 's' || *aux == 'S') { + self->requirement = 0; + } + else { + self->requirement = rc_parse_condset(&aux, parse, 0); + + if (parse->offset < 0) { + return; + } + + self->requirement->next = 0; + } + + while (*aux == 's' || *aux == 'S') { + aux++; + *next = rc_parse_condset(&aux, parse, 0); + + if (parse->offset < 0) { + return; + } + + next = &(*next)->next; + } + + *next = 0; + *memaddr = aux; + + self->measured_target = parse->measured_target; + self->measured_value = parse->measured_target ? RC_MEASURED_UNKNOWN : 0; + self->measured_as_percent = parse->measured_as_percent; + self->state = RC_TRIGGER_STATE_WAITING; + self->has_hits = 0; + self->has_required_hits = parse->has_required_hits; +} + +int rc_trigger_size(const char* memaddr) { + rc_trigger_t* self; + rc_parse_state_t parse; + rc_memref_t* memrefs; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + + self = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { + rc_trigger_t* self; + rc_parse_state_t parse; + + if (!buffer || !memaddr) + return NULL; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_trigger_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + + rc_parse_trigger_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : NULL; +} + +int rc_trigger_state_active(int state) +{ + switch (state) + { + case RC_TRIGGER_STATE_DISABLED: + case RC_TRIGGER_STATE_INACTIVE: + case RC_TRIGGER_STATE_TRIGGERED: + return 0; + + default: + return 1; + } +} + +static int rc_condset_is_measured_from_hitcount(const rc_condset_t* condset, unsigned measured_value) +{ + const rc_condition_t* condition; + for (condition = condset->conditions; condition; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED && condition->required_hits && + condition->current_hits == measured_value) { + return 1; + } + } + + return 0; +} + +static void rc_reset_trigger_hitcounts(rc_trigger_t* self) { + rc_condset_t* condset; + + if (self->requirement) { + rc_reset_condset(self->requirement); + } + + condset = self->alternative; + + while (condset) { + rc_reset_condset(condset); + condset = condset->next; + } +} + +int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + int ret; + char is_paused; + char is_primed; + + switch (self->state) + { + case RC_TRIGGER_STATE_TRIGGERED: + /* previously triggered. do nothing - return INACTIVE so caller doesn't think it triggered again */ + return RC_TRIGGER_STATE_INACTIVE; + + case RC_TRIGGER_STATE_DISABLED: + /* unsupported. do nothing - return INACTIVE */ + return RC_TRIGGER_STATE_INACTIVE; + + case RC_TRIGGER_STATE_INACTIVE: + /* not yet active. update the memrefs so deltas are correct when it becomes active, then return INACTIVE */ + rc_update_memref_values(self->memrefs, peek, ud); + return RC_TRIGGER_STATE_INACTIVE; + + default: + break; + } + + /* update the memory references */ + rc_update_memref_values(self->memrefs, peek, ud); + + /* process the trigger */ + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = ud; + eval_state.L = L; + + if (self->requirement != NULL) { + ret = rc_test_condset(self->requirement, &eval_state); + is_paused = self->requirement->is_paused; + is_primed = eval_state.primed; + } else { + ret = 1; + is_paused = 0; + is_primed = 1; + } + + condset = self->alternative; + if (condset) { + int sub = 0; + char sub_paused = 1; + char sub_primed = 0; + + do { + sub |= rc_test_condset(condset, &eval_state); + sub_paused &= condset->is_paused; + sub_primed |= eval_state.primed; + + condset = condset->next; + } while (condset); + + /* to trigger, the core must be true and at least one alt must be true */ + ret &= sub; + is_primed &= sub_primed; + + /* if the core is not paused, all alts must be paused to count as a paused trigger */ + is_paused |= sub_paused; + } + + /* if paused, the measured value may not be captured, keep the old value */ + if (!is_paused) { + rc_typed_value_convert(&eval_state.measured_value, RC_VALUE_TYPE_UNSIGNED); + self->measured_value = eval_state.measured_value.value.u32; + } + + /* if any ResetIf condition was true, reset the hit counts */ + if (eval_state.was_reset) { + /* if the measured value came from a hit count, reset it. do this before calling + * rc_reset_trigger_hitcounts in case we need to call rc_condset_is_measured_from_hitcount */ + if (eval_state.measured_from_hits) { + self->measured_value = 0; + } + else if (is_paused && self->measured_value) { + /* if the measured value is in a paused group, measured_from_hits won't have been set. + * attempt to determine if it should have been */ + if (self->requirement && self->requirement->is_paused && + rc_condset_is_measured_from_hitcount(self->requirement, self->measured_value)) { + self->measured_value = 0; + } + else { + for (condset = self->alternative; condset; condset = condset->next) { + if (condset->is_paused && rc_condset_is_measured_from_hitcount(condset, self->measured_value)) { + self->measured_value = 0; + break; + } + } + } + } + + rc_reset_trigger_hitcounts(self); + + /* if there were hit counts to clear, return RESET, but don't change the state */ + if (self->has_hits) { + self->has_hits = 0; + + /* cannot be PRIMED while ResetIf is true */ + if (self->state == RC_TRIGGER_STATE_PRIMED) + self->state = RC_TRIGGER_STATE_ACTIVE; + + return RC_TRIGGER_STATE_RESET; + } + + /* any hits that were tallied were just reset */ + eval_state.has_hits = 0; + is_primed = 0; + } + else if (ret) { + /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ + if (self->state == RC_TRIGGER_STATE_WAITING) { + rc_reset_trigger(self); + self->has_hits = 0; + return RC_TRIGGER_STATE_WAITING; + } + + /* trigger was triggered */ + self->state = RC_TRIGGER_STATE_TRIGGERED; + return RC_TRIGGER_STATE_TRIGGERED; + } + + /* did not trigger this frame - update the information we'll need for next time */ + self->has_hits = eval_state.has_hits; + + if (is_paused) { + self->state = RC_TRIGGER_STATE_PAUSED; + } + else if (is_primed) { + self->state = RC_TRIGGER_STATE_PRIMED; + } + else { + self->state = RC_TRIGGER_STATE_ACTIVE; + } + + /* if an individual condition was reset, notify the caller */ + if (eval_state.was_cond_reset) + return RC_TRIGGER_STATE_RESET; + + /* otherwise, just return the current state */ + return self->state; +} + +int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) { + /* for backwards compatibilty, rc_test_trigger always assumes the achievement is active */ + self->state = RC_TRIGGER_STATE_ACTIVE; + + return (rc_evaluate_trigger(self, peek, ud, L) == RC_TRIGGER_STATE_TRIGGERED); +} + +void rc_reset_trigger(rc_trigger_t* self) { + rc_reset_trigger_hitcounts(self); + + self->state = RC_TRIGGER_STATE_WAITING; + + if (self->measured_target) + self->measured_value = RC_MEASURED_UNKNOWN; + + self->has_hits = 0; +} diff --git a/src/rcheevos/src/rcheevos/value.c b/src/rcheevos/src/rcheevos/value.c new file mode 100644 index 000000000..54784caab --- /dev/null +++ b/src/rcheevos/src/rcheevos/value.c @@ -0,0 +1,719 @@ +#include "rc_internal.h" + +#include /* memset */ +#include /* isdigit */ +#include /* FLT_EPSILON */ + +static void rc_parse_cond_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_t** next_clause; + + next_clause = &self->conditions; + + do + { + parse->measured_target = 0; /* passing is_value=1 should prevent any conflicts, but clear it out anyway */ + *next_clause = rc_parse_condset(memaddr, parse, 1); + if (parse->offset < 0) { + return; + } + + if (**memaddr == 'S' || **memaddr == 's') { + /* alt groups not supported */ + parse->offset = RC_INVALID_VALUE_FLAG; + } + else if (parse->measured_target == 0) { + parse->offset = RC_MISSING_VALUE_MEASURED; + } + else if (**memaddr == '$') { + /* maximum of */ + ++(*memaddr); + next_clause = &(*next_clause)->next; + continue; + } + + break; + } while (1); + + (*next_clause)->next = 0; +} + +void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condition_t** next; + rc_condset_t** next_clause; + rc_condition_t* cond; + char buffer[64] = "A:"; + const char* buffer_ptr; + char* ptr; + + /* convert legacy format into condset */ + self->conditions = RC_ALLOC(rc_condset_t, parse); + memset(self->conditions, 0, sizeof(rc_condset_t)); + + next = &self->conditions->conditions; + next_clause = &self->conditions->next; + + for (;; ++(*memaddr)) { + buffer[0] = 'A'; /* reset to AddSource */ + ptr = &buffer[2]; + + /* extract the next clause */ + for (;; ++(*memaddr)) { + switch (**memaddr) { + case '_': /* add next */ + case '$': /* maximum of */ + case '\0': /* end of string */ + case ':': /* end of leaderboard clause */ + case ')': /* end of rich presence macro */ + *ptr = '\0'; + break; + + case '*': + *ptr++ = '*'; + + buffer_ptr = *memaddr + 1; + if (*buffer_ptr == '-') { + buffer[0] = 'B'; /* change to SubSource */ + ++(*memaddr); /* don't copy sign */ + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + else if (*buffer_ptr == '+') { + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + + /* if it looks like a floating point number, add the 'f' prefix */ + while (isdigit((unsigned char)*buffer_ptr)) + ++buffer_ptr; + if (*buffer_ptr == '.') + *ptr++ = 'f'; + continue; + + default: + *ptr++ = **memaddr; + continue; + } + + break; + } + + /* process the clause */ + buffer_ptr = buffer; + cond = rc_parse_condition(&buffer_ptr, parse, 0); + if (parse->offset < 0) + return; + + if (*buffer_ptr) { + /* whatever we copied as a single condition was not fully consumed */ + parse->offset = RC_INVALID_COMPARISON; + return; + } + + switch (cond->oper) { + case RC_OPERATOR_MULT: + case RC_OPERATOR_DIV: + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_NONE: + break; + + default: + parse->offset = RC_INVALID_OPERATOR; + return; + } + + *next = cond; + + if (**memaddr == '_') { + /* add next */ + next = &cond->next; + continue; + } + + if (cond->type == RC_CONDITION_SUB_SOURCE) { + /* cannot change SubSource to Measured. add a dummy condition */ + next = &cond->next; + buffer_ptr = "A:0"; + cond = rc_parse_condition(&buffer_ptr, parse, 0); + *next = cond; + } + + /* convert final AddSource condition to Measured */ + cond->type = RC_CONDITION_MEASURED; + cond->next = 0; + + if (**memaddr != '$') { + /* end of valid string */ + *next_clause = 0; + break; + } + + /* max of ($), start a new clause */ + *next_clause = RC_ALLOC(rc_condset_t, parse); + + if (parse->buffer) /* don't clear in sizing mode or pointer will break */ + memset(*next_clause, 0, sizeof(rc_condset_t)); + + next = &(*next_clause)->conditions; + next_clause = &(*next_clause)->next; + } +} + +void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + /* if it starts with a condition flag (M: A: B: C:), parse the conditions */ + if ((*memaddr)[1] == ':') { + rc_parse_cond_value(self, memaddr, parse); + } + else { + rc_parse_legacy_value(self, memaddr, parse); + } + + self->name = "(unnamed)"; + self->value.value = self->value.prior = 0; + self->value.changed = 0; + self->next = 0; +} + +int rc_value_size(const char* memaddr) { + rc_value_t* self; + rc_parse_state_t parse; + rc_memref_t* first_memref; + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &first_memref); + + self = RC_ALLOC(rc_value_t, &parse); + rc_parse_value_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return parse.offset; +} + +rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { + rc_value_t* self; + rc_parse_state_t parse; + + if (!buffer || !memaddr) + return NULL; + + rc_init_parse_state(&parse, buffer, L, funcs_ndx); + + self = RC_ALLOC(rc_value_t, &parse); + rc_init_parse_state_memrefs(&parse, &self->memrefs); + + rc_parse_value_internal(self, &memaddr, &parse); + + rc_destroy_parse_state(&parse); + return (parse.offset >= 0) ? self : NULL; +} + +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + int valid = 0; + + rc_update_memref_values(self->memrefs, peek, ud); + + value->value.i32 = 0; + value->type = RC_VALUE_TYPE_SIGNED; + + for (condset = self->conditions; condset != NULL; condset = condset->next) { + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = ud; + eval_state.L = L; + + rc_test_condset(condset, &eval_state); + + if (condset->is_paused) + continue; + + if (eval_state.was_reset) { + /* if any ResetIf condition was true, reset the hit counts + * NOTE: ResetIf only affects the current condset when used in values! + */ + rc_reset_condset(condset); + + /* if the measured value came from a hit count, reset it too */ + if (eval_state.measured_from_hits) { + eval_state.measured_value.value.u32 = 0; + eval_state.measured_value.type = RC_VALUE_TYPE_UNSIGNED; + } + } + + if (!valid) { + /* capture the first valid measurement */ + memcpy(value, &eval_state.measured_value, sizeof(*value)); + valid = 1; + } + else { + /* multiple condsets are currently only used for the MAX_OF operation. + * only keep the condset's value if it's higher than the current highest value. + */ + if (rc_typed_value_compare(&eval_state.measured_value, value, RC_OPERATOR_GT)) + memcpy(value, &eval_state.measured_value, sizeof(*value)); + } + } + + return valid; +} + +int rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { + rc_typed_value_t result; + int valid = rc_evaluate_value_typed(self, &result, peek, ud, L); + + if (valid) { + /* if not paused, store the value so that it's available when paused. */ + rc_typed_value_convert(&result, RC_VALUE_TYPE_UNSIGNED); + rc_update_memref_value(&self->value, result.value.u32); + } + else { + /* when paused, the Measured value will not be captured, use the last captured value. */ + result.value.u32 = self->value.value; + result.type = RC_VALUE_TYPE_UNSIGNED; + } + + rc_typed_value_convert(&result, RC_VALUE_TYPE_SIGNED); + return result.value.i32; +} + +void rc_reset_value(rc_value_t* self) { + rc_condset_t* condset = self->conditions; + while (condset != NULL) { + rc_reset_condset(condset); + condset = condset->next; + } + + self->value.value = self->value.prior = 0; + self->value.changed = 0; +} + +int rc_value_from_hits(rc_value_t* self) +{ + rc_condset_t* condset = self->conditions; + for (; condset != NULL; condset = condset->next) { + rc_condition_t* condition = condset->conditions; + for (; condition != NULL; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) + return (condition->required_hits != 0); + } + } + + return 0; +} + +void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { + parse->variables = variables; + *variables = 0; +} + +rc_value_t* rc_alloc_helper_variable(const char* memaddr, 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) { + rc_typed_value_t result; + + while (variable) { + if (rc_evaluate_value_typed(variable, &result, peek, ud, L)) { + /* store the raw bytes and type to be restored by rc_typed_value_from_memref_value */ + rc_update_memref_value(&variable->value, result.value.u32); + variable->value.type = result.type; + } + + variable = variable->next; + } +} + +void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref) { + value->value.u32 = memref->value; + + if (memref->size == RC_MEMSIZE_VARIABLE) { + /* a variable can be any of the supported types, but the raw data was copied into u32 */ + value->type = memref->type; + } + else { + /* not a variable, only u32 is supported */ + value->type = RC_VALUE_TYPE_UNSIGNED; + } +} + +void rc_typed_value_convert(rc_typed_value_t* value, char new_type) { + switch (new_type) { + case RC_VALUE_TYPE_UNSIGNED: + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: + return; + case RC_VALUE_TYPE_SIGNED: + value->value.u32 = (unsigned)value->value.i32; + break; + case RC_VALUE_TYPE_FLOAT: + value->value.u32 = (unsigned)value->value.f32; + break; + default: + value->value.u32 = 0; + break; + } + break; + + case RC_VALUE_TYPE_SIGNED: + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: + return; + case RC_VALUE_TYPE_UNSIGNED: + value->value.i32 = (int)value->value.u32; + break; + case RC_VALUE_TYPE_FLOAT: + value->value.i32 = (int)value->value.f32; + break; + default: + value->value.i32 = 0; + break; + } + break; + + case RC_VALUE_TYPE_FLOAT: + switch (value->type) { + case RC_VALUE_TYPE_FLOAT: + return; + case RC_VALUE_TYPE_UNSIGNED: + value->value.f32 = (float)value->value.u32; + break; + case RC_VALUE_TYPE_SIGNED: + value->value.f32 = (float)value->value.i32; + break; + default: + value->value.f32 = 0.0; + break; + } + break; + + default: + break; + } + + value->type = new_type; +} + +static rc_typed_value_t* rc_typed_value_convert_into(rc_typed_value_t* dest, const rc_typed_value_t* source, char new_type) { + memcpy(dest, source, sizeof(rc_typed_value_t)); + rc_typed_value_convert(dest, new_type); + return dest; +} + +void rc_typed_value_negate(rc_typed_value_t* value) { + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + rc_typed_value_convert(value, RC_VALUE_TYPE_SIGNED); + /* fallthrough to RC_VALUE_TYPE_SIGNED */ + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = -(value->value.i32); + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = -(value->value.f32); + break; + + default: + break; + } +} + +void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + if (amount->type != value->type && value->type != RC_VALUE_TYPE_NONE) + amount = rc_typed_value_convert_into(&converted, amount, value->type); + + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + value->value.u32 += amount->value.u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 += amount->value.i32; + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 += amount->value.f32; + break; + + case RC_VALUE_TYPE_NONE: + memcpy(value, amount, sizeof(rc_typed_value_t)); + break; + + default: + break; + } +} + +void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + /* the c standard for unsigned multiplication is well defined as non-overflowing truncation + * to the type's size. this allows negative multiplication through twos-complements. i.e. + * 1 * -1 (0xFFFFFFFF) = 0xFFFFFFFF = -1 + * 3 * -2 (0xFFFFFFFE) = 0x2FFFFFFFA & 0xFFFFFFFF = 0xFFFFFFFA = -6 + * 10 * -5 (0xFFFFFFFB) = 0x9FFFFFFCE & 0xFFFFFFFF = 0xFFFFFFCE = -50 + */ + value->value.u32 *= amount->value.u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.u32 *= (unsigned)amount->value.i32; + break; + + case RC_VALUE_TYPE_FLOAT: + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } + break; + + case RC_VALUE_TYPE_SIGNED: + switch (amount->type) + { + case RC_VALUE_TYPE_SIGNED: + value->value.i32 *= amount->value.i32; + break; + + case RC_VALUE_TYPE_UNSIGNED: + value->value.i32 *= (int)amount->value.u32; + break; + + case RC_VALUE_TYPE_FLOAT: + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } + break; + + case RC_VALUE_TYPE_FLOAT: + if (amount->type == RC_VALUE_TYPE_NONE) { + value->type = RC_VALUE_TYPE_NONE; + } + else { + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + } + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } +} + +void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + if (amount->value.u32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 /= amount->value.u32; + return; + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 /= (int)amount->value.u32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_SIGNED: + if (amount->value.i32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 /= amount->value.i32; + return; + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 /= (unsigned)amount->value.i32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_FLOAT: + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + + if (amount->value.f32 == 0.0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 /= amount->value.f32; +} + +static int rc_typed_value_compare_floats(float f1, float f2, char oper) { + if (f1 == f2) { + /* exactly equal */ + } + else { + /* attempt to match 7 significant digits (24-bit mantissa supports just over 7 significant decimal digits) */ + /* https://stackoverflow.com/questions/17333/what-is-the-most-effective-way-for-float-and-double-comparison */ + const float abs1 = (f1 < 0) ? -f1 : f1; + const float abs2 = (f2 < 0) ? -f2 : f2; + const float threshold = ((abs1 < abs2) ? abs1 : abs2) * FLT_EPSILON; + const float diff = f1 - f2; + const float abs_diff = (diff < 0) ? -diff : diff; + + if (abs_diff <= threshold) { + /* approximately equal */ + } + else if (diff > threshold) { + /* greater */ + switch (oper) { + case RC_OPERATOR_NE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + return 1; + + default: + return 0; + } + } + else { + /* lesser */ + switch (oper) { + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } + } + } + + /* exactly or approximately equal */ + switch (oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper) { + rc_typed_value_t converted_value2; + if (value2->type != value1->type) + value2 = rc_typed_value_convert_into(&converted_value2, value2, value1->type); + + switch (value1->type) { + case RC_VALUE_TYPE_UNSIGNED: + switch (oper) { + case RC_OPERATOR_EQ: return value1->value.u32 == value2->value.u32; + case RC_OPERATOR_NE: return value1->value.u32 != value2->value.u32; + case RC_OPERATOR_LT: return value1->value.u32 < value2->value.u32; + case RC_OPERATOR_LE: return value1->value.u32 <= value2->value.u32; + case RC_OPERATOR_GT: return value1->value.u32 > value2->value.u32; + case RC_OPERATOR_GE: return value1->value.u32 >= value2->value.u32; + default: return 1; + } + + case RC_VALUE_TYPE_SIGNED: + switch (oper) { + case RC_OPERATOR_EQ: return value1->value.i32 == value2->value.i32; + case RC_OPERATOR_NE: return value1->value.i32 != value2->value.i32; + case RC_OPERATOR_LT: return value1->value.i32 < value2->value.i32; + case RC_OPERATOR_LE: return value1->value.i32 <= value2->value.i32; + case RC_OPERATOR_GT: return value1->value.i32 > value2->value.i32; + case RC_OPERATOR_GE: return value1->value.i32 >= value2->value.i32; + default: return 1; + } + + case RC_VALUE_TYPE_FLOAT: + return rc_typed_value_compare_floats(value1->value.f32, value2->value.f32, oper); + + default: + return 1; + } +} diff --git a/src/rcheevos/src/rhash/cdreader.c b/src/rcheevos/src/rhash/cdreader.c new file mode 100644 index 000000000..c0f5c88a2 --- /dev/null +++ b/src/rcheevos/src/rhash/cdreader.c @@ -0,0 +1,879 @@ +#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, int64_t offset, int origin); +extern int64_t rc_file_tell(void* file_handle); +extern size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes); +extern void rc_file_close(void* file_handle); +extern int rc_hash_error(const char* message); +extern const char* rc_path_get_filename(const char* path); +extern int rc_path_compare_extension(const char* path, const char* ext); +extern rc_hash_message_callback verbose_message_callback; + +struct cdrom_t +{ + void* file_handle; /* the file handle for reading the track data */ + int sector_size; /* the size of each sector in the track data */ + int sector_header_size; /* the offset to the raw data within a sector block */ + int raw_data_size; /* the amount of raw data within a sector block */ + int64_t file_track_offset;/* the offset of the track data within the file */ + int track_first_sector; /* the first absolute sector associated to the track (includes pregap) */ + int track_pregap_sectors; /* the number of pregap sectors */ +#ifndef NDEBUG + uint32_t track_id; /* the index of the track */ +#endif +}; + +static int cdreader_get_sector(unsigned char header[16]) +{ + int minutes = (header[12] >> 4) * 10 + (header[12] & 0x0F); + int seconds = (header[13] >> 4) * 10 + (header[13] & 0x0F); + int frames = (header[14] >> 4) * 10 + (header[14] & 0x0F); + + /* convert the MSF value to a sector index, and subtract 150 (2 seconds) per: + * For data and mixed mode media (those conforming to ISO/IEC 10149), logical block address + * zero shall be assigned to the block at MSF address 00/02/00 */ + return ((minutes * 60) + seconds) * 75 + frames - 150; +} + +static void cdreader_determine_sector_size(struct cdrom_t* cdrom) +{ + /* Attempt to determine the sector and header sizes. The CUE file may be lying. + * Look for the sync pattern using each of the supported sector sizes. + * Then check for the presence of "CD001", which is gauranteed to be in either the + * boot record or primary volume descriptor, one of which is always at sector 16. + */ + const unsigned char sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + }; + + unsigned char header[32]; + const int64_t toc_sector = 16 + cdrom->track_pregap_sectors; + + cdrom->sector_size = 0; + cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2048; + + rc_file_seek(cdrom->file_handle, toc_sector * 2352 + cdrom->file_track_offset, SEEK_SET); + if (rc_file_read(cdrom->file_handle, header, sizeof(header)) < sizeof(header)) + return; + + if (memcmp(header, sync_pattern, 12) == 0) + { + cdrom->sector_size = 2352; + + if (memcmp(&header[25], "CD001", 5) == 0) + cdrom->sector_header_size = 24; + else + cdrom->sector_header_size = 16; + + cdrom->track_first_sector = cdreader_get_sector(header) - (int)toc_sector; + } + else + { + rc_file_seek(cdrom->file_handle, toc_sector * 2336 + cdrom->file_track_offset, SEEK_SET); + rc_file_read(cdrom->file_handle, header, sizeof(header)); + + if (memcmp(header, sync_pattern, 12) == 0) + { + cdrom->sector_size = 2336; + + if (memcmp(&header[25], "CD001", 5) == 0) + cdrom->sector_header_size = 24; + else + cdrom->sector_header_size = 16; + + cdrom->track_first_sector = cdreader_get_sector(header) - (int)toc_sector; + } + else + { + rc_file_seek(cdrom->file_handle, toc_sector * 2048 + cdrom->file_track_offset, SEEK_SET); + rc_file_read(cdrom->file_handle, header, sizeof(header)); + + if (memcmp(&header[1], "CD001", 5) == 0) + { + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + } + } +} + +static void* cdreader_open_bin_track(const char* path, uint32_t track) +{ + void* file_handle; + struct cdrom_t* cdrom; + + if (track > 1) + { + if (verbose_message_callback) + verbose_message_callback("Cannot locate secondary tracks without a cue sheet"); + + return NULL; + } + + file_handle = rc_file_open(path); + if (!file_handle) + return NULL; + + cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + return NULL; + cdrom->file_handle = file_handle; +#ifndef NDEBUG + cdrom->track_id = track; +#endif + + cdreader_determine_sector_size(cdrom); + + if (cdrom->sector_size == 0) + { + int64_t size; + + rc_file_seek(cdrom->file_handle, 0, SEEK_END); + size = rc_file_tell(cdrom->file_handle); + + if ((size % 2352) == 0) + { + /* raw tracks use all 2352 bytes and have a 24 byte header */ + cdrom->sector_size = 2352; + cdrom->sector_header_size = 24; + } + else if ((size % 2048) == 0) + { + /* cooked tracks eliminate all header/footer data */ + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + else if ((size % 2336) == 0) + { + /* MODE 2 format without 16-byte sync data */ + cdrom->sector_size = 2336; + cdrom->sector_header_size = 8; + } + else + { + free(cdrom); + + if (verbose_message_callback) + verbose_message_callback("Could not determine sector size"); + + return NULL; + } + } + + return cdrom; +} + +static int cdreader_open_bin(struct cdrom_t* cdrom, const char* path, const char* mode) +{ + cdrom->file_handle = rc_file_open(path); + if (!cdrom->file_handle) + return 0; + + /* determine sector size */ + cdreader_determine_sector_size(cdrom); + + /* could not determine, which means we'll probably have more issues later + * but use the CUE provided information anyway + */ + if (cdrom->sector_size == 0) + { + /* All of these modes have 2048 byte payloads. In MODE1/2352 and MODE2/2352 + * modes, the mode can actually be specified per sector to change the payload + * size, but that reduces the ability to recover from errors when the disc + * is damaged, so it's seldomly used, and when it is, it's mostly for audio + * or video data where a blip or two probably won't be noticed by the user. + * So, while we techincally support all of the following modes, we only do + * so with 2048 byte payloads. + * http://totalsonicmastering.com/cuesheetsyntax.htm + * MODE1/2048 ? CDROM Mode1 Data (cooked) [no header, no footer] + * MODE1/2352 ? CDROM Mode1 Data (raw) [16 byte header, 288 byte footer] + * MODE2/2336 ? CDROM-XA Mode2 Data [8 byte header, 280 byte footer] + * MODE2/2352 ? CDROM-XA Mode2 Data [24 byte header, 280 byte footer] + */ + if (memcmp(mode, "MODE2/2352", 10) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 24; + } + else if (memcmp(mode, "MODE1/2048", 10) == 0) + { + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + else if (memcmp(mode, "MODE2/2336", 10) == 0) + { + cdrom->sector_size = 2336; + cdrom->sector_header_size = 8; + } + else if (memcmp(mode, "MODE1/2352", 10) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 16; + } + else if (memcmp(mode, "AUDIO", 5) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2352; /* no header or footer data on audio tracks */ + } + } + + return (cdrom->sector_size != 0); +} + +static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name) +{ + const char* filename = rc_path_get_filename(cue_path); + const size_t bin_name_len = strlen(bin_name); + const size_t cue_path_len = filename - cue_path; + const size_t needed = cue_path_len + bin_name_len + 1; + + char* bin_filename = (char*)malloc(needed); + if (!bin_filename) + { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)needed); + rc_hash_error((const char*)buffer); + } + else + { + memcpy(bin_filename, cue_path, cue_path_len); + memcpy(bin_filename + cue_path_len, bin_name, bin_name_len + 1); + } + + return bin_filename; +} + +static int64_t cdreader_get_bin_size(const char* cue_path, const char* bin_name) +{ + int64_t size = 0; + char* bin_filename = cdreader_get_bin_path(cue_path, bin_name); + if (bin_filename) + { + /* disable verbose messaging while getting file size */ + rc_hash_message_callback old_verbose_message_callback = verbose_message_callback; + void* file_handle; + verbose_message_callback = NULL; + + file_handle = rc_file_open(bin_filename); + if (file_handle) + { + rc_file_seek(file_handle, 0, SEEK_END); + size = rc_file_tell(file_handle); + rc_file_close(file_handle); + } + + verbose_message_callback = old_verbose_message_callback; + free(bin_filename); + } + + return size; +} + +static void* cdreader_open_cue_track(const char* path, uint32_t track) +{ + void* cue_handle; + int64_t cue_offset = 0; + char buffer[1024]; + char* bin_filename = NULL; + char *ptr, *ptr2, *end; + int done = 0; + int session = 1; + size_t num_read = 0; + struct cdrom_t* cdrom = NULL; + + struct track_t + { + uint32_t id; + int sector_size; + int sector_count; + int first_sector; + int pregap_sectors; + int is_data; + int file_track_offset; + int file_first_sector; + char mode[16]; + char filename[256]; + } current_track, previous_track, largest_track; + + cue_handle = rc_file_open(path); + if (!cue_handle) + return NULL; + + memset(¤t_track, 0, sizeof(current_track)); + memset(&previous_track, 0, sizeof(previous_track)); + memset(&largest_track, 0, sizeof(largest_track)); + + do + { + num_read = rc_file_read(cue_handle, buffer, sizeof(buffer) - 1); + if (num_read == 0) + break; + + buffer[num_read] = 0; + if (num_read == sizeof(buffer) - 1) + end = buffer + sizeof(buffer) * 3 / 4; + else + end = buffer + num_read; + + for (ptr = buffer; ptr < end; ++ptr) + { + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "INDEX ", 6) == 0) + { + int m = 0, s = 0, f = 0; + int index; + int sector_offset; + + ptr += 6; + index = atoi(ptr); + + while (*ptr != ' ' && *ptr != '\n') + ++ptr; + while (*ptr == ' ') + ++ptr; + + /* convert mm:ss:ff to sector count */ + sscanf_s(ptr, "%d:%d:%d", &m, &s, &f); + sector_offset = ((m * 60) + s) * 75 + f; + + if (current_track.first_sector == -1) + { + current_track.first_sector = sector_offset; + if (strcmp(current_track.filename, previous_track.filename) == 0) + { + previous_track.sector_count = current_track.first_sector - previous_track.first_sector; + current_track.file_track_offset += previous_track.sector_count * previous_track.sector_size; + } + + /* if looking for the largest data track, determine previous track size */ + if (track == RC_HASH_CDTRACK_LARGEST && previous_track.sector_count > largest_track.sector_count && + previous_track.is_data) + { + memcpy(&largest_track, &previous_track, sizeof(largest_track)); + } + } + + if (index == 1) + { + current_track.pregap_sectors = (sector_offset - current_track.first_sector); + + if (verbose_message_callback) + { + char message[128]; + char* scan = current_track.mode; + while (*scan && !isspace((unsigned char)*scan)) + ++scan; + *scan = '\0'; + + /* it's undesirable to truncate offset to 32-bits, but %lld isn't defined in c89. */ + snprintf(message, sizeof(message), "Found %s track %d (first sector %d, sector size %d, %d pregap sectors)", + current_track.mode, current_track.id, current_track.first_sector, current_track.sector_size, current_track.pregap_sectors); + verbose_message_callback(message); + } + + if (current_track.id == track) + { + done = 1; + break; + } + + if (track == RC_HASH_CDTRACK_FIRST_DATA && current_track.is_data) + { + track = current_track.id; + done = 1; + break; + } + + if (track == RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION && session == 2) + { + track = current_track.id; + done = 1; + break; + } + } + } + else if (strncasecmp(ptr, "TRACK ", 6) == 0) + { + if (current_track.sector_size) + memcpy(&previous_track, ¤t_track, sizeof(current_track)); + + ptr += 6; + current_track.id = atoi(ptr); + + current_track.pregap_sectors = -1; + current_track.first_sector = -1; + + while (*ptr != ' ') + ++ptr; + while (*ptr == ' ') + ++ptr; + memcpy(current_track.mode, ptr, sizeof(current_track.mode)); + current_track.is_data = (memcmp(current_track.mode, "MODE", 4) == 0); + + if (current_track.is_data) + { + current_track.sector_size = atoi(ptr + 6); + } + else + { + /* assume AUDIO */ + current_track.sector_size = 2352; + } + } + else if (strncasecmp(ptr, "FILE ", 5) == 0) + { + if (current_track.sector_size) + { + memcpy(&previous_track, ¤t_track, sizeof(previous_track)); + + if (previous_track.sector_count == 0) + { + const uint32_t file_sector_count = (uint32_t)cdreader_get_bin_size(path, previous_track.filename) / previous_track.sector_size; + previous_track.sector_count = file_sector_count - previous_track.first_sector; + } + + /* if looking for the largest data track, check to see if this one is larger */ + if (track == RC_HASH_CDTRACK_LARGEST && previous_track.is_data && + previous_track.sector_count > largest_track.sector_count) + { + memcpy(&largest_track, &previous_track, sizeof(largest_track)); + } + } + + memset(¤t_track, 0, sizeof(current_track)); + + current_track.file_first_sector = previous_track.file_first_sector + + previous_track.first_sector + previous_track.sector_count; + + ptr += 5; + ptr2 = ptr; + if (*ptr == '"') + { + ++ptr; + do + { + ++ptr2; + } while (*ptr2 && *ptr2 != '\n' && *ptr2 != '"'); + } + else + { + do + { + ++ptr2; + } while (*ptr2 && *ptr2 != '\n' && *ptr2 != ' '); + } + + if (ptr2 - ptr < (int)sizeof(current_track.filename)) + memcpy(current_track.filename, ptr, ptr2 - ptr); + } + else if (strncasecmp(ptr, "REM ", 4) == 0) + { + ptr += 4; + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "SESSION ", 8) == 0) + { + ptr += 8; + while (*ptr == ' ') + ++ptr; + session = atoi(ptr); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + } + + if (done) + break; + + cue_offset += (ptr - buffer); + rc_file_seek(cue_handle, cue_offset, SEEK_SET); + + } while (1); + + rc_file_close(cue_handle); + + if (track == RC_HASH_CDTRACK_LARGEST) + { + if (current_track.sector_size && current_track.is_data) + { + const uint32_t file_sector_count = (uint32_t)cdreader_get_bin_size(path, current_track.filename) / current_track.sector_size; + current_track.sector_count = file_sector_count - current_track.first_sector; + + if (largest_track.sector_count > current_track.sector_count) + memcpy(¤t_track, &largest_track, sizeof(current_track)); + } + else + { + memcpy(¤t_track, &largest_track, sizeof(current_track)); + } + + track = current_track.id; + } + else if (track == RC_HASH_CDTRACK_LAST && !done) + { + track = current_track.id; + } + + if (current_track.id == track) + { + cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + { + snprintf((char*)buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); + rc_hash_error((const char*)buffer); + return NULL; + } + + cdrom->file_track_offset = current_track.file_track_offset; + cdrom->track_pregap_sectors = current_track.pregap_sectors; + cdrom->track_first_sector = current_track.file_first_sector + current_track.first_sector; +#ifndef NDEBUG + cdrom->track_id = current_track.id; +#endif + + /* verify existance of bin file */ + bin_filename = cdreader_get_bin_path(path, current_track.filename); + if (bin_filename) + { + if (cdreader_open_bin(cdrom, bin_filename, current_track.mode)) + { + if (verbose_message_callback) + { + if (cdrom->track_pregap_sectors) + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d, %d pregap sectors)", + track, cdrom->sector_size, cdrom->track_pregap_sectors); + else + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", track, cdrom->sector_size); + + verbose_message_callback((const char*)buffer); + } + } + else + { + if (cdrom->file_handle) + { + rc_file_close(cdrom->file_handle); + snprintf((char*)buffer, sizeof(buffer), "Could not determine sector size for %s track", current_track.mode); + } + else + { + snprintf((char*)buffer, sizeof(buffer), "Could not open %s", bin_filename); + } + + rc_hash_error((const char*)buffer); + + free(cdrom); + cdrom = NULL; + } + + free(bin_filename); + } + } + + return cdrom; +} + +static void* cdreader_open_gdi_track(const char* path, uint32_t track) +{ + void* file_handle; + char buffer[1024]; + char mode[16] = "MODE1/"; + char sector_size[16]; + char file[256]; + int64_t track_size; + int track_type; + char* bin_path = NULL; + uint32_t current_track = 0; + char* ptr, *ptr2, *end; + int lba = 0; + + uint32_t largest_track = 0; + int64_t largest_track_size = 0; + char largest_track_file[256]; + char largest_track_sector_size[16]; + int largest_track_lba = 0; + + int found = 0; + size_t num_read = 0; + int64_t file_offset = 0; + struct cdrom_t* cdrom = NULL; + + file_handle = rc_file_open(path); + if (!file_handle) + return NULL; + + file[0] = '\0'; + do + { + num_read = rc_file_read(file_handle, buffer, sizeof(buffer) - 1); + if (num_read == 0) + break; + + buffer[num_read] = 0; + if (num_read == sizeof(buffer) - 1) + end = buffer + sizeof(buffer) * 3 / 4; + else + end = buffer + num_read; + + ptr = buffer; + + /* the first line contains the number of tracks, so we can get the last track index from it */ + if (track == RC_HASH_CDTRACK_LAST) + track = atoi(ptr); + + /* first line contains the number of tracks and will be skipped */ + while (ptr < end) + { + /* skip until next newline */ + while (*ptr != '\n' && ptr < end) + ++ptr; + + /* skip newlines */ + while ((*ptr == '\n' || *ptr == '\r') && ptr < end) + ++ptr; + + /* line format: [trackid] [lba] [type] [sectorsize] [file] [?] */ + while (isspace((unsigned char)*ptr)) + ++ptr; + + current_track = (uint32_t)atoi(ptr); + if (track && current_track != track && track != RC_HASH_CDTRACK_FIRST_DATA) + continue; + + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + lba = atoi(ptr); + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + track_type = atoi(ptr); + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + ptr2 = sector_size; + while (isdigit((unsigned char)*ptr)) + *ptr2++ = *ptr++; + *ptr2 = '\0'; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + ptr2 = file; + if (*ptr == '\"') + { + ++ptr; + while (*ptr != '\"') + *ptr2++ = *ptr++; + ++ptr; + } + else + { + while (*ptr != ' ') + *ptr2++ = *ptr++; + } + *ptr2 = '\0'; + + if (track == current_track) + { + found = 1; + break; + } + else if (track == RC_HASH_CDTRACK_FIRST_DATA && track_type == 4) + { + track = current_track; + found = 1; + break; + } + else if (track == RC_HASH_CDTRACK_LARGEST && track_type == 4) + { + track_size = cdreader_get_bin_size(path, file); + if (track_size > largest_track_size) + { + largest_track_size = track_size; + largest_track = current_track; + largest_track_lba = lba; + strcpy_s(largest_track_file, sizeof(largest_track_file), file); + strcpy_s(largest_track_sector_size, sizeof(largest_track_sector_size), sector_size); + } + } + } + + if (found) + break; + + file_offset += (ptr - buffer); + rc_file_seek(file_handle, file_offset, SEEK_SET); + + } while (1); + + rc_file_close(file_handle); + + cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + { + snprintf((char*)buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); + rc_hash_error((const char*)buffer); + return NULL; + } + + /* if we were tracking the largest track, make it the current track. + * otherwise, current_track will be the requested track, or last track. */ + if (largest_track != 0 && largest_track != current_track) + { + current_track = largest_track; + strcpy_s(file, sizeof(file), largest_track_file); + strcpy_s(sector_size, sizeof(sector_size), largest_track_sector_size); + lba = largest_track_lba; + } + + /* open the bin file for the track - construct mode parameter from sector_size */ + ptr = &mode[6]; + ptr2 = sector_size; + while (*ptr2 && *ptr2 != '\"') + *ptr++ = *ptr2++; + *ptr = '\0'; + + bin_path = cdreader_get_bin_path(path, file); + if (cdreader_open_bin(cdrom, bin_path, mode)) + { + cdrom->track_pregap_sectors = 0; + cdrom->track_first_sector = lba; +#ifndef NDEBUG + cdrom->track_id = current_track; +#endif + + if (verbose_message_callback) + { + snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", current_track, cdrom->sector_size); + verbose_message_callback((const char*)buffer); + } + } + else + { + snprintf((char*)buffer, sizeof(buffer), "Could not open %s", bin_path); + rc_hash_error((const char*)buffer); + + free(cdrom); + cdrom = NULL; + } + + free(bin_path); + + return cdrom; +} + +static void* cdreader_open_track(const char* path, uint32_t track) +{ + /* backwards compatibility - 0 used to mean largest */ + if (track == 0) + track = RC_HASH_CDTRACK_LARGEST; + + if (rc_path_compare_extension(path, "cue")) + return cdreader_open_cue_track(path, track); + if (rc_path_compare_extension(path, "gdi")) + return cdreader_open_gdi_track(path, track); + + return cdreader_open_bin_track(path, track); +} + +static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + int64_t sector_start; + size_t num_read, total_read = 0; + uint8_t* buffer_ptr = (uint8_t*)buffer; + + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (!cdrom) + return 0; + + if (sector < (uint32_t)cdrom->track_first_sector) + return 0; + + sector_start = (int64_t)(sector - cdrom->track_first_sector) * cdrom->sector_size + + cdrom->sector_header_size + cdrom->file_track_offset; + + while (requested_bytes > (size_t)cdrom->raw_data_size) + { + rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, cdrom->raw_data_size); + total_read += num_read; + + if (num_read < (size_t)cdrom->raw_data_size) + return total_read; + + buffer_ptr += cdrom->raw_data_size; + sector_start += cdrom->sector_size; + requested_bytes -= cdrom->raw_data_size; + } + + rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, (int)requested_bytes); + total_read += num_read; + + return total_read; +} + +static void cdreader_close_track(void* track_handle) +{ + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (cdrom) + { + if (cdrom->file_handle) + rc_file_close(cdrom->file_handle); + + free(track_handle); + } +} + +static uint32_t cdreader_first_track_sector(void* track_handle) +{ + struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + if (cdrom) + return cdrom->track_first_sector + cdrom->track_pregap_sectors; + + return 0; +} + +void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader) +{ + cdreader->open_track = cdreader_open_track; + cdreader->read_sector = cdreader_read_sector; + cdreader->close_track = cdreader_close_track; + cdreader->first_track_sector = cdreader_first_track_sector; +} + +void rc_hash_init_default_cdreader(void) +{ + struct rc_hash_cdreader cdreader; + rc_hash_get_default_cdreader(&cdreader); + rc_hash_init_custom_cdreader(&cdreader); +} diff --git a/src/rcheevos/src/rhash/hash.c b/src/rcheevos/src/rhash/hash.c new file mode 100644 index 000000000..e93423b86 --- /dev/null +++ b/src/rcheevos/src/rhash/hash.c @@ -0,0 +1,2924 @@ +#include "rc_hash.h" + +#include "../rcheevos/rc_compat.h" + +#include "md5.h" + +#include +#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) +{ +#if defined(__STDC_WANT_SECURE_LIB__) + FILE* fp; + fopen_s(&fp, path, "rb"); + return fp; +#else + return fopen(path, "rb"); +#endif +} + +static void filereader_seek(void* file_handle, int64_t offset, int origin) +{ +#if defined(_WIN32) + _fseeki64((FILE*)file_handle, offset, origin); +#elif defined(_LARGEFILE64_SOURCE) + fseeko64((FILE*)file_handle, offset, origin); +#else + fseek((FILE*)file_handle, offset, origin); +#endif +} + +static int64_t filereader_tell(void* file_handle) +{ +#if defined(_WIN32) + return _ftelli64((FILE*)file_handle); +#elif defined(_LARGEFILE64_SOURCE) + return ftello64((FILE*)file_handle); +#else + return ftell((FILE*)file_handle); +#endif +} + +static size_t filereader_read(void* file_handle, void* buffer, size_t requested_bytes) +{ + return fread(buffer, 1, requested_bytes, (FILE*)file_handle); +} + +static void filereader_close(void* file_handle) +{ + fclose((FILE*)file_handle); +} + +/* for unit tests - normally would call rc_hash_init_custom_filereader(NULL) */ +void rc_hash_reset_filereader(void) +{ + filereader = NULL; +} + +void rc_hash_init_custom_filereader(struct rc_hash_filereader* reader) +{ + /* initialize with defaults first */ + filereader_funcs.open = filereader_open; + filereader_funcs.seek = filereader_seek; + filereader_funcs.tell = filereader_tell; + filereader_funcs.read = filereader_read; + filereader_funcs.close = filereader_close; + + /* hook up any provided custom handlers */ + if (reader) { + if (reader->open) + filereader_funcs.open = reader->open; + + if (reader->seek) + filereader_funcs.seek = reader->seek; + + if (reader->tell) + filereader_funcs.tell = reader->tell; + + if (reader->read) + filereader_funcs.read = reader->read; + + if (reader->close) + filereader_funcs.close = reader->close; + } + + filereader = &filereader_funcs; +} + +void* rc_file_open(const char* path) +{ + void* handle; + + if (!filereader) + { + rc_hash_init_custom_filereader(NULL); + if (!filereader) + return NULL; + } + + handle = filereader->open(path); + if (handle && verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "Opened %s", rc_path_get_filename(path)); + verbose_message_callback(message); + } + + return handle; +} + +void rc_file_seek(void* file_handle, int64_t offset, int origin) +{ + if (filereader) + filereader->seek(file_handle, offset, origin); +} + +int64_t rc_file_tell(void* file_handle) +{ + return (filereader) ? filereader->tell(file_handle) : 0; +} + +size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes) +{ + return (filereader) ? filereader->read(file_handle, buffer, requested_bytes) : 0; +} + +void rc_file_close(void* file_handle) +{ + if (filereader) + filereader->close(file_handle); +} + +/* ===================================================== */ + +static struct rc_hash_cdreader cdreader_funcs; +struct rc_hash_cdreader* cdreader = NULL; + +void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader) +{ + if (reader) + { + memcpy(&cdreader_funcs, reader, sizeof(cdreader_funcs)); + cdreader = &cdreader_funcs; + } + else + { + cdreader = NULL; + } +} + +static void* rc_cd_open_track(const char* path, uint32_t track) +{ + if (cdreader && cdreader->open_track) + return cdreader->open_track(path, track); + + rc_hash_error("no hook registered for cdreader_open_track"); + return NULL; +} + +static size_t rc_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + if (cdreader && cdreader->read_sector) + return cdreader->read_sector(track_handle, sector, buffer, requested_bytes); + + rc_hash_error("no hook registered for cdreader_read_sector"); + return 0; +} + +static uint32_t rc_cd_first_track_sector(void* track_handle) +{ + if (cdreader && cdreader->first_track_sector) + return cdreader->first_track_sector(track_handle); + + rc_hash_error("no hook registered for cdreader_first_track_sector"); + return 0; +} + +static void rc_cd_close_track(void* track_handle) +{ + if (cdreader && cdreader->close_track) + { + cdreader->close_track(track_handle); + return; + } + + rc_hash_error("no hook registered for cdreader_close_track"); +} + +static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, unsigned* size) +{ + uint8_t buffer[2048], *tmp; + int sector; + unsigned num_sectors = 0; + size_t filename_length; + const char* slash; + + if (!track_handle) + return 0; + + /* we start at the root. don't need to explicitly find it */ + if (*path == '\\') + ++path; + + filename_length = strlen(path); + slash = strrchr(path, '\\'); + if (slash) + { + /* find the directory record for the first part of the path */ + memcpy(buffer, path, slash - path); + buffer[slash - path] = '\0'; + + sector = rc_cd_find_file_sector(track_handle, (const char *)buffer, NULL); + if (!sector) + return 0; + + ++slash; + filename_length -= (slash - path); + path = slash; + } + else + { + unsigned logical_block_size; + + /* find the cd information */ + if (!rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 16, buffer, 256)) + return 0; + + /* the directory_record starts at 156, the sector containing the table of contents is 2 bytes into that. + * https://www.cdroller.com/htm/readdata.html + */ + sector = buffer[156 + 2] | (buffer[156 + 3] << 8) | (buffer[156 + 4] << 16); + + /* if the table of contents spans more than one sector, it's length of section will exceed it's logical block size */ + logical_block_size = (buffer[128] | (buffer[128 + 1] << 8)); /* logical block size */ + if (logical_block_size == 0) { + num_sectors = 1; + } else { + num_sectors = (buffer[156 + 10] | (buffer[156 + 11] << 8) | (buffer[156 + 12] << 16) | (buffer[156 + 13] << 24)); /* length of section */ + num_sectors /= logical_block_size; + } + } + + /* fetch and process the directory record */ + if (!rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) + return 0; + + tmp = buffer; + do + { + if (tmp >= buffer + sizeof(buffer) || !*tmp) + { + /* end of this path table block. if the path table spans multiple sectors, keep scanning */ + if (num_sectors > 1) + { + --num_sectors; + if (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer))) + { + tmp = buffer; + continue; + } + } + break; + } + + /* filename is 33 bytes into the record and the format is "FILENAME;version" or "DIRECTORY" */ + if ((tmp[32] == filename_length || tmp[33 + filename_length] == ';') && + strncasecmp((const char*)(tmp + 33), path, filename_length) == 0) + { + sector = tmp[2] | (tmp[3] << 8) | (tmp[4] << 16); + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Found %s at sector %d", path, sector); + verbose_message_callback(message); + } + + if (size) + *size = tmp[10] | (tmp[11] << 8) | (tmp[12] << 16) | (tmp[13] << 24); + + return sector; + } + + /* the first byte of the record is the length of the record */ + tmp += *tmp; + } while (1); + + return 0; +} + +/* ===================================================== */ + +const char* rc_path_get_filename(const char* path) +{ + const char* ptr = path + strlen(path); + do + { + if (ptr[-1] == '/' || ptr[-1] == '\\') + break; + + --ptr; + } while (ptr > path); + + return ptr; +} + +static const char* rc_path_get_extension(const char* path) +{ + const char* ptr = path + strlen(path); + do + { + if (ptr[-1] == '.') + return ptr; + + --ptr; + } while (ptr > path); + + return path + strlen(path); +} + +int rc_path_compare_extension(const char* path, const char* ext) +{ + size_t path_len = strlen(path); + size_t ext_len = strlen(ext); + const char* ptr = path + path_len - ext_len; + if (ptr[-1] != '.') + return 0; + + if (memcmp(ptr, ext, ext_len) == 0) + return 1; + + do + { + if (tolower(*ptr) != *ext) + return 0; + + ++ext; + ++ptr; + } while (*ptr); + + return 1; +} + +/* ===================================================== */ + +static void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF00FF00) >> 8 | + (temp & 0x00FF00FF) << 8; + *ptr++ = temp; + } +} + +static void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF000000) >> 24 | + (temp & 0x00FF0000) >> 8 | + (temp & 0x0000FF00) << 8 | + (temp & 0x000000FF) << 24; + *ptr++ = temp; + } +} + +static int rc_hash_finalize(md5_state_t* md5, char hash[33]) +{ + md5_byte_t digest[16]; + + md5_finish(md5, digest); + + /* NOTE: sizeof(hash) is 4 because it's still treated like a pointer, despite specifying a size */ + snprintf(hash, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Generated hash %s", hash); + verbose_message_callback(message); + } + + return 1; +} + +static int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + md5_state_t md5; + md5_init(&md5); + + if (buffer_size > MAX_BUFFER_SIZE) + buffer_size = MAX_BUFFER_SIZE; + + md5_append(&md5, buffer, (int)buffer_size); + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %u byte buffer", (unsigned)buffer_size); + verbose_message_callback(message); + } + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector, const char* name, unsigned size, const char* description) +{ + uint8_t buffer[2048]; + size_t num_read; + + if ((num_read = rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) < sizeof(buffer)) + { + char message[128]; + snprintf(message, sizeof(message), "Could not read %s", description); + return rc_hash_error(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + if (verbose_message_callback) + { + char message[128]; + if (name) + snprintf(message, sizeof(message), "Hashing %s title (%u bytes) and contents (%u bytes) ", name, (unsigned)strlen(name), size); + else + snprintf(message, sizeof(message), "Hashing %s contents (%u bytes @ sector %u)", description, size, sector); + + verbose_message_callback(message); + } + + if (size < (unsigned)num_read) + size = (unsigned)num_read; + + do + { + md5_append(md5, buffer, (int)num_read); + + if (size <= (unsigned)num_read) + break; + size -= (unsigned)num_read; + + ++sector; + if (size >= sizeof(buffer)) + num_read = rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + else + num_read = rc_cd_read_sector(track_handle, sector, buffer, size); + } while (num_read > 0); + + return 1; +} + +static int rc_hash_3do(char hash[33], const char* path) +{ + uint8_t buffer[2048]; + const uint8_t operafs_identifier[7] = { 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01 }; + void* track_handle; + md5_state_t md5; + int sector; + int block_size, block_location; + int offset, stop; + size_t size = 0; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* the Opera filesystem stores the volume information in the first 132 bytes of sector 0 + * https://github.com/barbeque/3dodump/blob/master/OperaFS-Format.md + */ + rc_cd_read_sector(track_handle, 0, buffer, 132); + + if (memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) + { + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Found 3DO CD, title=%.32s", &buffer[0x28]); + verbose_message_callback(message); + } + + /* include the volume header in the hash */ + md5_init(&md5); + md5_append(&md5, buffer, 132); + + /* the block size is at offset 0x4C (assume 0x4C is always 0) */ + block_size = buffer[0x4D] * 65536 + buffer[0x4E] * 256 + buffer[0x4F]; + + /* the root directory block location is at offset 0x64 (and duplicated several + * times, but we just look at the primary record) (assume 0x64 is always 0)*/ + block_location = buffer[0x65] * 65536 + buffer[0x66] * 256 + buffer[0x67]; + + /* multiply the block index by the block size to get the real address */ + block_location *= block_size; + + /* convert that to a sector and read it */ + sector = block_location / 2048; + + do + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + + /* offset to start of entries is at offset 0x10 (assume 0x10 and 0x11 are always 0) */ + offset = buffer[0x12] * 256 + buffer[0x13]; + + /* offset to end of entries is at offset 0x0C (assume 0x0C is always 0) */ + stop = buffer[0x0D] * 65536 + buffer[0x0E] * 256 + buffer[0x0F]; + + while (offset < stop) + { + if (buffer[offset + 0x03] == 0x02) /* file */ + { + if (strcasecmp((const char*)&buffer[offset + 0x20], "LaunchMe") == 0) + { + /* the block size is at offset 0x0C (assume 0x0C is always 0) */ + block_size = buffer[offset + 0x0D] * 65536 + buffer[offset + 0x0E] * 256 + buffer[offset + 0x0F]; + + /* the block location is at offset 0x44 (assume 0x44 is always 0) */ + block_location = buffer[offset + 0x45] * 65536 + buffer[offset + 0x46] * 256 + buffer[offset + 0x47]; + block_location *= block_size; + + /* the file size is at offset 0x10 (assume 0x10 is always 0) */ + size = (size_t)buffer[offset + 0x11] * 65536 + buffer[offset + 0x12] * 256 + buffer[offset + 0x13]; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing header (%u bytes) and %.32s (%u bytes) ", 132, &buffer[offset + 0x20], (unsigned)size); + verbose_message_callback(message); + } + + break; + } + } + + /* the number of extra copies of the file is at offset 0x40 (assume 0x40-0x42 are always 0) */ + offset += 0x48 + buffer[offset + 0x43] * 4; + } + + if (size != 0) + break; + + /* did not find the file, see if the directory listing is continued in another sector */ + offset = buffer[0x02] * 256 + buffer[0x03]; + + /* no more sectors to search*/ + if (offset == 0xFFFF) + break; + + /* get next sector */ + offset *= block_size; + sector = (block_location + offset) / 2048; + } while (1); + + if (size == 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Could not find LaunchMe"); + } + + sector = block_location / 2048; + + while (size > 2048) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= 2048; + } + + rc_cd_read_sector(track_handle, sector, buffer, size); + md5_append(&md5, buffer, (int)size); + } + else + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a 3DO CD"); + } + + rc_cd_close_track(track_handle); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_7800(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&buffer[1], "ATARI7800", 9) == 0) + { + rc_hash_verbose("Ignoring 7800 header"); + + buffer += 128; + buffer_size -= 128; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_arcade(char hash[33], const char* path) +{ + /* arcade hash is just the hash of the filename (no extension) - the cores are pretty stringent about having the right ROM data */ + const char* filename = rc_path_get_filename(path); + const char* ext = rc_path_get_extension(filename); + size_t filename_length = ext - filename - 1; + + /* fbneo supports loading subsystems by using specific folder names. + * if one is found, include it in the hash. + * https://github.com/libretro/FBNeo/blob/master/src/burner/libretro/README.md#emulating-consoles + */ + if (filename > path + 1) + { + int include_folder = 0; + const char* folder = filename - 1; + size_t parent_folder_length = 0; + + do + { + if (folder[-1] == '/' || folder[-1] == '\\') + break; + + --folder; + } while (folder > path); + + parent_folder_length = filename - folder - 1; + switch (parent_folder_length) + { + case 3: + if (memcmp(folder, "nes", 3) == 0 || + memcmp(folder, "fds", 3) == 0 || + memcmp(folder, "sms", 3) == 0 || + memcmp(folder, "msx", 3) == 0 || + memcmp(folder, "ngp", 3) == 0 || + memcmp(folder, "pce", 3) == 0 || + memcmp(folder, "sgx", 3) == 0) + include_folder = 1; + break; + case 4: + if (memcmp(folder, "tg16", 4) == 0) + include_folder = 1; + break; + case 6: + if (memcmp(folder, "coleco", 6) == 0 || + memcmp(folder, "sg1000", 6) == 0) + include_folder = 1; + break; + case 8: + if (memcmp(folder, "gamegear", 8) == 0 || + memcmp(folder, "megadriv", 8) == 0 || + memcmp(folder, "spectrum", 8) == 0) + include_folder = 1; + break; + default: + break; + } + + if (include_folder) + { + char buffer[128]; /* realistically, this should never need more than ~20 characters */ + if (parent_folder_length + filename_length + 1 < sizeof(buffer)) + { + memcpy(&buffer[0], folder, parent_folder_length); + buffer[parent_folder_length] = '_'; + memcpy(&buffer[parent_folder_length + 1], filename, filename_length); + return rc_hash_buffer(hash, (uint8_t*)&buffer[0], parent_folder_length + filename_length + 1); + } + } + } + + return rc_hash_buffer(hash, (uint8_t*)filename, filename_length); +} + +static int rc_hash_text(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + md5_state_t md5; + const uint8_t* scan = buffer; + const uint8_t* stop = buffer + buffer_size; + + md5_init(&md5); + + do { + /* find end of line */ + while (scan < stop && *scan != '\r' && *scan != '\n') + ++scan; + + md5_append(&md5, buffer, (int)(scan - buffer)); + + /* include a normalized line ending */ + /* NOTE: this causes a line ending to be hashed at the end of the file, even if one was not present */ + md5_append(&md5, (const uint8_t*)"\n", 1); + + /* skip newline */ + if (scan < stop && *scan == '\r') + ++scan; + if (scan < stop && *scan == '\n') + ++scan; + + buffer = scan; + } while (scan < stop); + + return rc_hash_finalize(&md5, hash); +} + +/* helper variable only used for testing */ +const char* _rc_hash_jaguar_cd_homebrew_hash = NULL; + +static int rc_hash_jaguar_cd(char hash[33], const char* path) +{ + uint8_t buffer[2352]; + char message[128]; + void* track_handle; + md5_state_t md5; + int byteswapped = 0; + unsigned size = 0; + unsigned offset = 0; + unsigned sector = 0; + unsigned remaining; + unsigned i; + + /* Jaguar CD header is in the first sector of the first data track OF THE SECOND SESSION. + * The first track must be an audio track, but may be a warning message or actual game audio */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* The header is an unspecified distance into the first sector, but usually two bytes in. + * It consists of 64 bytes of "TAIR" or "ATRI" repeating, depending on whether or not the data + * is byteswapped. Then another 32 byte that reads "ATARI APPROVED DATA HEADER ATRI " + * (possibly byteswapped). Then a big-endian 32-bit value for the address where the boot code + * should be loaded, and a second big-endian 32-bit value for the size of the boot code. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + + for (i = 64; i < sizeof(buffer) - 32 - 4 * 3; i++) + { + if (memcmp(&buffer[i], "TARA IPARPVODED TA AEHDAREA RT I", 32) == 0) + { + byteswapped = 1; + offset = i + 32 + 4; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + break; + } + else if (memcmp(&buffer[i], "ATARI APPROVED DATA HEADER ATRI ", 32) == 0) + { + byteswapped = 0; + offset = i + 32 + 4; + size = (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 3]); + break; + } + } + + if (size == 0) /* did not see ATARI APPROVED DATA HEADER */ + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Jaguar CD"); + } + + i = 0; /* only loop once */ + do + { + md5_init(&md5); + + offset += 4; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Hashing boot executable (%u bytes starting at %u bytes into sector %u)", size, offset, sector); + rc_hash_verbose(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + do + { + if (byteswapped) + rc_hash_byteswap16(buffer, &buffer[sizeof(buffer)]); + + remaining = sizeof(buffer) - offset; + if (remaining >= size) + { + md5_append(&md5, &buffer[offset], size); + size = 0; + break; + } + + md5_append(&md5, &buffer[offset], remaining); + size -= remaining; + offset = 0; + } while (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer)) == sizeof(buffer)); + + rc_cd_close_track(track_handle); + + if (size > 0) + return rc_hash_error("Not enough data"); + + rc_hash_finalize(&md5, hash); + + /* homebrew games all seem to have the same boot executable and store the actual game code in track 2. + * if we generated something other than the homebrew hash, return it. assume all homebrews are byteswapped. */ + if (strcmp(hash, "254487b59ab21bc005338e85cbf9fd2f") != 0 || !byteswapped) { + if (_rc_hash_jaguar_cd_homebrew_hash == NULL || strcmp(hash, _rc_hash_jaguar_cd_homebrew_hash) != 0) + return 1; + } + + /* if we've already been through the loop a second time, just return the hash */ + if (i == 1) + return 1; + ++i; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Potential homebrew at sector %u, checking for KART data in track 2", sector); + rc_hash_verbose(message); + } + + track_handle = rc_cd_open_track(path, 2); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* track 2 of the homebrew code has the 64 bytes or ATRI followed by 32 bytes of "ATARI APPROVED DATA HEADER ATRI!", + * then 64 bytes of KART repeating. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + if (memcmp(&buffer[0x5E], "RT!IRTKA", 8) != 0) + return rc_hash_error("Homebrew executable not found in track 2"); + + /* found KART data*/ + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Found KART data in track 2"); + rc_hash_verbose(message); + } + + offset = 0xA6; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + } while (1); +} + +static int rc_hash_lynx(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + if (buffer[0] == 'L' && buffer[1] == 'Y' && buffer[2] == 'N' && buffer[3] == 'X' && buffer[4] == 0) + { + rc_hash_verbose("Ignoring LYNX header"); + + buffer += 64; + buffer_size -= 64; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_neogeo_cd(char hash[33], const char* path) +{ + char buffer[1024], *ptr; + void* track_handle; + uint32_t sector; + unsigned size; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* https://wiki.neogeodev.org/index.php?title=IPL_file, https://wiki.neogeodev.org/index.php?title=PRG_file + * IPL file specifies data to be loaded before the game starts. PRG files are the executable code + */ + sector = rc_cd_find_file_sector(track_handle, "IPL.TXT", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a NeoGeo CD game disc"); + } + + if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) == 0) + { + rc_cd_close_track(track_handle); + return 0; + } + + md5_init(&md5); + + buffer[sizeof(buffer) - 1] = '\0'; + ptr = &buffer[0]; + do + { + char* start = ptr; + while (*ptr && *ptr != '.') + ++ptr; + + if (strncasecmp(ptr, ".PRG", 4) == 0) + { + ptr += 4; + *ptr++ = '\0'; + + sector = rc_cd_find_file_sector(track_handle, start, &size); + if (!sector || !rc_hash_cd_file(&md5, track_handle, sector, NULL, size, start)) + { + char error[128]; + rc_cd_close_track(track_handle); + snprintf(error, sizeof(error), "Could not read %.16s", start); + return rc_hash_error(error); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr != '\n') + break; + ++ptr; + } while (*ptr != '\0' && *ptr != '\x1a'); + + rc_cd_close_track(track_handle); + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_nes(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + if (buffer[0] == 'N' && buffer[1] == 'E' && buffer[2] == 'S' && buffer[3] == 0x1A) + { + rc_hash_verbose("Ignoring NES header"); + + buffer += 16; + buffer_size -= 16; + } + else if (buffer[0] == 'F' && buffer[1] == 'D' && buffer[2] == 'S' && buffer[3] == 0x1A) + { + rc_hash_verbose("Ignoring FDS header"); + + buffer += 16; + buffer_size -= 16; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_n64(char hash[33], const char* path) +{ + uint8_t* buffer; + uint8_t* stop; + const size_t buffer_size = 65536; + md5_state_t md5; + size_t remaining; + void* file_handle; + int is_v64 = 0; + int is_n64 = 0; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + buffer = (uint8_t*)malloc(buffer_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + stop = buffer + buffer_size; + + /* read first byte so we can detect endianness */ + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, 1); + + if (buffer[0] == 0x80) /* z64 format (big endian [native]) */ + { + } + else if (buffer[0] == 0x37) /* v64 format (byteswapped) */ + { + rc_hash_verbose("converting v64 to z64"); + is_v64 = 1; + } + else if (buffer[0] == 0x40) /* n64 format (little endian) */ + { + rc_hash_verbose("converting n64 to z64"); + is_n64 = 1; + } + else if (buffer[0] == 0xE8 || buffer[0] == 0x22) /* ndd format (don't byteswap) */ + { + } + else + { + free(buffer); + rc_file_close(file_handle); + + rc_hash_verbose("Not a Nintendo 64 ROM"); + return 0; + } + + /* calculate total file size */ + rc_file_seek(file_handle, 0, SEEK_END); + remaining = (size_t)rc_file_tell(file_handle); + if (remaining > MAX_BUFFER_SIZE) + remaining = MAX_BUFFER_SIZE; + + if (verbose_message_callback) + { + char message[64]; + snprintf(message, sizeof(message), "Hashing %u bytes", (unsigned)remaining); + verbose_message_callback(message); + } + + /* begin hashing */ + md5_init(&md5); + + rc_file_seek(file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) + { + rc_file_read(file_handle, buffer, (int)buffer_size); + + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) + { + rc_file_read(file_handle, buffer, (int)remaining); + + stop = buffer + remaining; + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)remaining); + } + + /* cleanup */ + rc_file_close(file_handle); + free(buffer); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_nintendo_ds(char hash[33], const char* path) +{ + uint8_t header[512]; + uint8_t* hash_buffer; + unsigned int hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; + size_t num_read; + int64_t offset = 0; + md5_state_t md5; + void* file_handle; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_SET); + if (rc_file_read(file_handle, header, sizeof(header)) != 512) + return rc_hash_error("Failed to read header"); + + if (header[0] == 0x2E && header[1] == 0x00 && header[2] == 0x00 && header[3] == 0xEA && + header[0xB0] == 0x44 && header[0xB1] == 0x46 && header[0xB2] == 0x96 && header[0xB3] == 0) + { + /* SuperCard header detected, ignore it */ + rc_hash_verbose("Ignoring SuperCard header"); + + offset = 512; + rc_file_seek(file_handle, offset, SEEK_SET); + rc_file_read(file_handle, header, sizeof(header)); + } + + arm9_addr = header[0x20] | (header[0x21] << 8) | (header[0x22] << 16) | (header[0x23] << 24); + arm9_size = header[0x2C] | (header[0x2D] << 8) | (header[0x2E] << 16) | (header[0x2F] << 24); + arm7_addr = header[0x30] | (header[0x31] << 8) | (header[0x32] << 16) | (header[0x33] << 24); + arm7_size = header[0x3C] | (header[0x3D] << 8) | (header[0x3E] << 16) | (header[0x3F] << 24); + icon_addr = header[0x68] | (header[0x69] << 8) | (header[0x6A] << 16) | (header[0x6B] << 24); + + if (arm9_size + arm7_size > 16 * 1024 * 1024) + { + /* sanity check - code blocks are typically less than 1MB each - assume not a DS ROM */ + snprintf((char*)header, sizeof(header), "arm9 code size (%u) + arm7 code size (%u) exceeds 16MB", arm9_size, arm7_size); + return rc_hash_error((const char*)header); + } + + hash_size = 0xA00; + if (arm9_size > hash_size) + hash_size = arm9_size; + if (arm7_size > hash_size) + hash_size = arm7_size; + + hash_buffer = (uint8_t*)malloc(hash_size); + if (!hash_buffer) + { + rc_file_close(file_handle); + + snprintf((char*)header, sizeof(header), "Failed to allocate %u bytes", hash_size); + return rc_hash_error((const char*)header); + } + + md5_init(&md5); + + rc_hash_verbose("Hashing 352 byte header"); + md5_append(&md5, header, 0x160); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Hashing %u byte arm9 code (at %08X)", arm9_size, arm9_addr); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, arm9_addr + offset, SEEK_SET); + rc_file_read(file_handle, hash_buffer, arm9_size); + md5_append(&md5, hash_buffer, arm9_size); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Hashing %u byte arm7 code (at %08X)", arm7_size, arm7_addr); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, arm7_addr + offset, SEEK_SET); + rc_file_read(file_handle, hash_buffer, arm7_size); + md5_append(&md5, hash_buffer, arm7_size); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Hashing 2560 byte icon and labels data (at %08X)", icon_addr); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, icon_addr + offset, SEEK_SET); + num_read = rc_file_read(file_handle, hash_buffer, 0xA00); + if (num_read < 0xA00) + { + /* some homebrew games don't provide a full icon block, and no data after the icon block. + * if we didn't get a full icon block, fill the remaining portion with 0s + */ + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Warning: only got %u bytes for icon and labels data, 0-padding to 2560 bytes", (unsigned)num_read); + verbose_message_callback((const char*)header); + } + + memset(&hash_buffer[num_read], 0, 0xA00 - num_read); + } + md5_append(&md5, hash_buffer, 0xA00); + + free(hash_buffer); + rc_file_close(file_handle); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_gamecube(char hash[33], const char* path) +{ + md5_state_t md5; + void* file_handle; + const uint32_t BASE_HEADER_SIZE = 0x2440; + const uint32_t MAX_HEADER_SIZE = 1024 * 1024; + uint32_t apploader_header_size, apploader_body_size, apploader_trailer_size, header_size; + uint8_t quad_buffer[4]; + uint8_t addr_buffer[0xD8]; + uint8_t* buffer; + uint32_t dol_offset; + uint32_t dol_offsets[18]; + uint32_t dol_sizes[18]; + uint32_t dol_buf_size = 0; + uint32_t ix; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + /* Verify Gamecube */ + rc_file_seek(file_handle, 0x1c, SEEK_SET); + rc_file_read(file_handle, quad_buffer, 4); + if (quad_buffer[0] != 0xC2|| quad_buffer[1] != 0x33 || quad_buffer[2] != 0x9F || quad_buffer[3] != 0x3D) + { + rc_file_close(file_handle); + return rc_hash_error("Not a Gamecube disc"); + } + + /* GetApploaderSize */ + rc_file_seek(file_handle, BASE_HEADER_SIZE + 0x14, SEEK_SET); + apploader_header_size = 0x20; + rc_file_read(file_handle, quad_buffer, 4); + apploader_body_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(file_handle, quad_buffer, 4); + apploader_trailer_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + header_size = BASE_HEADER_SIZE + apploader_header_size + apploader_body_size + apploader_trailer_size; + if (header_size > MAX_HEADER_SIZE) header_size = MAX_HEADER_SIZE; + + /* Hash headers */ + buffer = (uint8_t*)malloc(header_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, header_size); + md5_init(&md5); + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %u byte header", header_size); + verbose_message_callback(message); + } + md5_append(&md5, buffer, header_size); + + /* GetBootDOLOffset + * Base header size is guaranteed larger than 0x423 therefore buffer contains dol_offset right now + */ + dol_offset = (buffer[0x420] << 24) | (buffer[0x421] << 16) | (buffer[0x422] << 8) | buffer[0x423]; + free(buffer); + + /* Find offsetsand sizes for the 7 main.dol code segments and 11 main.dol data segments */ + rc_file_seek(file_handle, dol_offset, SEEK_SET); + rc_file_read(file_handle, addr_buffer, 0xD8); + for (ix = 0; ix < 18; ix++) + { + dol_offsets[ix] = + (addr_buffer[0x0 + ix * 4] << 24) | + (addr_buffer[0x1 + ix * 4] << 16) | + (addr_buffer[0x2 + ix * 4] << 8) | + addr_buffer[0x3 + ix * 4]; + dol_sizes[ix] = + (addr_buffer[0x90 + ix * 4] << 24) | + (addr_buffer[0x91 + ix * 4] << 16) | + (addr_buffer[0x92 + ix * 4] << 8) | + addr_buffer[0x93 + ix * 4]; + dol_buf_size = (dol_sizes[ix] > dol_buf_size) ? dol_sizes[ix] : dol_buf_size; + } + + /* Iterate through the 18 main.dol segments and hash each */ + buffer = (uint8_t*)malloc(dol_buf_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + for (ix = 0; ix < 18; ix++) + { + if (dol_sizes[ix] == 0) + continue; + rc_file_seek(file_handle, dol_offsets[ix], SEEK_SET); + rc_file_read(file_handle, buffer, dol_sizes[ix]); + if (verbose_message_callback) + { + char message[128]; + if (ix < 7) + snprintf(message, sizeof(message), "Hashing %u byte main.dol code segment %u", dol_sizes[ix], ix); + else + snprintf(message, sizeof(message), "Hashing %u byte main.dol data segment %u", dol_sizes[ix], ix - 7); + verbose_message_callback(message); + } + md5_append(&md5, buffer, dol_sizes[ix]); + } + + /* Finalize */ + rc_file_close(file_handle); + free(buffer); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_pce(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it (expect ROM data to be multiple of 128KB) */ + uint32_t calc_size = ((uint32_t)buffer_size / 0x20000) * 0x20000; + if (buffer_size - calc_size == 512) + { + rc_hash_verbose("Ignoring PCE header"); + + buffer += 512; + buffer_size -= 512; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +static int rc_hash_pce_track(char hash[33], void* track_handle) +{ + uint8_t buffer[2048]; + md5_state_t md5; + 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 + */ + if (rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 1, buffer, 128) < 128) + { + return rc_hash_error("Not a PC Engine CD"); + } + + /* normal PC Engine CD will have a header block in sector 1 */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) + { + /* the title of the disc is the last 22 bytes of the header */ + md5_init(&md5); + md5_append(&md5, &buffer[106], 22); + + if (verbose_message_callback) + { + char message[128]; + buffer[128] = '\0'; + snprintf(message, sizeof(message), "Found PC Engine CD, title=%.22s", &buffer[106]); + verbose_message_callback(message); + } + + /* the first three bytes specify the sector of the program data, and the fourth byte + * is the number of sectors. + */ + sector = (buffer[0] << 16) + (buffer[1] << 8) + buffer[2]; + num_sectors = buffer[3]; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %d sectors starting at sector %d", num_sectors, sector); + verbose_message_callback(message); + } + + sector += rc_cd_first_track_sector(track_handle); + while (num_sectors > 0) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + /* GameExpress CDs use a standard Joliet filesystem - locate and hash the BOOT.BIN */ + else if ((sector = rc_cd_find_file_sector(track_handle, "BOOT.BIN", &size)) != 0 && size < MAX_BUFFER_SIZE) + { + md5_init(&md5); + while (size > sizeof(buffer)) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= sizeof(buffer); + } + + if (size > 0) + { + rc_cd_read_sector(track_handle, sector, buffer, size); + md5_append(&md5, buffer, size); + } + } + else + { + return rc_hash_error("Not a PC Engine CD"); + } + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_pce_cd(char hash[33], const char* path) +{ + int result; + void* track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_error("Could not open track"); + + result = rc_hash_pce_track(hash, track_handle); + + rc_cd_close_track(track_handle); + + return result; +} + +static int rc_hash_pcfx_cd(char hash[33], const char* path) +{ + uint8_t buffer[2048]; + void* track_handle; + md5_state_t md5; + int sector, num_sectors; + + /* PC-FX executable can be in any track. Assume it's in the largest data track and check there first */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LARGEST); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* PC-FX CD will have a header marker in sector 0 */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, 32); + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) != 0) + { + rc_cd_close_track(track_handle); + + /* not found in the largest data track, check track 2 */ + track_handle = rc_cd_open_track(path, 2); + if (!track_handle) + return rc_hash_error("Could not open track"); + + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, 32); + } + + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) == 0) + { + /* PC-FX boot header fills the first two sectors of the disc + * https://bitbucket.org/trap15/pcfxtools/src/master/pcfx-cdlink.c + * the important stuff is the first 128 bytes of the second sector (title being the first 32) */ + rc_cd_read_sector(track_handle, sector + 1, buffer, 128); + + md5_init(&md5); + md5_append(&md5, buffer, 128); + + if (verbose_message_callback) + { + char message[128]; + buffer[128] = '\0'; + snprintf(message, sizeof(message), "Found PC-FX CD, title=%.32s", &buffer[0]); + verbose_message_callback(message); + } + + /* the program sector is in bytes 33-36 (assume byte 36 is 0) */ + sector = (buffer[34] << 16) + (buffer[33] << 8) + buffer[32]; + + /* the number of sectors the program occupies is in bytes 37-40 (assume byte 40 is 0) */ + num_sectors = (buffer[38] << 16) + (buffer[37] << 8) + buffer[36]; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %d sectors starting at sector %d", num_sectors, sector); + verbose_message_callback(message); + } + + sector += rc_cd_first_track_sector(track_handle); + while (num_sectors > 0) + { + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + else + { + int result = 0; + rc_cd_read_sector(track_handle, sector + 1, buffer, 128); + + /* some PC-FX CDs still identify as PCE CDs */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) + result = rc_hash_pce_track(hash, track_handle); + + rc_cd_close_track(track_handle); + if (result) + return result; + + return rc_hash_error("Not a PC-FX CD"); + } + + rc_cd_close_track(track_handle); + + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_dreamcast(char hash[33], const char* path) +{ + uint8_t buffer[256] = ""; + void* track_handle; + char exe_file[32] = ""; + unsigned size; + uint32_t sector; + int result = 0; + md5_state_t md5; + int i = 0; + + /* track 03 is the data track that contains the TOC and IP.BIN */ + track_handle = rc_cd_open_track(path, 3); + if (track_handle) + { + /* first 256 bytes from first sector should have IP.BIN structure that stores game meta information + * https://mc.pp.se/dc/ip.bin.html */ + rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle), buffer, sizeof(buffer)); + } + + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) + { + if (track_handle) + rc_cd_close_track(track_handle); + + /* not a gd-rom dreamcast file. check for mil-cd by looking for the marker in the first data track */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_error("Could not open track"); + + rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle), buffer, sizeof(buffer)); + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) + { + /* did not find marker on track 3 or first data track */ + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Dreamcast CD"); + } + } + + /* start the hash with the game meta information */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)buffer, 256); + + if (verbose_message_callback) + { + char message[256]; + uint8_t* ptr = &buffer[0xFF]; + while (ptr > &buffer[0x80] && ptr[-1] == ' ') + --ptr; + *ptr = '\0'; + + snprintf(message, sizeof(message), "Found Dreamcast CD: %.128s (%.16s)", (const char*)&buffer[0x80], (const char*)&buffer[0x40]); + verbose_message_callback(message); + } + + /* the boot filename is 96 bytes into the meta information (https://mc.pp.se/dc/ip0000.bin.html) */ + /* remove whitespace from bootfile */ + i = 0; + while (!isspace((unsigned char)buffer[96 + i]) && i < 16) + ++i; + + /* sometimes boot file isn't present on meta information. + * nothing can be done, as even the core doesn't run the game in this case. */ + if (i == 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Boot executable not specified on IP.BIN"); + } + + memcpy(exe_file, &buffer[96], i); + exe_file[i] = '\0'; + + sector = rc_cd_find_file_sector(track_handle, exe_file, &size); + if (sector == 0) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Could not locate boot executable"); + } + + if (rc_cd_read_sector(track_handle, sector, buffer, 1)) + { + /* the boot executable is in the primary data track */ + } + else + { + rc_cd_close_track(track_handle); + + /* the boot executable is normally in the last track */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LAST); + } + + result = rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "boot executable"); + rc_cd_close_track(track_handle); + + rc_hash_finalize(&md5, hash); + return result; +} + +static int rc_hash_find_playstation_executable(void* track_handle, const char* boot_key, const char* cdrom_prefix, + char exe_name[], unsigned exe_name_size, unsigned* exe_size) +{ + uint8_t buffer[2048]; + unsigned size; + char* ptr; + char* start; + const size_t boot_key_len = strlen(boot_key); + const size_t cdrom_prefix_len = strlen(cdrom_prefix); + int sector; + + sector = rc_cd_find_file_sector(track_handle, "SYSTEM.CNF", NULL); + if (!sector) + return 0; + + size = (unsigned)rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer) - 1); + buffer[size] = '\0'; + + sector = 0; + for (ptr = (char*)buffer; *ptr; ++ptr) + { + if (strncmp(ptr, boot_key, boot_key_len) == 0) + { + ptr += boot_key_len; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (*ptr == '=') + { + ++ptr; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) + ptr += cdrom_prefix_len; + while (*ptr == '\\') + ++ptr; + + start = ptr; + while (!isspace((unsigned char)*ptr) && *ptr != ';') + ++ptr; + + size = (unsigned)(ptr - start); + if (size >= exe_name_size) + size = exe_name_size - 1; + + memcpy(exe_name, start, size); + exe_name[size] = '\0'; + + if (verbose_message_callback) + { + snprintf((char*)buffer, sizeof(buffer), "Looking for boot executable: %s", exe_name); + verbose_message_callback((const char*)buffer); + } + + sector = rc_cd_find_file_sector(track_handle, exe_name, exe_size); + break; + } + } + + /* advance to end of line */ + while (*ptr && *ptr != '\n') + ++ptr; + } + + return sector; +} + +static int rc_hash_psx(char hash[33], const char* path) +{ + uint8_t buffer[32]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + unsigned size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + sector = rc_hash_find_playstation_executable(track_handle, "BOOT", "cdrom:", exe_name, sizeof(exe_name), &size); + if (!sector) + { + sector = rc_cd_find_file_sector(track_handle, "PSX.EXE", &size); + if (sector) + memcpy(exe_name, "PSX.EXE", 8); + } + + if (!sector) + { + rc_hash_error("Could not locate primary executable"); + } + else if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) + { + rc_hash_error("Could not read primary executable"); + } + else + { + if (memcmp(buffer, "PS-X EXE", 7) != 0) + { + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "%s did not contain PS-X EXE marker", exe_name); + verbose_message_callback(message); + } + } + else + { + /* the PS-X EXE header specifies the executable size as a 4-byte value 28 bytes into the header, which doesn't + * include the header itself. We want to include the header in the hash, so append another 2048 to that value. + */ + size = (((uint8_t)buffer[31] << 24) | ((uint8_t)buffer[30] << 16) | ((uint8_t)buffer[29] << 8) | (uint8_t)buffer[28]) + 2048; + } + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(&md5, hash); + } + + rc_cd_close_track(track_handle); + + return result; +} + +static int rc_hash_ps2(char hash[33], const char* path) +{ + uint8_t buffer[4]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + unsigned size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + sector = rc_hash_find_playstation_executable(track_handle, "BOOT2", "cdrom0:", exe_name, sizeof(exe_name), &size); + if (!sector) + { + rc_hash_error("Could not locate primary executable"); + } + else if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) + { + rc_hash_error("Could not read primary executable"); + } + else + { + if (memcmp(buffer, "\x7f\x45\x4c\x46", 4) != 0) + { + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "%s did not contain ELF marker", exe_name); + verbose_message_callback(message); + } + } + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(&md5, hash); + } + + rc_cd_close_track(track_handle); + + return result; +} + +static int rc_hash_psp(char hash[33], const char* path) +{ + void* track_handle; + uint32_t sector; + unsigned size; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* http://www.romhacking.net/forum/index.php?topic=30899.0 + * PSP_GAME/PARAM.SFO contains key/value pairs identifying the game for the system (i.e. serial number, + * name, version). PSP_GAME/SYSDIR/EBOOT.BIN is the encrypted primary executable. + */ + sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\PARAM.SFO", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a PSP game disc"); + } + + md5_init(&md5); + if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\PARAM.SFO")) + { + rc_cd_close_track(track_handle); + return 0; + } + + sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\SYSDIR\\EBOOT.BIN", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Could not find primary executable"); + } + + if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\SYSDIR\\EBOOT.BIN")) + { + rc_cd_close_track(track_handle); + return 0; + } + + rc_cd_close_track(track_handle); + return rc_hash_finalize(&md5, hash); +} + +static int rc_hash_sega_cd(char hash[33], const char* path) +{ + uint8_t buffer[512]; + void* track_handle; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* the first 512 bytes of sector 0 are a volume header and ROM header that uniquely identify the game. + * After that is an arbitrary amount of code that ensures the game is being run in the correct region. + * Then more arbitrary code follows that actually starts the boot process. Somewhere in there, the + * primary executable is loaded. In many cases, a single game will have multiple executables, so even + * if we could determine the primary one, it's just the tip of the iceberg. As such, we've decided that + * hashing the volume and ROM headers is sufficient for identifying the game, and we'll have to trust + * that our players aren't modifying anything else on the disc. + */ + rc_cd_read_sector(track_handle, 0, buffer, sizeof(buffer)); + rc_cd_close_track(track_handle); + + if (memcmp(buffer, "SEGADISCSYSTEM ", 16) != 0 && /* Sega CD */ + memcmp(buffer, "SEGA SEGASATURN ", 16) != 0) /* Sega Saturn */ + { + return rc_hash_error("Not a Sega CD"); + } + + return rc_hash_buffer(hash, buffer, sizeof(buffer)); +} + +static int rc_hash_snes(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + uint32_t calc_size = ((uint32_t)buffer_size / 0x2000) * 0x2000; + if (buffer_size - calc_size == 512) + { + rc_hash_verbose("Ignoring SNES header"); + + buffer += 512; + buffer_size -= 512; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + +struct rc_buffered_file +{ + const uint8_t* read_ptr; + const uint8_t* data; + size_t data_size; +}; + +static struct rc_buffered_file rc_buffered_file; + +static void* rc_file_open_buffered_file(const char* path) +{ + struct rc_buffered_file* handle = (struct rc_buffered_file*)malloc(sizeof(struct rc_buffered_file)); + (void)path; + + if (handle) + memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + + return handle; +} + +void rc_file_seek_buffered_file(void* file_handle, int64_t offset, int origin) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + switch (origin) + { + case SEEK_SET: buffered_file->read_ptr = buffered_file->data + offset; break; + case SEEK_CUR: buffered_file->read_ptr += offset; break; + case SEEK_END: buffered_file->read_ptr = buffered_file->data + buffered_file->data_size - offset; break; + } + + if (buffered_file->read_ptr < buffered_file->data) + buffered_file->read_ptr = buffered_file->data; + else if (buffered_file->read_ptr > buffered_file->data + buffered_file->data_size) + buffered_file->read_ptr = buffered_file->data + buffered_file->data_size; +} + +int64_t rc_file_tell_buffered_file(void* file_handle) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + return (buffered_file->read_ptr - buffered_file->data); +} + +size_t rc_file_read_buffered_file(void* file_handle, void* buffer, size_t requested_bytes) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + const int64_t remaining = buffered_file->data_size - (buffered_file->read_ptr - buffered_file->data); + if ((int)requested_bytes > remaining) + requested_bytes = (int)remaining; + + memcpy(buffer, buffered_file->read_ptr, requested_bytes); + buffered_file->read_ptr += requested_bytes; + return requested_bytes; +} + +void rc_file_close_buffered_file(void* file_handle) +{ + free(file_handle); +} + +static int rc_hash_file_from_buffer(char hash[33], int console_id, const uint8_t* buffer, size_t buffer_size) +{ + struct rc_hash_filereader buffered_filereader_funcs; + struct rc_hash_filereader* old_filereader = filereader; + int result; + + memset(&buffered_filereader_funcs, 0, sizeof(buffered_filereader_funcs)); + buffered_filereader_funcs.open = rc_file_open_buffered_file; + buffered_filereader_funcs.close = rc_file_close_buffered_file; + buffered_filereader_funcs.read = rc_file_read_buffered_file; + buffered_filereader_funcs.seek = rc_file_seek_buffered_file; + buffered_filereader_funcs.tell = rc_file_tell_buffered_file; + filereader = &buffered_filereader_funcs; + + rc_buffered_file.data = rc_buffered_file.read_ptr = buffer; + rc_buffered_file.data_size = buffer_size; + + result = rc_hash_generate_from_file(hash, console_id, "[buffered file]"); + + filereader = old_filereader; + return result; +} + +int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* buffer, size_t buffer_size) +{ + switch (console_id) + { + default: + { + char message[128]; + snprintf(message, sizeof(message), "Unsupported console for buffer hash: %d", console_id); + return rc_hash_error(message); + } + + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_ARCADIA_2001: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_ADVANCE: + case RC_CONSOLE_GAMEBOY_COLOR: + case RC_CONSOLE_GAME_GEAR: + case RC_CONSOLE_INTELLIVISION: + case RC_CONSOLE_INTERTON_VC_4000: + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_MSX: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_PC8800: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: + case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: + case RC_CONSOLE_WONDERSWAN: + return rc_hash_buffer(hash, buffer, buffer_size); + + case RC_CONSOLE_ARDUBOY: + /* https://en.wikipedia.org/wiki/Intel_HEX */ + return rc_hash_text(hash, buffer, buffer_size); + + case RC_CONSOLE_ATARI_7800: + return rc_hash_7800(hash, buffer, buffer_size); + + case RC_CONSOLE_ATARI_LYNX: + return rc_hash_lynx(hash, buffer, buffer_size); + + case RC_CONSOLE_NINTENDO: + return rc_hash_nes(hash, buffer, buffer_size); + + case RC_CONSOLE_PC_ENGINE: /* NOTE: does not support PCEngine CD */ + return rc_hash_pce(hash, buffer, buffer_size); + + case RC_CONSOLE_SUPER_NINTENDO: + return rc_hash_snes(hash, buffer, buffer_size); + + case RC_CONSOLE_NINTENDO_64: + case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: + return rc_hash_file_from_buffer(hash, console_id, buffer, buffer_size); + } +} + +static int rc_hash_whole_file(char hash[33], const char* path) +{ + md5_state_t md5; + uint8_t* buffer; + int64_t size; + const size_t buffer_size = 65536; + void* file_handle; + size_t remaining; + int result = 0; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_END); + size = rc_file_tell(file_handle); + + if (verbose_message_callback) + { + char message[1024]; + if (size > MAX_BUFFER_SIZE) + snprintf(message, sizeof(message), "Hashing first %u bytes (of %u bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(path)); + else + snprintf(message, sizeof(message), "Hashing %s (%u bytes)", rc_path_get_filename(path), (unsigned)size); + verbose_message_callback(message); + } + + if (size > MAX_BUFFER_SIZE) + remaining = MAX_BUFFER_SIZE; + else + remaining = (size_t)size; + + md5_init(&md5); + + buffer = (uint8_t*)malloc(buffer_size); + if (buffer) + { + rc_file_seek(file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) + { + rc_file_read(file_handle, buffer, (int)buffer_size); + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) + { + rc_file_read(file_handle, buffer, (int)remaining); + md5_append(&md5, buffer, (int)remaining); + } + + free(buffer); + result = rc_hash_finalize(&md5, hash); + } + + rc_file_close(file_handle); + return result; +} + +static int rc_hash_buffered_file(char hash[33], int console_id, const char* path) +{ + uint8_t* buffer; + int64_t size; + int result = 0; + void* file_handle; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_END); + size = rc_file_tell(file_handle); + + if (verbose_message_callback) + { + char message[1024]; + if (size > MAX_BUFFER_SIZE) + snprintf(message, sizeof(message), "Buffering first %u bytes (of %d bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(path)); + else + snprintf(message, sizeof(message), "Buffering %s (%d bytes)", rc_path_get_filename(path), (unsigned)size); + verbose_message_callback(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + buffer = (uint8_t*)malloc((size_t)size); + if (buffer) + { + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, (int)size); + + result = rc_hash_generate_from_buffer(hash, console_id, buffer, (size_t)size); + + free(buffer); + } + + rc_file_close(file_handle); + return result; +} + +static int rc_hash_path_is_absolute(const char* path) +{ + if (!path[0]) + return 0; + + /* "/path/to/file" or "\path\to\file" */ + if (path[0] == '/' || path[0] == '\\') + return 1; + + /* "C:\path\to\file" */ + if (path[1] == ':' && path[2] == '\\') + return 1; + + /* "scheme:/path/to/file" */ + while (*path) + { + if (path[0] == ':' && path[1] == '/') + return 1; + + ++path; + } + + return 0; +} + +static const char* rc_hash_get_first_item_from_playlist(const char* path) +{ + char buffer[1024]; + char* disc_path; + char* ptr, *start, *next; + size_t num_read, path_len, file_len; + void* file_handle; + + file_handle = rc_file_open(path); + if (!file_handle) + { + rc_hash_error("Could not open playlist"); + return NULL; + } + + num_read = rc_file_read(file_handle, buffer, sizeof(buffer) - 1); + buffer[num_read] = '\0'; + + rc_file_close(file_handle); + + ptr = start = buffer; + do + { + /* ignore empty and commented lines */ + while (*ptr == '#' || *ptr == '\r' || *ptr == '\n') + { + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr) + ++ptr; + } + + /* find and extract the current line */ + start = ptr; + while (*ptr && *ptr != '\n') + ++ptr; + next = ptr; + + /* remove trailing whitespace - especially '\r' */ + while (ptr > start && isspace((unsigned char)ptr[-1])) + --ptr; + + /* if we found a non-empty line, break out of the loop to process it */ + file_len = ptr - start; + if (file_len) + break; + + /* did we reach the end of the file? */ + if (!*next) + return NULL; + + /* if the line only contained whitespace, keep searching */ + ptr = next + 1; + } while (1); + + if (verbose_message_callback) + { + char message[1024]; + snprintf(message, sizeof(message), "Extracted %.*s from playlist", (int)file_len, start); + verbose_message_callback(message); + } + + start[file_len++] = '\0'; + if (rc_hash_path_is_absolute(start)) + path_len = 0; + else + path_len = rc_path_get_filename(path) - path; + + disc_path = (char*)malloc(path_len + file_len + 1); + if (!disc_path) + return NULL; + + if (path_len) + memcpy(disc_path, path, path_len); + + memcpy(&disc_path[path_len], start, file_len); + return disc_path; +} + +static int rc_hash_generate_from_playlist(char hash[33], 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_ARCADIA_2001: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_ADVANCE: + case RC_CONSOLE_GAMEBOY_COLOR: + case RC_CONSOLE_GAME_GEAR: + case RC_CONSOLE_INTELLIVISION: + case RC_CONSOLE_INTERTON_VC_4000: + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: + case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: + case RC_CONSOLE_WONDERSWAN: + /* generic whole-file hash - don't buffer */ + return rc_hash_whole_file(hash, path); + + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_MSX: + case RC_CONSOLE_PC8800: + /* generic whole-file hash with m3u support - don't buffer */ + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_whole_file(hash, path); + + case RC_CONSOLE_ARDUBOY: + case RC_CONSOLE_ATARI_7800: + case RC_CONSOLE_ATARI_LYNX: + case RC_CONSOLE_NINTENDO: + case RC_CONSOLE_PC_ENGINE: + case RC_CONSOLE_SUPER_NINTENDO: + /* additional logic whole-file hash - buffer then call rc_hash_generate_from_buffer */ + return rc_hash_buffered_file(hash, console_id, path); + + case RC_CONSOLE_3DO: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_3do(hash, path); + + case RC_CONSOLE_ARCADE: + return rc_hash_arcade(hash, path); + + case RC_CONSOLE_ATARI_JAGUAR_CD: + return rc_hash_jaguar_cd(hash, path); + + case RC_CONSOLE_DREAMCAST: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_dreamcast(hash, path); + + case RC_CONSOLE_GAMECUBE: + return rc_hash_gamecube(hash, path); + + case RC_CONSOLE_NEO_GEO_CD: + return rc_hash_neogeo_cd(hash, path); + + case RC_CONSOLE_NINTENDO_64: + return rc_hash_n64(hash, path); + + case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: + return rc_hash_nintendo_ds(hash, path); + + case RC_CONSOLE_PC_ENGINE_CD: + if (rc_path_compare_extension(path, "cue") || rc_path_compare_extension(path, "chd")) + return rc_hash_pce_cd(hash, path); + + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_buffered_file(hash, console_id, path); + + case RC_CONSOLE_PCFX: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_pcfx_cd(hash, path); + + case RC_CONSOLE_PLAYSTATION: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_psx(hash, path); + + case RC_CONSOLE_PLAYSTATION_2: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_ps2(hash, path); + + case RC_CONSOLE_PSP: + return rc_hash_psp(hash, path); + + case RC_CONSOLE_SEGA_CD: + case RC_CONSOLE_SATURN: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_sega_cd(hash, path); + } +} + +static void rc_hash_iterator_append_console(struct rc_hash_iterator* iterator, uint8_t console_id) +{ + int i = 0; + while (iterator->consoles[i] != 0) + { + if (iterator->consoles[i] == console_id) + return; + + ++i; + } + + iterator->consoles[i] = console_id; +} + +static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, const char* path) +{ + size_t size = iterator->buffer_size; + if (size == 0) + { + /* attempt to use disk size to determine system */ + void* file = rc_file_open(path); + if (file) + { + rc_file_seek(file, 0, SEEK_END); + size = (size_t)rc_file_tell(file); + rc_file_close(file); + } + } + + if (size == 512 * 9 * 80) /* 360KB */ + { + /* FAT-12 3.5" DD (512 byte sectors, 9 sectors per track, 80 tracks per side */ + /* FAT-12 5.25" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (size == 512 * 9 * 80 * 2) /* 720KB */ + { + /* FAT-12 3.5" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (size == 512 * 9 * 40) /* 180KB */ + { + /* FAT-12 5.25" DD (512 byte sectors, 9 sectors per track, 40 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + + /* AMSDOS 3" - 40 tracks */ + iterator->consoles[1] = RC_CONSOLE_AMSTRAD_PC; + } + else if (size == 256 * 16 * 35) /* 140KB */ + { + /* Apple II new format - 256 byte sectors, 16 sectors per track, 35 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + else if (size == 256 * 13 * 35) /* 113.75KB */ + { + /* Apple II old format - 256 byte sectors, 13 sectors per track, 35 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + + /* once a best guess has been identified, make sure the others are added as fallbacks */ + + /* check MSX first, as Apple II isn't supported by RetroArch, and RAppleWin won't use the iterator */ + rc_hash_iterator_append_console(iterator, RC_CONSOLE_MSX); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_AMSTRAD_PC); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); +} + +void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) +{ + int need_path = !buffer; + + memset(iterator, 0, sizeof(*iterator)); + iterator->buffer = buffer; + iterator->buffer_size = buffer_size; + + iterator->consoles[0] = 0; + + do + { + const char* ext = rc_path_get_extension(path); + switch (tolower(*ext)) + { + case '2': + if (rc_path_compare_extension(ext, "2d")) + { + iterator->consoles[0] = RC_CONSOLE_SHARPX1; + } + break; + + case '7': + if (rc_path_compare_extension(ext, "7z")) + { + /* decompressing zip file not supported */ + iterator->consoles[0] = RC_CONSOLE_ARCADE; + need_path = 1; + } + break; + + case '8': + /* http://tibasicdev.wikidot.com/file-extensions */ + if (rc_path_compare_extension(ext, "83g") || + rc_path_compare_extension(ext, "83p")) + { + iterator->consoles[0] = RC_CONSOLE_TI83; + } + break; + + case 'a': + if (rc_path_compare_extension(ext, "a78")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_7800; + } + break; + + case 'b': + if (rc_path_compare_extension(ext, "bin")) + { + if (buffer_size == 0) + { + /* raw bin file may be a CD track. if it's more than 32MB, try a CD hash. */ + void* file = rc_file_open(path); + if (file) + { + int64_t size; + + rc_file_seek(file, 0, SEEK_END); + size = rc_file_tell(file); + rc_file_close(file); + + if (size > 32 * 1024 * 1024) + { + iterator->consoles[0] = RC_CONSOLE_3DO; /* 4DO supports directly opening the bin file */ + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION; /* PCSX ReARMed supports directly opening the bin file*/ + iterator->consoles[2] = RC_CONSOLE_PLAYSTATION_2; /* PCSX2 supports directly opening the bin file*/ + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* Genesis Plus GX supports directly opening the bin file*/ + + /* fallback to megadrive which just does a full hash. */ + iterator->consoles[4] = RC_CONSOLE_MEGA_DRIVE; + break; + } + } + } + + /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, + * Fairchild Channel F, Arcadia 2001, and Interton VC 4000. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; + } + else if (rc_path_compare_extension(ext, "bs")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; + } + break; + + case 'c': + if (rc_path_compare_extension(ext, "cue")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[2] = RC_CONSOLE_DREAMCAST; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[5] = RC_CONSOLE_3DO; + iterator->consoles[6] = RC_CONSOLE_PCFX; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_ATARI_JAGUAR_CD; + need_path = 1; + } + else if (rc_path_compare_extension(ext, "chd")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[2] = RC_CONSOLE_DREAMCAST; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[5] = RC_CONSOLE_3DO; + iterator->consoles[6] = RC_CONSOLE_PCFX; + need_path = 1; + } + else if (rc_path_compare_extension(ext, "col")) + { + iterator->consoles[0] = RC_CONSOLE_COLECOVISION; + } + else if (rc_path_compare_extension(ext, "cas")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (rc_path_compare_extension(ext, "chf")) + { + iterator->consoles[0] = RC_CONSOLE_FAIRCHILD_CHANNEL_F; + } + break; + + case 'd': + if (rc_path_compare_extension(ext, "dsk")) + { + rc_hash_initialize_dsk_iterator(iterator, path); + } + else if (rc_path_compare_extension(ext, "d64")) + { + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + } + else if (rc_path_compare_extension(ext, "d88")) + { + iterator->consoles[0] = RC_CONSOLE_PC8800; + iterator->consoles[1] = RC_CONSOLE_SHARPX1; + } + break; + + case 'f': + if (rc_path_compare_extension(ext, "fig")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; + } + else if (rc_path_compare_extension(ext, "fds")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO; + } + else if (rc_path_compare_extension(ext, "fd")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + } + break; + + case 'g': + if (rc_path_compare_extension(ext, "gba")) + { + iterator->consoles[0] = RC_CONSOLE_GAMEBOY_ADVANCE; + } + else if (rc_path_compare_extension(ext, "gbc")) + { + iterator->consoles[0] = RC_CONSOLE_GAMEBOY_COLOR; + } + else if (rc_path_compare_extension(ext, "gb")) + { + iterator->consoles[0] = RC_CONSOLE_GAMEBOY; + } + else if (rc_path_compare_extension(ext, "gg")) + { + iterator->consoles[0] = RC_CONSOLE_GAME_GEAR; + } + else if (rc_path_compare_extension(ext, "gdi")) + { + iterator->consoles[0] = RC_CONSOLE_DREAMCAST; + } + break; + + case 'h': + if (rc_path_compare_extension(ext, "hex")) + { + iterator->consoles[0] = RC_CONSOLE_ARDUBOY; + } + break; + + case 'i': + if (rc_path_compare_extension(ext, "iso")) + { + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[1] = RC_CONSOLE_PSP; + iterator->consoles[2] = RC_CONSOLE_3DO; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + need_path = 1; + } + break; + + case 'j': + if (rc_path_compare_extension(ext, "jag")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_JAGUAR; + } + break; + + case 'k': + if (rc_path_compare_extension(ext, "k7")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* tape */ + } + break; + + case 'l': + if (rc_path_compare_extension(ext, "lnx")) + { + iterator->consoles[0] = RC_CONSOLE_ATARI_LYNX; + } + break; + + case 'm': + if (rc_path_compare_extension(ext, "m3u")) + { + const char* disc_path = rc_hash_get_first_item_from_playlist(path); + if (!disc_path) /* did not find a disc */ + return; + + iterator->buffer = NULL; /* ignore buffer; assume it's the m3u contents */ + + path = iterator->path = disc_path; + continue; /* retry with disc_path */ + } + else if (rc_path_compare_extension(ext, "md")) + { + iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; + } + else if (rc_path_compare_extension(ext, "min")) + { + iterator->consoles[0] = RC_CONSOLE_POKEMON_MINI; + } + else if (rc_path_compare_extension(ext, "mx1")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (rc_path_compare_extension(ext, "mx2")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (rc_path_compare_extension(ext, "m5")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* cartridge */ + } + else if (rc_path_compare_extension(ext, "m7")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* cartridge */ + } + break; + + case 'n': + if (rc_path_compare_extension(ext, "nes")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO; + } + else if (rc_path_compare_extension(ext, "nds")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_DS; /* ASSERT: handles both DS and DSi */ + } + else if (rc_path_compare_extension(ext, "n64") || + rc_path_compare_extension(ext, "ndd")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; + } + else if (rc_path_compare_extension(ext, "ngc")) + { + iterator->consoles[0] = RC_CONSOLE_NEOGEO_POCKET; + } + else if (rc_path_compare_extension(ext, "nib")) + { + /* also Apple II, but both are full-file hashes */ + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + } + break; + + case 'p': + if (rc_path_compare_extension(ext, "pce")) + { + iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; + } + else if (rc_path_compare_extension(ext, "pgm")) + { + iterator->consoles[0] = RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER; + } + break; + + case 'r': + if (rc_path_compare_extension(ext, "rom")) + { + /* rom is associated with MSX, Thomson TO-8, and Fairchild Channel F. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + if (rc_path_compare_extension(ext, "ri")) + { + iterator->consoles[0] = RC_CONSOLE_MSX; + } + break; + + case 's': + if (rc_path_compare_extension(ext, "smc") || + rc_path_compare_extension(ext, "sfc") || + rc_path_compare_extension(ext, "swc")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; + } + else if (rc_path_compare_extension(ext, "sg")) + { + iterator->consoles[0] = RC_CONSOLE_SG1000; + } + else if (rc_path_compare_extension(ext, "sgx")) + { + iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; + } + else if (rc_path_compare_extension(ext, "sv")) + { + iterator->consoles[0] = RC_CONSOLE_SUPERVISION; + } + else if (rc_path_compare_extension(ext, "sap")) + { + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + } + break; + + case 't': + if (rc_path_compare_extension(ext, "tap")) + { + iterator->consoles[0] = RC_CONSOLE_ORIC; + } + else if (rc_path_compare_extension(ext, "tic")) + { + iterator->consoles[0] = RC_CONSOLE_TIC80; + } + else if (rc_path_compare_extension(ext, "tvc")) + { + iterator->consoles[0] = RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER; + } + break; + + case 'u': + if (rc_path_compare_extension(ext, "uze")) + { + iterator->consoles[0] = RC_CONSOLE_UZEBOX; + } + break; + + case 'v': + if (rc_path_compare_extension(ext, "vb")) + { + iterator->consoles[0] = RC_CONSOLE_VIRTUAL_BOY; + } + else if (rc_path_compare_extension(ext, "v64")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; + } + break; + + case 'w': + if (rc_path_compare_extension(ext, "wsc")) + { + iterator->consoles[0] = RC_CONSOLE_WONDERSWAN; + } + else if (rc_path_compare_extension(ext, "wasm")) + { + iterator->consoles[0] = RC_CONSOLE_WASM4; + } + else if (rc_path_compare_extension(ext, "woz")) + { + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + break; + + case 'z': + if (rc_path_compare_extension(ext, "zip")) + { + /* decompressing zip file not supported */ + iterator->consoles[0] = RC_CONSOLE_ARCADE; + need_path = 1; + } + else if (rc_path_compare_extension(ext, "z64")) + { + iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; + } + break; + } + + if (verbose_message_callback) + { + char message[256]; + int count = 0; + while (iterator->consoles[count]) + ++count; + + snprintf(message, sizeof(message), "Found %d potential consoles for %s file extension", count, ext); + verbose_message_callback(message); + } + + /* loop is only for specific cases that redirect to another file - like m3u */ + break; + } while (1); + + if (need_path && !iterator->path) + iterator->path = strdup(path); + + /* if we didn't match the extension, default to something that does a whole file hash */ + if (!iterator->consoles[0]) + iterator->consoles[0] = RC_CONSOLE_GAMEBOY; +} + +void rc_hash_destroy_iterator(struct rc_hash_iterator* iterator) +{ + if (iterator->path) + { + free((void*)iterator->path); + iterator->path = NULL; + } +} + +int rc_hash_iterate(char hash[33], struct rc_hash_iterator* iterator) +{ + int next_console; + int result = 0; + + do + { + next_console = iterator->consoles[iterator->index]; + if (next_console == 0) + { + hash[0] = '\0'; + break; + } + + ++iterator->index; + + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Trying console %d", next_console); + verbose_message_callback(message); + } + + if (iterator->buffer) + result = rc_hash_generate_from_buffer(hash, next_console, iterator->buffer, iterator->buffer_size); + else + result = rc_hash_generate_from_file(hash, next_console, iterator->path); + + } while (!result); + + return result; +} diff --git a/src/rcheevos/src/rhash/md5.c b/src/rcheevos/src/rhash/md5.c new file mode 100644 index 000000000..f3a520566 --- /dev/null +++ b/src/rcheevos/src/rhash/md5.c @@ -0,0 +1,382 @@ +/* + Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.c is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order + either statically or dynamically; added missing #include + in library. + 2002-03-11 lpd Corrected argument list for main(), and added int return + type, in test program and T value program. + 2002-02-21 lpd Added missing #include in test program. + 2000-07-03 lpd Patched to eliminate warnings about "constant is + unsigned in ANSI C, signed in traditional"; made test program + self-checking. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). + 1999-05-03 lpd Original version. + */ + +#include "md5.h" +#include +#include + +#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ +#ifdef ARCH_IS_BIG_ENDIAN +# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) +#else +# define BYTE_ORDER 0 +#endif + +#define T_MASK ((md5_word_t)~0) +#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) +#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) +#define T3 0x242070db +#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) +#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) +#define T6 0x4787c62a +#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) +#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) +#define T9 0x698098d8 +#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) +#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) +#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) +#define T13 0x6b901122 +#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) +#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) +#define T16 0x49b40821 +#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) +#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) +#define T19 0x265e5a51 +#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) +#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) +#define T22 0x02441453 +#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) +#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) +#define T25 0x21e1cde6 +#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) +#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) +#define T28 0x455a14ed +#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) +#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) +#define T31 0x676f02d9 +#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) +#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) +#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) +#define T35 0x6d9d6122 +#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) +#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) +#define T38 0x4bdecfa9 +#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) +#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) +#define T41 0x289b7ec6 +#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) +#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) +#define T44 0x04881d05 +#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) +#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) +#define T47 0x1fa27cf8 +#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) +#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) +#define T50 0x432aff97 +#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) +#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) +#define T53 0x655b59c3 +#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) +#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) +#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) +#define T57 0x6fa87e4f +#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) +#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) +#define T60 0x4e0811a1 +#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) +#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) +#define T63 0x2ad7d2bb +#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) + + +static void +md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) +{ + md5_word_t + a = pms->abcd[0], b = pms->abcd[1], + c = pms->abcd[2], d = pms->abcd[3]; + md5_word_t t; +#if BYTE_ORDER > 0 + /* Define storage only for big-endian CPUs. */ + md5_word_t X[16]; +#else + /* Define storage for little-endian or both types of CPUs. */ + md5_word_t xbuf[16]; + const md5_word_t *X; +#endif + + { +#if BYTE_ORDER == 0 + /* + * Determine dynamically whether this is a big-endian or + * little-endian machine, since we can use a more efficient + * algorithm on the latter. + */ + static const int w = 1; + + if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ +#endif +#if BYTE_ORDER <= 0 /* little-endian */ + { + /* + * On little-endian machines, we can process properly aligned + * data without copying it. + */ + if (!((ptrdiff_t)data & 3)) { + /* data are properly aligned */ + X = (const md5_word_t *)data; + } else { + /* not aligned */ + memcpy(xbuf, data, 64); + X = xbuf; + } + } +#endif +#if BYTE_ORDER == 0 + else /* dynamic big-endian */ +#endif +#if BYTE_ORDER >= 0 /* big-endian */ + { + /* + * On big-endian machines, we must arrange the bytes in the + * right order. + */ + const md5_byte_t *xp = data; + int i; + +# if BYTE_ORDER == 0 + X = xbuf; /* (dynamic only) */ +# else +# define xbuf X /* (static only) */ +# endif + for (i = 0; i < 16; ++i, xp += 4) + xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24); + } +#endif + } + +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + + /* Round 1. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ +#define F(x, y, z) (((x) & (y)) | (~(x) & (z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + F(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 7, T1); + SET(d, a, b, c, 1, 12, T2); + SET(c, d, a, b, 2, 17, T3); + SET(b, c, d, a, 3, 22, T4); + SET(a, b, c, d, 4, 7, T5); + SET(d, a, b, c, 5, 12, T6); + SET(c, d, a, b, 6, 17, T7); + SET(b, c, d, a, 7, 22, T8); + SET(a, b, c, d, 8, 7, T9); + SET(d, a, b, c, 9, 12, T10); + SET(c, d, a, b, 10, 17, T11); + SET(b, c, d, a, 11, 22, T12); + SET(a, b, c, d, 12, 7, T13); + SET(d, a, b, c, 13, 12, T14); + SET(c, d, a, b, 14, 17, T15); + SET(b, c, d, a, 15, 22, T16); +#undef SET + + /* Round 2. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ +#define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + G(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 1, 5, T17); + SET(d, a, b, c, 6, 9, T18); + SET(c, d, a, b, 11, 14, T19); + SET(b, c, d, a, 0, 20, T20); + SET(a, b, c, d, 5, 5, T21); + SET(d, a, b, c, 10, 9, T22); + SET(c, d, a, b, 15, 14, T23); + SET(b, c, d, a, 4, 20, T24); + SET(a, b, c, d, 9, 5, T25); + SET(d, a, b, c, 14, 9, T26); + SET(c, d, a, b, 3, 14, T27); + SET(b, c, d, a, 8, 20, T28); + SET(a, b, c, d, 13, 5, T29); + SET(d, a, b, c, 2, 9, T30); + SET(c, d, a, b, 7, 14, T31); + SET(b, c, d, a, 12, 20, T32); +#undef SET + + /* Round 3. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + H(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 5, 4, T33); + SET(d, a, b, c, 8, 11, T34); + SET(c, d, a, b, 11, 16, T35); + SET(b, c, d, a, 14, 23, T36); + SET(a, b, c, d, 1, 4, T37); + SET(d, a, b, c, 4, 11, T38); + SET(c, d, a, b, 7, 16, T39); + SET(b, c, d, a, 10, 23, T40); + SET(a, b, c, d, 13, 4, T41); + SET(d, a, b, c, 0, 11, T42); + SET(c, d, a, b, 3, 16, T43); + SET(b, c, d, a, 6, 23, T44); + SET(a, b, c, d, 9, 4, T45); + SET(d, a, b, c, 12, 11, T46); + SET(c, d, a, b, 15, 16, T47); + SET(b, c, d, a, 2, 23, T48); +#undef SET + + /* Round 4. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ +#define I(x, y, z) ((y) ^ ((x) | ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + I(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 6, T49); + SET(d, a, b, c, 7, 10, T50); + SET(c, d, a, b, 14, 15, T51); + SET(b, c, d, a, 5, 21, T52); + SET(a, b, c, d, 12, 6, T53); + SET(d, a, b, c, 3, 10, T54); + SET(c, d, a, b, 10, 15, T55); + SET(b, c, d, a, 1, 21, T56); + SET(a, b, c, d, 8, 6, T57); + SET(d, a, b, c, 15, 10, T58); + SET(c, d, a, b, 6, 15, T59); + SET(b, c, d, a, 13, 21, T60); + SET(a, b, c, d, 4, 6, T61); + SET(d, a, b, c, 11, 10, T62); + SET(c, d, a, b, 2, 15, T63); + SET(b, c, d, a, 9, 21, T64); +#undef SET + + /* Then perform the following additions. (That is increment each + of the four registers by the value it had before this block + was started.) */ + pms->abcd[0] += a; + pms->abcd[1] += b; + pms->abcd[2] += c; + pms->abcd[3] += d; +} + +void +md5_init(md5_state_t *pms) +{ + pms->count[0] = pms->count[1] = 0; + pms->abcd[0] = 0x67452301; + pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; + pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; + pms->abcd[3] = 0x10325476; +} + +void +md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) +{ + const md5_byte_t *p = data; + int left = nbytes; + int offset = (pms->count[0] >> 3) & 63; + md5_word_t nbits = (md5_word_t)(nbytes << 3); + + if (nbytes <= 0) + return; + + /* Update the message length. */ + pms->count[1] += nbytes >> 29; + pms->count[0] += nbits; + if (pms->count[0] < nbits) + pms->count[1]++; + + /* Process an initial partial block. */ + if (offset) { + int copy = (offset + nbytes > 64 ? 64 - offset : nbytes); + + memcpy(pms->buf + offset, p, copy); + if (offset + copy < 64) + return; + p += copy; + left -= copy; + md5_process(pms, pms->buf); + } + + /* Process full blocks. */ + for (; left >= 64; p += 64, left -= 64) + md5_process(pms, p); + + /* Process a final partial block. */ + if (left) + memcpy(pms->buf, p, left); +} + +void +md5_finish(md5_state_t *pms, md5_byte_t digest[16]) +{ + static const md5_byte_t pad[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + md5_byte_t data[8]; + int i; + + /* Save the length before padding. */ + for (i = 0; i < 8; ++i) + data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); + /* Pad to 56 bytes mod 64. */ + md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); + /* Append the length. */ + md5_append(pms, data, 8); + for (i = 0; i < 16; ++i) + digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); +} diff --git a/src/rcheevos/src/rhash/md5.h b/src/rcheevos/src/rhash/md5.h new file mode 100644 index 000000000..698c995d8 --- /dev/null +++ b/src/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/src/rcheevos/src/rurl/url.c b/src/rcheevos/src/rurl/url.c new file mode 100644 index 000000000..a4a25ec73 --- /dev/null +++ b/src/rcheevos/src/rurl/url.c @@ -0,0 +1,402 @@ +#include "rc_url.h" + +#include "../rcheevos/rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +#if RCHEEVOS_URL_SSL +#define RCHEEVOS_URL_PROTOCOL "https" +#else +#define RCHEEVOS_URL_PROTOCOL "http" +#endif + +static int rc_url_encode(char* encoded, size_t len, const char* str) { + for (;;) { + switch (*str) { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + case '-': case '_': case '.': case '~': + if (len < 2) + return -1; + + *encoded++ = *str++; + --len; + break; + + case ' ': + if (len < 2) + return -1; + + *encoded++ = '+'; + ++str; + --len; + break; + + default: + if (len < 4) + return -1; + + snprintf(encoded, len, "%%%02x", (unsigned char)*str); + encoded += 3; + ++str; + len -= 3; + break; + + case '\0': + *encoded = 0; + return 0; + } + } +} + +int rc_url_award_cheevo(char* buffer, size_t size, const char* user_name, const char* login_token, + unsigned cheevo_id, int hardcore, const char* game_hash) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=awardachievement&u=%s&t=%s&a=%u&h=%d", + urle_user_name, + urle_login_token, + cheevo_id, + hardcore ? 1 : 0 + ); + + if (game_hash && strlen(game_hash) == 32 && (size - (size_t)written) >= 35) { + written += snprintf(buffer + written, size - (size_t)written, "&m=%s", game_hash); + } + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_submit_lboard(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned lboard_id, int value) { + char urle_user_name[64]; + char urle_login_token[64]; + char signature[64]; + unsigned char hash[16]; + md5_state_t state; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + /* Evaluate the signature. */ + snprintf(signature, sizeof(signature), "%u%s%d", lboard_id, user_name, value); + md5_init(&state); + md5_append(&state, (unsigned char*)signature, (int)strlen(signature)); + md5_finish(&state, hash); + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=submitlbentry&u=%s&t=%s&i=%u&s=%d&v=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + urle_user_name, + urle_login_token, + lboard_id, + value, + hash[ 0], hash[ 1], hash[ 2], hash[ 3], hash[ 4], hash[ 5], hash[ 6], hash[ 7], + hash[ 8], hash[ 9], hash[10], hash[11],hash[12], hash[13], hash[14], hash[15] + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_gameid(char* buffer, size_t size, const char* hash) { + int written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=gameid&m=%s", + hash + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_patch(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=patch&u=%s&t=%s&g=%u", + urle_user_name, + urle_login_token, + gameid + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_badge_image(char* buffer, size_t size, const char* badge_name) { + int written = snprintf( + buffer, + size, + "http://i.retroachievements.org/Badge/%s", + badge_name + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_login_with_password(char* buffer, size_t size, const char* user_name, const char* password) { + char urle_user_name[64]; + char urle_password[256]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_password, sizeof(urle_password), password) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=login&u=%s&p=%s", + urle_user_name, + urle_password + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_login_with_token(char* buffer, size_t size, const char* user_name, const char* login_token) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=login&u=%s&t=%s", + urle_user_name, + urle_login_token + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_get_unlock_list(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid, int hardcore) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=unlocks&u=%s&t=%s&g=%u&h=%d", + urle_user_name, + urle_login_token, + gameid, + hardcore ? 1 : 0 + ); + + return (size_t)written >= size ? -1 : 0; +} + +int rc_url_post_playing(char* buffer, size_t size, const char* user_name, const char* login_token, unsigned gameid) { + char urle_user_name[64]; + char urle_login_token[64]; + int written; + + if (rc_url_encode(urle_user_name, sizeof(urle_user_name), user_name) != 0) { + return -1; + } + + if (rc_url_encode(urle_login_token, sizeof(urle_login_token), login_token) != 0) { + return -1; + } + + written = snprintf( + buffer, + size, + RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php?r=postactivity&u=%s&t=%s&a=3&m=%u", + urle_user_name, + urle_login_token, + gameid + ); + + return (size_t)written >= size ? -1 : 0; +} + +static int rc_url_append_param_equals(char* buffer, size_t buffer_size, size_t buffer_offset, const char* param) +{ + int written = 0; + size_t param_len; + + if (buffer_offset >= buffer_size) + return -1; + + if (buffer_offset) { + buffer += buffer_offset; + buffer_size -= buffer_offset; + + if (buffer[-1] != '?') { + *buffer++ = '&'; + buffer_size--; + written = 1; + } + } + + param_len = strlen(param); + if (param_len + 1 >= buffer_size) + return -1; + memcpy(buffer, param, param_len); + buffer[param_len] = '='; + + written += (int)param_len + 1; + return written + (int)buffer_offset; +} + +static int rc_url_append_unum(char* buffer, size_t buffer_size, size_t* buffer_offset, const char* param, unsigned value) +{ + int written = rc_url_append_param_equals(buffer, buffer_size, *buffer_offset, param); + if (written > 0) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%u", value); + + if (chars + written < (int)buffer_size) { + memcpy(&buffer[written], num, chars + 1); + *buffer_offset = written + chars; + return 0; + } + } + + return -1; +} + +static int rc_url_append_str(char* buffer, size_t buffer_size, size_t* buffer_offset, const char* param, const char* value) +{ + int written = rc_url_append_param_equals(buffer, buffer_size, *buffer_offset, param); + if (written > 0) { + buffer += written; + buffer_size -= written; + + if (rc_url_encode(buffer, buffer_size, value) == 0) { + written += (int)strlen(buffer); + *buffer_offset = written; + return 0; + } + } + + return -1; +} + +static int rc_url_build_dorequest(char* url_buffer, size_t url_buffer_size, size_t* buffer_offset, + const char* api, const char* user_name) +{ + const char* base_url = RCHEEVOS_URL_PROTOCOL"://retroachievements.org/dorequest.php"; + size_t written = strlen(base_url); + int failure = 0; + + if (url_buffer_size < written + 1) + return -1; + memcpy(url_buffer, base_url, written); + url_buffer[written++] = '?'; + + failure |= rc_url_append_str(url_buffer, url_buffer_size, &written, "r", api); + if (user_name) + failure |= rc_url_append_str(url_buffer, url_buffer_size, &written, "u", user_name); + + *buffer_offset += written; + return failure; +} + +int rc_url_ping(char* url_buffer, size_t url_buffer_size, char* post_buffer, size_t post_buffer_size, + const char* user_name, const char* login_token, unsigned gameid, const char* rich_presence) +{ + size_t written = 0; + int failure = rc_url_build_dorequest(url_buffer, url_buffer_size, &written, "ping", user_name); + failure |= rc_url_append_unum(url_buffer, url_buffer_size, &written, "g", gameid); + + written = 0; + failure |= rc_url_append_str(post_buffer, post_buffer_size, &written, "t", login_token); + + if (rich_presence && *rich_presence) + failure |= rc_url_append_str(post_buffer, post_buffer_size, &written, "m", rich_presence); + + if (failure) { + if (url_buffer_size) + url_buffer[0] = '\0'; + if (post_buffer_size) + post_buffer[0] = '\0'; + } + + return failure; +} + +int rc_url_get_lboard_entries(char* buffer, size_t size, unsigned lboard_id, unsigned first_index, unsigned count) +{ + size_t written = 0; + int failure = rc_url_build_dorequest(buffer, size, &written, "lbinfo", NULL); + failure |= rc_url_append_unum(buffer, size, &written, "i", lboard_id); + if (first_index > 1) + failure |= rc_url_append_unum(buffer, size, &written, "o", first_index - 1); + failure |= rc_url_append_unum(buffer, size, &written, "c", count); + + return failure; +} + +int rc_url_get_lboard_entries_near_user(char* buffer, size_t size, unsigned lboard_id, const char* user_name, unsigned count) +{ + size_t written = 0; + int failure = rc_url_build_dorequest(buffer, size, &written, "lbinfo", NULL); + failure |= rc_url_append_unum(buffer, size, &written, "i", lboard_id); + failure |= rc_url_append_str(buffer, size, &written, "u", user_name); + failure |= rc_url_append_unum(buffer, size, &written, "c", count); + + return failure; +} + +#undef RCHEEVOS_URL_PROTOCOL diff --git a/src/rcheevos/test/Makefile b/src/rcheevos/test/Makefile new file mode 100644 index 000000000..5fddd19b2 --- /dev/null +++ b/src/rcheevos/test/Makefile @@ -0,0 +1,198 @@ +# supported parameters +# ARCH architecture - "x86" or "x64" [detected if not set] +# BUILD use flags for specified upstream consumer - "c89" or "retroarch" [default to "c89] +# DEBUG if set to anything, builds with DEBUG symbols +# HAVE_LUA if set to anything, includes the lua functionality + +RC_SRC=../src/rcheevos +RC_URL_SRC=../src/rurl +RC_HASH_SRC=../src/rhash +RC_API_SRC=../src/rapi +LUA_SRC=lua/src + +# default parameter values +ifeq ($(ARCH),) + UNAME := $(shell uname -s) + ifeq ($(findstring MINGW64, $(UNAME)), MINGW64) + ARCH=x64 + else ifeq ($(findstring MINGW32, $(UNAME)), MINGW32) + ARCH=x86 + else + $(error Could not determine ARCH) + endif +endif + +ifeq ($(BUILD),) + BUILD=c89 +endif + +# OS specific stuff +ifeq ($(OS),Windows_NT) + EXE=.exe +else ifeq ($(findstring mingw, $(CC)), mingw) + EXE=.exe +else + EXE= +endif + +# source files +OBJ=$(RC_SRC)/alloc.o \ + $(RC_SRC)/compat.o \ + $(RC_SRC)/condition.o \ + $(RC_SRC)/condset.o \ + $(RC_SRC)/consoleinfo.o \ + $(RC_SRC)/format.o \ + $(RC_SRC)/lboard.o \ + $(RC_SRC)/memref.o \ + $(RC_SRC)/operand.o \ + $(RC_SRC)/rc_client.o \ + $(RC_SRC)/rc_libretro.o \ + $(RC_SRC)/rc_validate.o \ + $(RC_SRC)/richpresence.o \ + $(RC_SRC)/runtime.o \ + $(RC_SRC)/runtime_progress.o \ + $(RC_SRC)/trigger.o \ + $(RC_SRC)/value.o \ + $(RC_HASH_SRC)/cdreader.o \ + $(RC_HASH_SRC)/hash.o \ + $(RC_HASH_SRC)/md5.o \ + $(RC_URL_SRC)/url.o \ + $(RC_API_SRC)/rc_api_common.o \ + $(RC_API_SRC)/rc_api_editor.o \ + $(RC_API_SRC)/rc_api_info.o \ + $(RC_API_SRC)/rc_api_runtime.o \ + $(RC_API_SRC)/rc_api_user.o \ + rcheevos/test_condition.o \ + rcheevos/test_condset.o \ + rcheevos/test_consoleinfo.o \ + rcheevos/test_format.o \ + rcheevos/test_lboard.o \ + rcheevos/test_memref.o \ + rcheevos/test_operand.o \ + rcheevos/test_rc_client.o \ + rcheevos/test_rc_libretro.o \ + rcheevos/test_rc_validate.o \ + rcheevos/test_richpresence.o \ + rcheevos/test_runtime.o \ + rcheevos/test_runtime_progress.o \ + rcheevos/test_trigger.o \ + rcheevos/test_value.o \ + rurl/test_url.o \ + rhash/data.o \ + rhash/mock_filereader.o \ + rhash/test_cdreader.o \ + rhash/test_hash.o \ + rapi/test_rc_api_common.o \ + rapi/test_rc_api_editor.o \ + rapi/test_rc_api_info.o \ + rapi/test_rc_api_runtime.o \ + rapi/test_rc_api_user.o \ + test.o + +# compile flags +CFLAGS=-Wall -Wno-long-long -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 +INCLUDES=-I../include -I$(RC_SRC) -I. + +ifeq ($(ARCH), x86) + CFLAGS += -m32 + LDFLAGS += -m32 +else ifeq ($(ARCH), x64) + CFLAGS += -m64 + LDFLAGS += -m64 +else + $(error unknown ARCH "$(ARCH)") +endif + +EXTRA= +ifdef DEBUG + CFLAGS += -O0 -g + EXTRA += DEBUG +else + CFLAGS += -O3 +endif + +# more strict validation for source files to eliminate warnings/errors in upstream consumers +# 3DS build (retroarch) doesn't support signed char +SRC_CFLAGS=-pedantic -Wsign-compare -fno-signed-char + +ifeq ($(BUILD), c89) + CFLAGS += -std=c89 +else ifeq ($(BUILD), retroarch) + # RetroArch builds with gcc8 and gnu99 which adds some extra warning validations + SRC_CFLAGS += -std=gnu99 -D_GNU_SOURCE -Wall -Warray-bounds=2 +else + $(error unknown BUILD "$(BUILD)") +endif + +# LUA support +ifdef HAVE_LUA + OBJ += $(LUA_SRC)/lapi.o $(LUA_SRC)/lcode.o $(LUA_SRC)/lctype.o $(LUA_SRC)/ldebug.o \ + $(LUA_SRC)/ldo.o $(LUA_SRC)/ldump.o $(LUA_SRC)/lfunc.o $(LUA_SRC)/lgc.o $(LUA_SRC)/llex.o \ + $(LUA_SRC)/lmem.o $(LUA_SRC)/lobject.o $(LUA_SRC)/lopcodes.o $(LUA_SRC)/lparser.o \ + $(LUA_SRC)/lstate.o $(LUA_SRC)/lstring.o $(LUA_SRC)/ltable.o $(LUA_SRC)/ltm.o \ + $(LUA_SRC)/lundump.o $(LUA_SRC)/lvm.o $(LUA_SRC)/lzio.o $(LUA_SRC)/lauxlib.o \ + $(LUA_SRC)/lbaselib.o $(LUA_SRC)/lbitlib.o $(LUA_SRC)/lcorolib.o $(LUA_SRC)/ldblib.o \ + $(LUA_SRC)/liolib.o $(LUA_SRC)/lmathlib.o $(LUA_SRC)/loslib.o $(LUA_SRC)/lstrlib.o \ + $(LUA_SRC)/ltablib.o $(LUA_SRC)/lutf8lib.o $(LUA_SRC)/loadlib.o $(LUA_SRC)/linit.o + + CFLAGS += -DLUA_32BITS + + ifeq ($(BUILD), c89) + CFLAGS += -DLUA_USE_C89 + endif + + INCLUDES += -I$(LUA_SRC) + EXTRA += |HAVE_LUA +else + CFLAGS += -DRC_DISABLE_LUA -Werror +endif + +# recipes +$(info ==== rcheevos test [$(BUILD)/$(ARCH)$(EXTRA)] ====) + +all: test + +$(RC_SRC)/%.o: $(RC_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(RC_HASH_SRC)/%.o: $(RC_HASH_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(RC_URL_SRC)/%.o: $(RC_URL_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(RC_API_SRC)/%.o: $(RC_API_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(LUA_SRC)/%.o: $(LUA_SRC)/%.c + $(CC) $(CFLAGS) -I$(LUA_SRC) -c $< -o $@ + +%.o: %.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +test: $(OBJ) + $(CC) $(LDFLAGS) -o $@$(EXE) $+ -lm + @echo --------------------------------- + +check_ctype: + @if grep -rnI "isalpha([^(u]" ../src/*; \ + then echo "*** Error: isalpha without unsigned char cast" && false; \ + fi + @if grep -rnI "isalnum([^(u]" ../src/*; \ + then echo "*** Error: isalnum without unsigned char cast" && false; \ + fi + @if grep -rnI "isdigit([^(u]" ../src/*; \ + then echo "*** Error: isdigit without unsigned char cast" && false; \ + fi + @if grep -rnI "isspace([^(u]" ../src/*; \ + then echo "*** Error: isspace without unsigned char cast" && false; \ + fi + +runtests: test + @./test$(EXE) + +valgrind: test + @valgrind --leak-check=full --error-exitcode=1 ./test$(EXE) + +clean: + rm -f test$(EXE) $(OBJ) diff --git a/src/rcheevos/test/libretro.h b/src/rcheevos/test/libretro.h new file mode 100644 index 000000000..30009f505 --- /dev/null +++ b/src/rcheevos/test/libretro.h @@ -0,0 +1,205 @@ +/** + * Provides copies of structures and constants from + * https://github.com/libretro/RetroArch/blob/master/libretro-common/include/libretro.h + * for unit testing without pulling in the entire libretro project. + */ + +#ifndef LIBRETRO_H__ +#define LIBRETRO_H__ + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Regular save RAM. This RAM is usually found on a game cartridge, + * backed up by a battery. + * If save game data is too complex for a single memory buffer, + * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment + * callback can be used. */ +#define RETRO_MEMORY_SAVE_RAM 0 + +/* Some games have a built-in clock to keep track of time. + * This memory is usually just a couple of bytes to keep track of time. + */ +#define RETRO_MEMORY_RTC 1 + +/* System ram lets a frontend peek into a game systems main RAM. */ +#define RETRO_MEMORY_SYSTEM_RAM 2 + +/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */ +#define RETRO_MEMORY_VIDEO_RAM 3 + + +#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ +#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ +#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */ +#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */ +#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */ +#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ +#define RETRO_MEMDESC_ALIGN_4 (2 << 16) +#define RETRO_MEMDESC_ALIGN_8 (3 << 16) +#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ +#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) +#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) +struct retro_memory_descriptor +{ + uint64_t flags; + + /* Pointer to the start of the relevant ROM or RAM chip. + * It's strongly recommended to use 'offset' if possible, rather than + * doing math on the pointer. + * + * If the same byte is mapped my multiple descriptors, their descriptors + * must have the same pointer. + * If 'start' does not point to the first byte in the pointer, put the + * difference in 'offset' instead. + * + * May be NULL if there's nothing usable here (e.g. hardware registers and + * open bus). No flags should be set if the pointer is NULL. + * It's recommended to minimize the number of descriptors if possible, + * but not mandatory. */ + void *ptr; + size_t offset; + + /* This is the location in the emulated address space + * where the mapping starts. */ + size_t start; + + /* Which bits must be same as in 'start' for this mapping to apply. + * The first memory descriptor to claim a certain byte is the one + * that applies. + * A bit which is set in 'start' must also be set in this. + * Can be zero, in which case each byte is assumed mapped exactly once. + * In this case, 'len' must be a power of two. */ + size_t select; + + /* If this is nonzero, the set bits are assumed not connected to the + * memory chip's address pins. */ + size_t disconnect; + + /* This one tells the size of the current memory area. + * If, after start+disconnect are applied, the address is higher than + * this, the highest bit of the address is cleared. + * + * If the address is still too high, the next highest bit is cleared. + * Can be zero, in which case it's assumed to be infinite (as limited + * by 'select' and 'disconnect'). */ + size_t len; + + /* To go from emulated address to physical address, the following + * order applies: + * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */ + + /* The address space name must consist of only a-zA-Z0-9_-, + * should be as short as feasible (maximum length is 8 plus the NUL), + * and may not be any other address space plus one or more 0-9A-F + * at the end. + * However, multiple memory descriptors for the same address space is + * allowed, and the address space name can be empty. NULL is treated + * as empty. + * + * Address space names are case sensitive, but avoid lowercase if possible. + * The same pointer may exist in multiple address spaces. + * + * Examples: + * blank+blank - valid (multiple things may be mapped in the same namespace) + * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace) + * 'A'+'B' - valid (neither is a prefix of each other) + * 'S'+blank - valid ('S' is not in 0-9A-F) + * 'a'+blank - valid ('a' is not in 0-9A-F) + * 'a'+'A' - valid (neither is a prefix of each other) + * 'AR'+blank - valid ('R' is not in 0-9A-F) + * 'ARB'+blank - valid (the B can't be part of the address either, because + * there is no namespace 'AR') + * blank+'B' - not valid, because it's ambigous which address space B1234 + * would refer to. + * The length can't be used for that purpose; the frontend may want + * to append arbitrary data to an address, without a separator. */ + const char *addrspace; + + /* TODO: When finalizing this one, add a description field, which should be + * "WRAM" or something roughly equally long. */ + + /* TODO: When finalizing this one, replace 'select' with 'limit', which tells + * which bits can vary and still refer to the same address (limit = ~select). + * TODO: limit? range? vary? something else? */ + + /* TODO: When finalizing this one, if 'len' is above what 'select' (or + * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len' + * and 'select' != 0, and the mappings don't tell how the system switches the + * banks. */ + + /* TODO: When finalizing this one, fix the 'len' bit removal order. + * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00. + * Algorithm: Take bits highest to lowest, but if it goes above len, clear + * the most recent addition and continue on the next bit. + * TODO: Can the above be optimized? Is "remove the lowest bit set in both + * pointer and 'len'" equivalent? */ + + /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing + * the emulated memory in 32-bit chunks, native endian. But that's nothing + * compared to Darek Mihocka + * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE + * RAM backwards! I'll want to represent both of those, via some flags. + * + * I suspect MAME either didn't think of that idea, or don't want the #ifdef. + * Not sure which, nor do I really care. */ + + /* TODO: Some of those flags are unused and/or don't really make sense. Clean + * them up. */ +}; + +/* The frontend may use the largest value of 'start'+'select' in a + * certain namespace to infer the size of the address space. + * + * If the address space is larger than that, a mapping with .ptr=NULL + * should be at the end of the array, with .select set to all ones for + * as long as the address space is big. + * + * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags): + * SNES WRAM: + * .start=0x7E0000, .len=0x20000 + * (Note that this must be mapped before the ROM in most cases; some of the + * ROM mappers + * try to claim $7E0000, or at least $7E8000.) + * SNES SPC700 RAM: + * .addrspace="S", .len=0x10000 + * SNES WRAM mirrors: + * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000 + * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000 + * SNES WRAM mirrors, alternate equivalent descriptor: + * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF + * (Various similar constructions can be created by combining parts of + * the above two.) + * SNES LoROM (512KB, mirrored a couple of times): + * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024 + * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024 + * SNES HiROM (4MB): + * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024 + * SNES ExHiROM (8MB): + * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024 + * Clarify the size of the address space: + * .ptr=NULL, .select=0xFFFFFF + * .len can be implied by .select in many of them, but was included for clarity. + */ + +struct retro_memory_map +{ + const struct retro_memory_descriptor *descriptors; + unsigned num_descriptors; +}; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/rcheevos/test/lua/Makefile b/src/rcheevos/test/lua/Makefile new file mode 100644 index 000000000..119110d2f --- /dev/null +++ b/src/rcheevos/test/lua/Makefile @@ -0,0 +1,114 @@ +# Makefile for installing Lua +# See doc/readme.html for installation and customization instructions. + +# == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT ======================= + +# Your platform. See PLATS for possible values. +PLAT= none + +# Where to install. The installation starts in the src and doc directories, +# so take care if INSTALL_TOP is not an absolute path. See the local target. +# You may want to make INSTALL_LMOD and INSTALL_CMOD consistent with +# LUA_ROOT, LUA_LDIR, and LUA_CDIR in luaconf.h. +INSTALL_TOP= /usr/local +INSTALL_BIN= $(INSTALL_TOP)/bin +INSTALL_INC= $(INSTALL_TOP)/include +INSTALL_LIB= $(INSTALL_TOP)/lib +INSTALL_MAN= $(INSTALL_TOP)/man/man1 +INSTALL_LMOD= $(INSTALL_TOP)/share/lua/$V +INSTALL_CMOD= $(INSTALL_TOP)/lib/lua/$V + +# How to install. If your install program does not support "-p", then +# you may have to run ranlib on the installed liblua.a. +INSTALL= install -p +INSTALL_EXEC= $(INSTALL) -m 0755 +INSTALL_DATA= $(INSTALL) -m 0644 +# +# If you don't have "install" you can use "cp" instead. +# INSTALL= cp -p +# INSTALL_EXEC= $(INSTALL) +# INSTALL_DATA= $(INSTALL) + +# Other utilities. +MKDIR= mkdir -p +RM= rm -f + +# == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE ======= + +# Convenience platforms targets. +PLATS= aix bsd c89 freebsd generic linux macosx mingw posix solaris + +# What to install. +TO_BIN= lua luac +TO_INC= lua.h luaconf.h lualib.h lauxlib.h lua.hpp +TO_LIB= liblua.a +TO_MAN= lua.1 luac.1 + +# Lua version and release. +V= 5.3 +R= $V.4 + +# Targets start here. +all: $(PLAT) + +$(PLATS) clean: + cd src && $(MAKE) $@ + +test: dummy + src/lua -v + +install: dummy + cd src && $(MKDIR) $(INSTALL_BIN) $(INSTALL_INC) $(INSTALL_LIB) $(INSTALL_MAN) $(INSTALL_LMOD) $(INSTALL_CMOD) + cd src && $(INSTALL_EXEC) $(TO_BIN) $(INSTALL_BIN) + cd src && $(INSTALL_DATA) $(TO_INC) $(INSTALL_INC) + cd src && $(INSTALL_DATA) $(TO_LIB) $(INSTALL_LIB) + cd doc && $(INSTALL_DATA) $(TO_MAN) $(INSTALL_MAN) + +uninstall: + cd src && cd $(INSTALL_BIN) && $(RM) $(TO_BIN) + cd src && cd $(INSTALL_INC) && $(RM) $(TO_INC) + cd src && cd $(INSTALL_LIB) && $(RM) $(TO_LIB) + cd doc && cd $(INSTALL_MAN) && $(RM) $(TO_MAN) + +local: + $(MAKE) install INSTALL_TOP=../install + +none: + @echo "Please do 'make PLATFORM' where PLATFORM is one of these:" + @echo " $(PLATS)" + @echo "See doc/readme.html for complete instructions." + +# make may get confused with test/ and install/ +dummy: + +# echo config parameters +echo: + @cd src && $(MAKE) -s echo + @echo "PLAT= $(PLAT)" + @echo "V= $V" + @echo "R= $R" + @echo "TO_BIN= $(TO_BIN)" + @echo "TO_INC= $(TO_INC)" + @echo "TO_LIB= $(TO_LIB)" + @echo "TO_MAN= $(TO_MAN)" + @echo "INSTALL_TOP= $(INSTALL_TOP)" + @echo "INSTALL_BIN= $(INSTALL_BIN)" + @echo "INSTALL_INC= $(INSTALL_INC)" + @echo "INSTALL_LIB= $(INSTALL_LIB)" + @echo "INSTALL_MAN= $(INSTALL_MAN)" + @echo "INSTALL_LMOD= $(INSTALL_LMOD)" + @echo "INSTALL_CMOD= $(INSTALL_CMOD)" + @echo "INSTALL_EXEC= $(INSTALL_EXEC)" + @echo "INSTALL_DATA= $(INSTALL_DATA)" + +# echo pkg-config data +pc: + @echo "version=$R" + @echo "prefix=$(INSTALL_TOP)" + @echo "libdir=$(INSTALL_LIB)" + @echo "includedir=$(INSTALL_INC)" + +# list targets that do not create files (but not all makes understand .PHONY) +.PHONY: all $(PLATS) clean test install local none dummy echo pecho lecho + +# (end of Makefile) diff --git a/src/rcheevos/test/lua/README b/src/rcheevos/test/lua/README new file mode 100644 index 000000000..0b31908a0 --- /dev/null +++ b/src/rcheevos/test/lua/README @@ -0,0 +1,6 @@ + +This is Lua 5.3.4, released on 12 Jan 2017. + +For installation instructions, license details, and +further information about Lua, see doc/readme.html. + diff --git a/src/rcheevos/test/lua/doc/contents.html b/src/rcheevos/test/lua/doc/contents.html new file mode 100644 index 000000000..445556f96 --- /dev/null +++ b/src/rcheevos/test/lua/doc/contents.html @@ -0,0 +1,619 @@ + + + +Lua 5.3 Reference Manual - contents + + + + + + + +

+Lua +Lua 5.3 Reference Manual +

+ +

+The reference manual is the official definition of the Lua language. +
+For a complete introduction to Lua programming, see the book +Programming in Lua. + +

+ +

+ +Copyright © 2015–2017 Lua.org, PUC-Rio. +Freely available under the terms of the +Lua license. + + +

Contents

+ + +

Index

+ + + + + + + + + + + + + + diff --git a/src/rcheevos/test/lua/doc/index.css b/src/rcheevos/test/lua/doc/index.css new file mode 100644 index 000000000..c96183573 --- /dev/null +++ b/src/rcheevos/test/lua/doc/index.css @@ -0,0 +1,21 @@ +ul { + list-style-type: none ; +} + +ul.contents { + padding: 0 ; +} + +table { + border: none ; + border-spacing: 0 ; + border-collapse: collapse ; +} + +td { + vertical-align: top ; + padding: 0 ; + text-align: left ; + line-height: 1.25 ; + width: 15% ; +} diff --git a/src/rcheevos/test/lua/doc/logo.gif b/src/rcheevos/test/lua/doc/logo.gif new file mode 100644 index 000000000..5c77eacc3 Binary files /dev/null and b/src/rcheevos/test/lua/doc/logo.gif differ diff --git a/src/rcheevos/test/lua/doc/lua.1 b/src/rcheevos/test/lua/doc/lua.1 new file mode 100644 index 000000000..d728d0b80 --- /dev/null +++ b/src/rcheevos/test/lua/doc/lua.1 @@ -0,0 +1,112 @@ +.\" $Id: lua.man,v 1.14 2016/10/17 15:43:50 lhf Exp $ +.TH LUA 1 "$Date: 2016/10/17 15:43:50 $" +.SH NAME +lua \- Lua interpreter +.SH SYNOPSIS +.B lua +[ +.I options +] +[ +.I script +[ +.I args +] +] +.SH DESCRIPTION +.B lua +is the standalone Lua interpreter. +It loads and executes Lua programs, +either in textual source form or +in precompiled binary form. +(Precompiled binaries are output by +.BR luac , +the Lua compiler.) +.B lua +can be used as a batch interpreter and also interactively. +.LP +The given +.I options +are handled in order and then +the Lua program in file +.I script +is loaded and executed. +The given +.I args +are available to +.I script +as strings in a global table named +.BR arg . +If no options or arguments are given, +then +.B "\-v \-i" +is assumed when the standard input is a terminal; +otherwise, +.B "\-" +is assumed. +.LP +In interactive mode, +.B lua +prompts the user, +reads lines from the standard input, +and executes them as they are read. +If the line contains an expression or list of expressions, +then the line is evaluated and the results are printed. +If a line does not contain a complete statement, +then a secondary prompt is displayed and +lines are read until a complete statement is formed or +a syntax error is found. +.LP +At the very start, +before even handling the command line, +.B lua +checks the contents of the environment variables +.B LUA_INIT_5_3 +or +.BR LUA_INIT , +in that order. +If the contents is of the form +.RI '@ filename ', +then +.I filename +is executed. +Otherwise, the string is assumed to be a Lua statement and is executed. +.SH OPTIONS +.TP +.BI \-e " stat" +execute statement +.IR stat . +.TP +.B \-i +enter interactive mode after executing +.IR script . +.TP +.BI \-l " name" +execute the equivalent of +.IB name =require(' name ') +before executing +.IR script . +.TP +.B \-v +show version information. +.TP +.B \-E +ignore environment variables. +.TP +.B \-\- +stop handling options. +.TP +.B \- +stop handling options and execute the standard input as a file. +.SH "SEE ALSO" +.BR luac (1) +.br +The documentation at lua.org, +especially section 7 of the reference manual. +.SH DIAGNOSTICS +Error messages should be self explanatory. +.SH AUTHORS +R. Ierusalimschy, +L. H. de Figueiredo, +W. Celes +.\" EOF diff --git a/src/rcheevos/test/lua/doc/lua.css b/src/rcheevos/test/lua/doc/lua.css new file mode 100644 index 000000000..5bedf7eb8 --- /dev/null +++ b/src/rcheevos/test/lua/doc/lua.css @@ -0,0 +1,164 @@ +html { + background-color: #F8F8F8 ; +} + +body { + background-color: #FFFFFF ; + color: #000000 ; + font-family: Helvetica, Arial, sans-serif ; + text-align: justify ; + line-height: 1.25 ; + margin: 16px auto ; + padding: 32px ; + border: solid #a0a0a0 1px ; + border-radius: 20px ; + max-width: 70em ; + width: 90% ; +} + +h1, h2, h3, h4 { + color: #000080 ; + font-family: Verdana, Geneva, sans-serif ; + font-weight: normal ; + font-style: normal ; + text-align: left ; +} + +h1 { + font-size: 28pt ; +} + +h1 img { + vertical-align: text-bottom ; +} + +h2:before { + content: "\2756" ; + padding-right: 0.5em ; +} + +a { + text-decoration: none ; +} + +a:link { + color: #000080 ; +} + +a:link:hover, a:visited:hover { + background-color: #D0D0FF ; + color: #000080 ; + border-radius: 4px ; +} + +a:link:active, a:visited:active { + color: #FF0000 ; +} + +div.menubar { + padding-bottom: 0.5em ; +} + +p.menubar { + margin-left: 2.5em ; +} + +.menubar a:hover { + margin: -3px -3px -3px -3px ; + padding: 3px 3px 3px 3px ; + border-radius: 4px ; +} + +:target { + background-color: #F0F0F0 ; + margin: -8px ; + padding: 8px ; + border-radius: 8px ; + outline: none ; +} + +hr { + display: none ; +} + +table hr { + background-color: #a0a0a0 ; + color: #a0a0a0 ; + border: 0 ; + height: 1px ; + display: block ; +} + +.footer { + color: gray ; + font-size: x-small ; + text-transform: lowercase ; +} + +input[type=text] { + border: solid #a0a0a0 2px ; + border-radius: 2em ; + background-image: url('images/search.png') ; + background-repeat: no-repeat ; + background-position: 4px center ; + padding-left: 20px ; + height: 2em ; +} + +pre.session { + background-color: #F8F8F8 ; + padding: 1em ; + border-radius: 8px ; +} + +td.gutter { + width: 4% ; +} + +table.columns { + border: none ; + border-spacing: 0 ; + border-collapse: collapse ; +} + +table.columns td { + vertical-align: top ; + padding: 0 ; + padding-bottom: 1em ; + text-align: justify ; + line-height: 1.25 ; +} + +p.logos a:link:hover, p.logos a:visited:hover { + background-color: inherit ; +} + +table.book { + border: none ; + border-spacing: 0 ; + border-collapse: collapse ; +} + +table.book td { + padding: 0 ; + vertical-align: top ; +} + +table.book td.cover { + padding-right: 1em ; +} + +table.book img { + border: solid #000080 1px ; +} + +table.book span { + font-size: small ; + text-align: left ; + display: block ; + margin-top: 0.25em ; +} + +img { + background-color: white ; +} diff --git a/src/rcheevos/test/lua/doc/luac.1 b/src/rcheevos/test/lua/doc/luac.1 new file mode 100644 index 000000000..33a4ed00a --- /dev/null +++ b/src/rcheevos/test/lua/doc/luac.1 @@ -0,0 +1,118 @@ +.\" $Id: luac.man,v 1.29 2011/11/16 13:53:40 lhf Exp $ +.TH LUAC 1 "$Date: 2011/11/16 13:53:40 $" +.SH NAME +luac \- Lua compiler +.SH SYNOPSIS +.B luac +[ +.I options +] [ +.I filenames +] +.SH DESCRIPTION +.B luac +is the Lua compiler. +It translates programs written in the Lua programming language +into binary files containing precompiled chunks +that can be later loaded and executed. +.LP +The main advantages of precompiling chunks are: +faster loading, +protecting source code from accidental user changes, +and +off-line syntax checking. +Precompiling does not imply faster execution +because in Lua chunks are always compiled into bytecodes before being executed. +.B luac +simply allows those bytecodes to be saved in a file for later execution. +Precompiled chunks are not necessarily smaller than the corresponding source. +The main goal in precompiling is faster loading. +.LP +In the command line, +you can mix +text files containing Lua source and +binary files containing precompiled chunks. +.B luac +produces a single output file containing the combined bytecodes +for all files given. +Executing the combined file is equivalent to executing the given files. +By default, +the output file is named +.BR luac.out , +but you can change this with the +.B \-o +option. +.LP +Precompiled chunks are +.I not +portable across different architectures. +Moreover, +the internal format of precompiled chunks +is likely to change when a new version of Lua is released. +Make sure you save the source files of all Lua programs that you precompile. +.LP +.SH OPTIONS +.TP +.B \-l +produce a listing of the compiled bytecode for Lua's virtual machine. +Listing bytecodes is useful to learn about Lua's virtual machine. +If no files are given, then +.B luac +loads +.B luac.out +and lists its contents. +Use +.B \-l \-l +for a full listing. +.TP +.BI \-o " file" +output to +.IR file , +instead of the default +.BR luac.out . +(You can use +.B "'\-'" +for standard output, +but not on platforms that open standard output in text mode.) +The output file may be one of the given files because +all files are loaded before the output file is written. +Be careful not to overwrite precious files. +.TP +.B \-p +load files but do not generate any output file. +Used mainly for syntax checking and for testing precompiled chunks: +corrupted files will probably generate errors when loaded. +If no files are given, then +.B luac +loads +.B luac.out +and tests its contents. +No messages are displayed if the file loads without errors. +.TP +.B \-s +strip debug information before writing the output file. +This saves some space in very large chunks, +but if errors occur when running a stripped chunk, +then the error messages may not contain the full information they usually do. +In particular, +line numbers and names of local variables are lost. +.TP +.B \-v +show version information. +.TP +.B \-\- +stop handling options. +.TP +.B \- +stop handling options and process standard input. +.SH "SEE ALSO" +.BR lua (1) +.br +The documentation at lua.org. +.SH DIAGNOSTICS +Error messages should be self explanatory. +.SH AUTHORS +R. Ierusalimschy, +L. H. de Figueiredo, +W. Celes +.\" EOF diff --git a/src/rcheevos/test/lua/doc/manual.css b/src/rcheevos/test/lua/doc/manual.css new file mode 100644 index 000000000..aa0e677dd --- /dev/null +++ b/src/rcheevos/test/lua/doc/manual.css @@ -0,0 +1,21 @@ +h3 code { + font-family: inherit ; + font-size: inherit ; +} + +pre, code { + font-size: 12pt ; +} + +span.apii { + color: gray ; + float: right ; + font-family: inherit ; + font-style: normal ; + font-size: small ; +} + +h2:before { + content: "" ; + padding-right: 0em ; +} diff --git a/src/rcheevos/test/lua/doc/manual.html b/src/rcheevos/test/lua/doc/manual.html new file mode 100644 index 000000000..3126b5d6a --- /dev/null +++ b/src/rcheevos/test/lua/doc/manual.html @@ -0,0 +1,10985 @@ + + + +Lua 5.3 Reference Manual + + + + + + + +

+Lua +Lua 5.3 Reference Manual +

+ +

+by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes + +

+ +Copyright © 2015–2017 Lua.org, PUC-Rio. +Freely available under the terms of the +Lua license. + + +

+ + +

+ + + + + + +

1 – Introduction

+ +

+Lua is a powerful, efficient, lightweight, embeddable scripting language. +It supports procedural programming, +object-oriented programming, functional programming, +data-driven programming, and data description. + + +

+Lua combines simple procedural syntax with powerful data description +constructs based on associative arrays and extensible semantics. +Lua is dynamically typed, +runs by interpreting bytecode with a register-based +virtual machine, +and has automatic memory management with +incremental garbage collection, +making it ideal for configuration, scripting, +and rapid prototyping. + + +

+Lua is implemented as a library, written in clean C, +the common subset of Standard C and C++. +The Lua distribution includes a host program called lua, +which uses the Lua library to offer a complete, +standalone Lua interpreter, +for interactive or batch use. +Lua is intended to be used both as a powerful, lightweight, +embeddable scripting language for any program that needs one, +and as a powerful but lightweight and efficient stand-alone language. + + +

+As an extension language, Lua has no notion of a "main" program: +it works embedded in a host client, +called the embedding program or simply the host. +(Frequently, this host is the stand-alone lua program.) +The host program can invoke functions to execute a piece of Lua code, +can write and read Lua variables, +and can register C functions to be called by Lua code. +Through the use of C functions, Lua can be augmented to cope with +a wide range of different domains, +thus creating customized programming languages sharing a syntactical framework. + + +

+Lua is free software, +and is provided as usual with no guarantees, +as stated in its license. +The implementation described in this manual is available +at Lua's official web site, www.lua.org. + + +

+Like any other reference manual, +this document is dry in places. +For a discussion of the decisions behind the design of Lua, +see the technical papers available at Lua's web site. +For a detailed introduction to programming in Lua, +see Roberto's book, Programming in Lua. + + + +

2 – Basic Concepts

+ +

+This section describes the basic concepts of the language. + + + +

2.1 – Values and Types

+ +

+Lua is a dynamically typed language. +This means that +variables do not have types; only values do. +There are no type definitions in the language. +All values carry their own type. + + +

+All values in Lua are first-class values. +This means that all values can be stored in variables, +passed as arguments to other functions, and returned as results. + + +

+There are eight basic types in Lua: +nil, boolean, number, +string, function, userdata, +thread, and table. +The type nil has one single value, nil, +whose main property is to be different from any other value; +it usually represents the absence of a useful value. +The type boolean has two values, false and true. +Both nil and false make a condition false; +any other value makes it true. +The type number represents both +integer numbers and real (floating-point) numbers. +The type string represents immutable sequences of bytes. + +Lua is 8-bit clean: +strings can contain any 8-bit value, +including embedded zeros ('\0'). +Lua is also encoding-agnostic; +it makes no assumptions about the contents of a string. + + +

+The type number uses two internal representations, +or two subtypes, +one called integer and the other called float. +Lua has explicit rules about when each representation is used, +but it also converts between them automatically as needed (see §3.4.3). +Therefore, +the programmer may choose to mostly ignore the difference +between integers and floats +or to assume complete control over the representation of each number. +Standard Lua uses 64-bit integers and double-precision (64-bit) floats, +but you can also compile Lua so that it +uses 32-bit integers and/or single-precision (32-bit) floats. +The option with 32 bits for both integers and floats +is particularly attractive +for small machines and embedded systems. +(See macro LUA_32BITS in file luaconf.h.) + + +

+Lua can call (and manipulate) functions written in Lua and +functions written in C (see §3.4.10). +Both are represented by the type function. + + +

+The type userdata is provided to allow arbitrary C data to +be stored in Lua variables. +A userdata value represents a block of raw memory. +There are two kinds of userdata: +full userdata, +which is an object with a block of memory managed by Lua, +and light userdata, +which is simply a C pointer value. +Userdata has no predefined operations in Lua, +except assignment and identity test. +By using metatables, +the programmer can define operations for full userdata values +(see §2.4). +Userdata values cannot be created or modified in Lua, +only through the C API. +This guarantees the integrity of data owned by the host program. + + +

+The type thread represents independent threads of execution +and it is used to implement coroutines (see §2.6). +Lua threads are not related to operating-system threads. +Lua supports coroutines on all systems, +even those that do not support threads natively. + + +

+The type table implements associative arrays, +that is, arrays that can be indexed not only with numbers, +but with any Lua value except nil and NaN. +(Not a Number is a special value used to represent +undefined or unrepresentable numerical results, such as 0/0.) +Tables can be heterogeneous; +that is, they can contain values of all types (except nil). +Any key with value nil is not considered part of the table. +Conversely, any key that is not part of a table has +an associated value nil. + + +

+Tables are the sole data-structuring mechanism in Lua; +they can be used to represent ordinary arrays, lists, +symbol tables, sets, records, graphs, trees, etc. +To represent records, Lua uses the field name as an index. +The language supports this representation by +providing a.name as syntactic sugar for a["name"]. +There are several convenient ways to create tables in Lua +(see §3.4.9). + + +

+Like indices, +the values of table fields can be of any type. +In particular, +because functions are first-class values, +table fields can contain functions. +Thus tables can also carry methods (see §3.4.11). + + +

+The indexing of tables follows +the definition of raw equality in the language. +The expressions a[i] and a[j] +denote the same table element +if and only if i and j are raw equal +(that is, equal without metamethods). +In particular, floats with integral values +are equal to their respective integers +(e.g., 1.0 == 1). +To avoid ambiguities, +any float with integral value used as a key +is converted to its respective integer. +For instance, if you write a[2.0] = true, +the actual key inserted into the table will be the +integer 2. +(On the other hand, +2 and "2" are different Lua values and therefore +denote different table entries.) + + +

+Tables, functions, threads, and (full) userdata values are objects: +variables do not actually contain these values, +only references to them. +Assignment, parameter passing, and function returns +always manipulate references to such values; +these operations do not imply any kind of copy. + + +

+The library function type returns a string describing the type +of a given value (see §6.1). + + + + + +

2.2 – Environments and the Global Environment

+ +

+As will be discussed in §3.2 and §3.3.3, +any reference to a free name +(that is, a name not bound to any declaration) var +is syntactically translated to _ENV.var. +Moreover, every chunk is compiled in the scope of +an external local variable named _ENV (see §3.3.2), +so _ENV itself is never a free name in a chunk. + + +

+Despite the existence of this external _ENV variable and +the translation of free names, +_ENV is a completely regular name. +In particular, +you can define new variables and parameters with that name. +Each reference to a free name uses the _ENV that is +visible at that point in the program, +following the usual visibility rules of Lua (see §3.5). + + +

+Any table used as the value of _ENV is called an environment. + + +

+Lua keeps a distinguished environment called the global environment. +This value is kept at a special index in the C registry (see §4.5). +In Lua, the global variable _G is initialized with this same value. +(_G is never used internally.) + + +

+When Lua loads a chunk, +the default value for its _ENV upvalue +is the global environment (see load). +Therefore, by default, +free names in Lua code refer to entries in the global environment +(and, therefore, they are also called global variables). +Moreover, all standard libraries are loaded in the global environment +and some functions there operate on that environment. +You can use load (or loadfile) +to load a chunk with a different environment. +(In C, you have to load the chunk and then change the value +of its first upvalue.) + + + + + +

2.3 – Error Handling

+ +

+Because Lua is an embedded extension language, +all Lua actions start from C code in the host program +calling a function from the Lua library. +(When you use Lua standalone, +the lua application is the host program.) +Whenever an error occurs during +the compilation or execution of a Lua chunk, +control returns to the host, +which can take appropriate measures +(such as printing an error message). + + +

+Lua code can explicitly generate an error by calling the +error function. +If you need to catch errors in Lua, +you can use pcall or xpcall +to call a given function in protected mode. + + +

+Whenever there is an error, +an error object (also called an error message) +is propagated with information about the error. +Lua itself only generates errors whose error object is a string, +but programs may generate errors with +any value as the error object. +It is up to the Lua program or its host to handle such error objects. + + +

+When you use xpcall or lua_pcall, +you may give a message handler +to be called in case of errors. +This function is called with the original error object +and returns a new error object. +It is called before the error unwinds the stack, +so that it can gather more information about the error, +for instance by inspecting the stack and creating a stack traceback. +This message handler is still protected by the protected call; +so, an error inside the message handler +will call the message handler again. +If this loop goes on for too long, +Lua breaks it and returns an appropriate message. +(The message handler is called only for regular runtime errors. +It is not called for memory-allocation errors +nor for errors while running finalizers.) + + + + + +

2.4 – Metatables and Metamethods

+ +

+Every value in Lua can have a metatable. +This metatable is an ordinary Lua table +that defines the behavior of the original value +under certain special operations. +You can change several aspects of the behavior +of operations over a value by setting specific fields in its metatable. +For instance, when a non-numeric value is the operand of an addition, +Lua checks for a function in the field "__add" of the value's metatable. +If it finds one, +Lua calls this function to perform the addition. + + +

+The key for each event in a metatable is a string +with the event name prefixed by two underscores; +the corresponding values are called metamethods. +In the previous example, the key is "__add" +and the metamethod is the function that performs the addition. + + +

+You can query the metatable of any value +using the getmetatable function. +Lua queries metamethods in metatables using a raw access (see rawget). +So, to retrieve the metamethod for event ev in object o, +Lua does the equivalent to the following code: + +

+     rawget(getmetatable(o) or {}, "__ev")
+
+ +

+You can replace the metatable of tables +using the setmetatable function. +You cannot change the metatable of other types from Lua code +(except by using the debug library (§6.10)); +you should use the C API for that. + + +

+Tables and full userdata have individual metatables +(although multiple tables and userdata can share their metatables). +Values of all other types share one single metatable per type; +that is, there is one single metatable for all numbers, +one for all strings, etc. +By default, a value has no metatable, +but the string library sets a metatable for the string type (see §6.4). + + +

+A metatable controls how an object behaves in +arithmetic operations, bitwise operations, +order comparisons, concatenation, length operation, calls, and indexing. +A metatable also can define a function to be called +when a userdata or a table is garbage collected (§2.5). + + +

+For the unary operators (negation, length, and bitwise NOT), +the metamethod is computed and called with a dummy second operand, +equal to the first one. +This extra operand is only to simplify Lua's internals +(by making these operators behave like a binary operation) +and may be removed in future versions. +(For most uses this extra operand is irrelevant.) + + +

+A detailed list of events controlled by metatables is given next. +Each operation is identified by its corresponding key. + + + +

    + +
  • __add: +the addition (+) operation. +If any operand for an addition is not a number +(nor a string coercible to a number), +Lua will try to call a metamethod. +First, Lua will check the first operand (even if it is valid). +If that operand does not define a metamethod for __add, +then Lua will check the second operand. +If Lua can find a metamethod, +it calls the metamethod with the two operands as arguments, +and the result of the call +(adjusted to one value) +is the result of the operation. +Otherwise, +it raises an error. +
  • + +
  • __sub: +the subtraction (-) operation. +Behavior similar to the addition operation. +
  • + +
  • __mul: +the multiplication (*) operation. +Behavior similar to the addition operation. +
  • + +
  • __div: +the division (/) operation. +Behavior similar to the addition operation. +
  • + +
  • __mod: +the modulo (%) operation. +Behavior similar to the addition operation. +
  • + +
  • __pow: +the exponentiation (^) operation. +Behavior similar to the addition operation. +
  • + +
  • __unm: +the negation (unary -) operation. +Behavior similar to the addition operation. +
  • + +
  • __idiv: +the floor division (//) operation. +Behavior similar to the addition operation. +
  • + +
  • __band: +the bitwise AND (&) operation. +Behavior similar to the addition operation, +except that Lua will try a metamethod +if any operand is neither an integer +nor a value coercible to an integer (see §3.4.3). +
  • + +
  • __bor: +the bitwise OR (|) operation. +Behavior similar to the bitwise AND operation. +
  • + +
  • __bxor: +the bitwise exclusive OR (binary ~) operation. +Behavior similar to the bitwise AND operation. +
  • + +
  • __bnot: +the bitwise NOT (unary ~) operation. +Behavior similar to the bitwise AND operation. +
  • + +
  • __shl: +the bitwise left shift (<<) operation. +Behavior similar to the bitwise AND operation. +
  • + +
  • __shr: +the bitwise right shift (>>) operation. +Behavior similar to the bitwise AND operation. +
  • + +
  • __concat: +the concatenation (..) operation. +Behavior similar to the addition operation, +except that Lua will try a metamethod +if any operand is neither a string nor a number +(which is always coercible to a string). +
  • + +
  • __len: +the length (#) operation. +If the object is not a string, +Lua will try its metamethod. +If there is a metamethod, +Lua calls it with the object as argument, +and the result of the call +(always adjusted to one value) +is the result of the operation. +If there is no metamethod but the object is a table, +then Lua uses the table length operation (see §3.4.7). +Otherwise, Lua raises an error. +
  • + +
  • __eq: +the equal (==) operation. +Behavior similar to the addition operation, +except that Lua will try a metamethod only when the values +being compared are either both tables or both full userdata +and they are not primitively equal. +The result of the call is always converted to a boolean. +
  • + +
  • __lt: +the less than (<) operation. +Behavior similar to the addition operation, +except that Lua will try a metamethod only when the values +being compared are neither both numbers nor both strings. +The result of the call is always converted to a boolean. +
  • + +
  • __le: +the less equal (<=) operation. +Unlike other operations, +the less-equal operation can use two different events. +First, Lua looks for the __le metamethod in both operands, +like in the less than operation. +If it cannot find such a metamethod, +then it will try the __lt metamethod, +assuming that a <= b is equivalent to not (b < a). +As with the other comparison operators, +the result is always a boolean. +(This use of the __lt event can be removed in future versions; +it is also slower than a real __le metamethod.) +
  • + +
  • __index: +The indexing access table[key]. +This event happens when table is not a table or +when key is not present in table. +The metamethod is looked up in table. + + +

    +Despite the name, +the metamethod for this event can be either a function or a table. +If it is a function, +it is called with table and key as arguments, +and the result of the call +(adjusted to one value) +is the result of the operation. +If it is a table, +the final result is the result of indexing this table with key. +(This indexing is regular, not raw, +and therefore can trigger another metamethod.) +

  • + +
  • __newindex: +The indexing assignment table[key] = value. +Like the index event, +this event happens when table is not a table or +when key is not present in table. +The metamethod is looked up in table. + + +

    +Like with indexing, +the metamethod for this event can be either a function or a table. +If it is a function, +it is called with table, key, and value as arguments. +If it is a table, +Lua does an indexing assignment to this table with the same key and value. +(This assignment is regular, not raw, +and therefore can trigger another metamethod.) + + +

    +Whenever there is a __newindex metamethod, +Lua does not perform the primitive assignment. +(If necessary, +the metamethod itself can call rawset +to do the assignment.) +

  • + +
  • __call: +The call operation func(args). +This event happens when Lua tries to call a non-function value +(that is, func is not a function). +The metamethod is looked up in func. +If present, +the metamethod is called with func as its first argument, +followed by the arguments of the original call (args). +All results of the call +are the result of the operation. +(This is the only metamethod that allows multiple results.) +
  • + +
+ +

+It is a good practice to add all needed metamethods to a table +before setting it as a metatable of some object. +In particular, the __gc metamethod works only when this order +is followed (see §2.5.1). + + +

+Because metatables are regular tables, +they can contain arbitrary fields, +not only the event names defined above. +Some functions in the standard library +(e.g., tostring) +use other fields in metatables for their own purposes. + + + + + +

2.5 – Garbage Collection

+ +

+Lua performs automatic memory management. +This means that +you do not have to worry about allocating memory for new objects +or freeing it when the objects are no longer needed. +Lua manages memory automatically by running +a garbage collector to collect all dead objects +(that is, objects that are no longer accessible from Lua). +All memory used by Lua is subject to automatic management: +strings, tables, userdata, functions, threads, internal structures, etc. + + +

+Lua implements an incremental mark-and-sweep collector. +It uses two numbers to control its garbage-collection cycles: +the garbage-collector pause and +the garbage-collector step multiplier. +Both use percentage points as units +(e.g., a value of 100 means an internal value of 1). + + +

+The garbage-collector pause +controls how long the collector waits before starting a new cycle. +Larger values make the collector less aggressive. +Values smaller than 100 mean the collector will not wait to +start a new cycle. +A value of 200 means that the collector waits for the total memory in use +to double before starting a new cycle. + + +

+The garbage-collector step multiplier +controls the relative speed of the collector relative to +memory allocation. +Larger values make the collector more aggressive but also increase +the size of each incremental step. +You should not use values smaller than 100, +because they make the collector too slow and +can result in the collector never finishing a cycle. +The default is 200, +which means that the collector runs at "twice" +the speed of memory allocation. + + +

+If you set the step multiplier to a very large number +(larger than 10% of the maximum number of +bytes that the program may use), +the collector behaves like a stop-the-world collector. +If you then set the pause to 200, +the collector behaves as in old Lua versions, +doing a complete collection every time Lua doubles its +memory usage. + + +

+You can change these numbers by calling lua_gc in C +or collectgarbage in Lua. +You can also use these functions to control +the collector directly (e.g., stop and restart it). + + + +

2.5.1 – Garbage-Collection Metamethods

+ +

+You can set garbage-collector metamethods for tables +and, using the C API, +for full userdata (see §2.4). +These metamethods are also called finalizers. +Finalizers allow you to coordinate Lua's garbage collection +with external resource management +(such as closing files, network or database connections, +or freeing your own memory). + + +

+For an object (table or userdata) to be finalized when collected, +you must mark it for finalization. + +You mark an object for finalization when you set its metatable +and the metatable has a field indexed by the string "__gc". +Note that if you set a metatable without a __gc field +and later create that field in the metatable, +the object will not be marked for finalization. + + +

+When a marked object becomes garbage, +it is not collected immediately by the garbage collector. +Instead, Lua puts it in a list. +After the collection, +Lua goes through that list. +For each object in the list, +it checks the object's __gc metamethod: +If it is a function, +Lua calls it with the object as its single argument; +if the metamethod is not a function, +Lua simply ignores it. + + +

+At the end of each garbage-collection cycle, +the finalizers for objects are called in +the reverse order that the objects were marked for finalization, +among those collected in that cycle; +that is, the first finalizer to be called is the one associated +with the object marked last in the program. +The execution of each finalizer may occur at any point during +the execution of the regular code. + + +

+Because the object being collected must still be used by the finalizer, +that object (and other objects accessible only through it) +must be resurrected by Lua. +Usually, this resurrection is transient, +and the object memory is freed in the next garbage-collection cycle. +However, if the finalizer stores the object in some global place +(e.g., a global variable), +then the resurrection is permanent. +Moreover, if the finalizer marks a finalizing object for finalization again, +its finalizer will be called again in the next cycle where the +object is unreachable. +In any case, +the object memory is freed only in a GC cycle where +the object is unreachable and not marked for finalization. + + +

+When you close a state (see lua_close), +Lua calls the finalizers of all objects marked for finalization, +following the reverse order that they were marked. +If any finalizer marks objects for collection during that phase, +these marks have no effect. + + + + + +

2.5.2 – Weak Tables

+ +

+A weak table is a table whose elements are +weak references. +A weak reference is ignored by the garbage collector. +In other words, +if the only references to an object are weak references, +then the garbage collector will collect that object. + + +

+A weak table can have weak keys, weak values, or both. +A table with weak values allows the collection of its values, +but prevents the collection of its keys. +A table with both weak keys and weak values allows the collection of +both keys and values. +In any case, if either the key or the value is collected, +the whole pair is removed from the table. +The weakness of a table is controlled by the +__mode field of its metatable. +If the __mode field is a string containing the character 'k', +the keys in the table are weak. +If __mode contains 'v', +the values in the table are weak. + + +

+A table with weak keys and strong values +is also called an ephemeron table. +In an ephemeron table, +a value is considered reachable only if its key is reachable. +In particular, +if the only reference to a key comes through its value, +the pair is removed. + + +

+Any change in the weakness of a table may take effect only +at the next collect cycle. +In particular, if you change the weakness to a stronger mode, +Lua may still collect some items from that table +before the change takes effect. + + +

+Only objects that have an explicit construction +are removed from weak tables. +Values, such as numbers and light C functions, +are not subject to garbage collection, +and therefore are not removed from weak tables +(unless their associated values are collected). +Although strings are subject to garbage collection, +they do not have an explicit construction, +and therefore are not removed from weak tables. + + +

+Resurrected objects +(that is, objects being finalized +and objects accessible only through objects being finalized) +have a special behavior in weak tables. +They are removed from weak values before running their finalizers, +but are removed from weak keys only in the next collection +after running their finalizers, when such objects are actually freed. +This behavior allows the finalizer to access properties +associated with the object through weak tables. + + +

+If a weak table is among the resurrected objects in a collection cycle, +it may not be properly cleared until the next cycle. + + + + + + + +

2.6 – Coroutines

+ +

+Lua supports coroutines, +also called collaborative multithreading. +A coroutine in Lua represents an independent thread of execution. +Unlike threads in multithread systems, however, +a coroutine only suspends its execution by explicitly calling +a yield function. + + +

+You create a coroutine by calling coroutine.create. +Its sole argument is a function +that is the main function of the coroutine. +The create function only creates a new coroutine and +returns a handle to it (an object of type thread); +it does not start the coroutine. + + +

+You execute a coroutine by calling coroutine.resume. +When you first call coroutine.resume, +passing as its first argument +a thread returned by coroutine.create, +the coroutine starts its execution by +calling its main function. +Extra arguments passed to coroutine.resume are passed +as arguments to that function. +After the coroutine starts running, +it runs until it terminates or yields. + + +

+A coroutine can terminate its execution in two ways: +normally, when its main function returns +(explicitly or implicitly, after the last instruction); +and abnormally, if there is an unprotected error. +In case of normal termination, +coroutine.resume returns true, +plus any values returned by the coroutine main function. +In case of errors, coroutine.resume returns false +plus an error object. + + +

+A coroutine yields by calling coroutine.yield. +When a coroutine yields, +the corresponding coroutine.resume returns immediately, +even if the yield happens inside nested function calls +(that is, not in the main function, +but in a function directly or indirectly called by the main function). +In the case of a yield, coroutine.resume also returns true, +plus any values passed to coroutine.yield. +The next time you resume the same coroutine, +it continues its execution from the point where it yielded, +with the call to coroutine.yield returning any extra +arguments passed to coroutine.resume. + + +

+Like coroutine.create, +the coroutine.wrap function also creates a coroutine, +but instead of returning the coroutine itself, +it returns a function that, when called, resumes the coroutine. +Any arguments passed to this function +go as extra arguments to coroutine.resume. +coroutine.wrap returns all the values returned by coroutine.resume, +except the first one (the boolean error code). +Unlike coroutine.resume, +coroutine.wrap does not catch errors; +any error is propagated to the caller. + + +

+As an example of how coroutines work, +consider the following code: + +

+     function foo (a)
+       print("foo", a)
+       return coroutine.yield(2*a)
+     end
+     
+     co = coroutine.create(function (a,b)
+           print("co-body", a, b)
+           local r = foo(a+1)
+           print("co-body", r)
+           local r, s = coroutine.yield(a+b, a-b)
+           print("co-body", r, s)
+           return b, "end"
+     end)
+     
+     print("main", coroutine.resume(co, 1, 10))
+     print("main", coroutine.resume(co, "r"))
+     print("main", coroutine.resume(co, "x", "y"))
+     print("main", coroutine.resume(co, "x", "y"))
+

+When you run it, it produces the following output: + +

+     co-body 1       10
+     foo     2
+     main    true    4
+     co-body r
+     main    true    11      -9
+     co-body x       y
+     main    true    10      end
+     main    false   cannot resume dead coroutine
+
+ +

+You can also create and manipulate coroutines through the C API: +see functions lua_newthread, lua_resume, +and lua_yield. + + + + + +

3 – The Language

+ +

+This section describes the lexis, the syntax, and the semantics of Lua. +In other words, +this section describes +which tokens are valid, +how they can be combined, +and what their combinations mean. + + +

+Language constructs will be explained using the usual extended BNF notation, +in which +{a} means 0 or more a's, and +[a] means an optional a. +Non-terminals are shown like non-terminal, +keywords are shown like kword, +and other terminal symbols are shown like ‘=’. +The complete syntax of Lua can be found in §9 +at the end of this manual. + + + +

3.1 – Lexical Conventions

+ +

+Lua is a free-form language. +It ignores spaces (including new lines) and comments +between lexical elements (tokens), +except as delimiters between names and keywords. + + +

+Names +(also called identifiers) +in Lua can be any string of letters, +digits, and underscores, +not beginning with a digit and +not being a reserved word. +Identifiers are used to name variables, table fields, and labels. + + +

+The following keywords are reserved +and cannot be used as names: + + +

+     and       break     do        else      elseif    end
+     false     for       function  goto      if        in
+     local     nil       not       or        repeat    return
+     then      true      until     while
+
+ +

+Lua is a case-sensitive language: +and is a reserved word, but And and AND +are two different, valid names. +As a convention, +programs should avoid creating +names that start with an underscore followed by +one or more uppercase letters (such as _VERSION). + + +

+The following strings denote other tokens: + +

+     +     -     *     /     %     ^     #
+     &     ~     |     <<    >>    //
+     ==    ~=    <=    >=    <     >     =
+     (     )     {     }     [     ]     ::
+     ;     :     ,     .     ..    ...
+
+ +

+A short literal string +can be delimited by matching single or double quotes, +and can contain the following C-like escape sequences: +'\a' (bell), +'\b' (backspace), +'\f' (form feed), +'\n' (newline), +'\r' (carriage return), +'\t' (horizontal tab), +'\v' (vertical tab), +'\\' (backslash), +'\"' (quotation mark [double quote]), +and '\'' (apostrophe [single quote]). +A backslash followed by a line break +results in a newline in the string. +The escape sequence '\z' skips the following span +of white-space characters, +including line breaks; +it is particularly useful to break and indent a long literal string +into multiple lines without adding the newlines and spaces +into the string contents. +A short literal string cannot contain unescaped line breaks +nor escapes not forming a valid escape sequence. + + +

+We can specify any byte in a short literal string by its numeric value +(including embedded zeros). +This can be done +with the escape sequence \xXX, +where XX is a sequence of exactly two hexadecimal digits, +or with the escape sequence \ddd, +where ddd is a sequence of up to three decimal digits. +(Note that if a decimal escape sequence is to be followed by a digit, +it must be expressed using exactly three digits.) + + +

+The UTF-8 encoding of a Unicode character +can be inserted in a literal string with +the escape sequence \u{XXX} +(note the mandatory enclosing brackets), +where XXX is a sequence of one or more hexadecimal digits +representing the character code point. + + +

+Literal strings can also be defined using a long format +enclosed by long brackets. +We define an opening long bracket of level n as an opening +square bracket followed by n equal signs followed by another +opening square bracket. +So, an opening long bracket of level 0 is written as [[, +an opening long bracket of level 1 is written as [=[, +and so on. +A closing long bracket is defined similarly; +for instance, +a closing long bracket of level 4 is written as ]====]. +A long literal starts with an opening long bracket of any level and +ends at the first closing long bracket of the same level. +It can contain any text except a closing bracket of the same level. +Literals in this bracketed form can run for several lines, +do not interpret any escape sequences, +and ignore long brackets of any other level. +Any kind of end-of-line sequence +(carriage return, newline, carriage return followed by newline, +or newline followed by carriage return) +is converted to a simple newline. + + +

+For convenience, +when the opening long bracket is immediately followed by a newline, +the newline is not included in the string. +As an example, in a system using ASCII +(in which 'a' is coded as 97, +newline is coded as 10, and '1' is coded as 49), +the five literal strings below denote the same string: + +

+     a = 'alo\n123"'
+     a = "alo\n123\""
+     a = '\97lo\10\04923"'
+     a = [[alo
+     123"]]
+     a = [==[
+     alo
+     123"]==]
+
+ +

+Any byte in a literal string not +explicitly affected by the previous rules represents itself. +However, Lua opens files for parsing in text mode, +and the system file functions may have problems with +some control characters. +So, it is safer to represent +non-text data as a quoted literal with +explicit escape sequences for the non-text characters. + + +

+A numeric constant (or numeral) +can be written with an optional fractional part +and an optional decimal exponent, +marked by a letter 'e' or 'E'. +Lua also accepts hexadecimal constants, +which start with 0x or 0X. +Hexadecimal constants also accept an optional fractional part +plus an optional binary exponent, +marked by a letter 'p' or 'P'. +A numeric constant with a radix point or an exponent +denotes a float; +otherwise, +if its value fits in an integer, +it denotes an integer. +Examples of valid integer constants are + +

+     3   345   0xff   0xBEBADA
+

+Examples of valid float constants are + +

+     3.0     3.1416     314.16e-2     0.31416E1     34e1
+     0x0.1E  0xA23p-4   0X1.921FB54442D18P+1
+
+ +

+A comment starts with a double hyphen (--) +anywhere outside a string. +If the text immediately after -- is not an opening long bracket, +the comment is a short comment, +which runs until the end of the line. +Otherwise, it is a long comment, +which runs until the corresponding closing long bracket. +Long comments are frequently used to disable code temporarily. + + + + + +

3.2 – Variables

+ +

+Variables are places that store values. +There are three kinds of variables in Lua: +global variables, local variables, and table fields. + + +

+A single name can denote a global variable or a local variable +(or a function's formal parameter, +which is a particular kind of local variable): + +

+	var ::= Name
+

+Name denotes identifiers, as defined in §3.1. + + +

+Any variable name is assumed to be global unless explicitly declared +as a local (see §3.3.7). +Local variables are lexically scoped: +local variables can be freely accessed by functions +defined inside their scope (see §3.5). + + +

+Before the first assignment to a variable, its value is nil. + + +

+Square brackets are used to index a table: + +

+	var ::= prefixexp ‘[’ exp ‘]’
+

+The meaning of accesses to table fields can be changed via metatables. +An access to an indexed variable t[i] is equivalent to +a call gettable_event(t,i). +(See §2.4 for a complete description of the +gettable_event function. +This function is not defined or callable in Lua. +We use it here only for explanatory purposes.) + + +

+The syntax var.Name is just syntactic sugar for +var["Name"]: + +

+	var ::= prefixexp ‘.’ Name
+
+ +

+An access to a global variable x +is equivalent to _ENV.x. +Due to the way that chunks are compiled, +_ENV is never a global name (see §2.2). + + + + + +

3.3 – Statements

+ +

+Lua supports an almost conventional set of statements, +similar to those in Pascal or C. +This set includes +assignments, control structures, function calls, +and variable declarations. + + + +

3.3.1 – Blocks

+ +

+A block is a list of statements, +which are executed sequentially: + +

+	block ::= {stat}
+

+Lua has empty statements +that allow you to separate statements with semicolons, +start a block with a semicolon +or write two semicolons in sequence: + +

+	stat ::= ‘;’
+
+ +

+Function calls and assignments +can start with an open parenthesis. +This possibility leads to an ambiguity in Lua's grammar. +Consider the following fragment: + +

+     a = b + c
+     (print or io.write)('done')
+

+The grammar could see it in two ways: + +

+     a = b + c(print or io.write)('done')
+     
+     a = b + c; (print or io.write)('done')
+

+The current parser always sees such constructions +in the first way, +interpreting the open parenthesis +as the start of the arguments to a call. +To avoid this ambiguity, +it is a good practice to always precede with a semicolon +statements that start with a parenthesis: + +

+     ;(print or io.write)('done')
+
+ +

+A block can be explicitly delimited to produce a single statement: + +

+	stat ::= do block end
+

+Explicit blocks are useful +to control the scope of variable declarations. +Explicit blocks are also sometimes used to +add a return statement in the middle +of another block (see §3.3.4). + + + + + +

3.3.2 – Chunks

+ +

+The unit of compilation of Lua is called a chunk. +Syntactically, +a chunk is simply a block: + +

+	chunk ::= block
+
+ +

+Lua handles a chunk as the body of an anonymous function +with a variable number of arguments +(see §3.4.11). +As such, chunks can define local variables, +receive arguments, and return values. +Moreover, such anonymous function is compiled as in the +scope of an external local variable called _ENV (see §2.2). +The resulting function always has _ENV as its only upvalue, +even if it does not use that variable. + + +

+A chunk can be stored in a file or in a string inside the host program. +To execute a chunk, +Lua first loads it, +precompiling the chunk's code into instructions for a virtual machine, +and then Lua executes the compiled code +with an interpreter for the virtual machine. + + +

+Chunks can also be precompiled into binary form; +see program luac and function string.dump for details. +Programs in source and compiled forms are interchangeable; +Lua automatically detects the file type and acts accordingly (see load). + + + + + +

3.3.3 – Assignment

+ +

+Lua allows multiple assignments. +Therefore, the syntax for assignment +defines a list of variables on the left side +and a list of expressions on the right side. +The elements in both lists are separated by commas: + +

+	stat ::= varlist ‘=’ explist
+	varlist ::= var {‘,’ var}
+	explist ::= exp {‘,’ exp}
+

+Expressions are discussed in §3.4. + + +

+Before the assignment, +the list of values is adjusted to the length of +the list of variables. +If there are more values than needed, +the excess values are thrown away. +If there are fewer values than needed, +the list is extended with as many nil's as needed. +If the list of expressions ends with a function call, +then all values returned by that call enter the list of values, +before the adjustment +(except when the call is enclosed in parentheses; see §3.4). + + +

+The assignment statement first evaluates all its expressions +and only then the assignments are performed. +Thus the code + +

+     i = 3
+     i, a[i] = i+1, 20
+

+sets a[3] to 20, without affecting a[4] +because the i in a[i] is evaluated (to 3) +before it is assigned 4. +Similarly, the line + +

+     x, y = y, x
+

+exchanges the values of x and y, +and + +

+     x, y, z = y, z, x
+

+cyclically permutes the values of x, y, and z. + + +

+The meaning of assignments to global variables +and table fields can be changed via metatables. +An assignment to an indexed variable t[i] = val is equivalent to +settable_event(t,i,val). +(See §2.4 for a complete description of the +settable_event function. +This function is not defined or callable in Lua. +We use it here only for explanatory purposes.) + + +

+An assignment to a global name x = val +is equivalent to the assignment +_ENV.x = val (see §2.2). + + + + + +

3.3.4 – Control Structures

+The control structures +if, while, and repeat have the usual meaning and +familiar syntax: + + + + +

+	stat ::= while exp do block end
+	stat ::= repeat block until exp
+	stat ::= if exp then block {elseif exp then block} [else block] end
+

+Lua also has a for statement, in two flavors (see §3.3.5). + + +

+The condition expression of a +control structure can return any value. +Both false and nil are considered false. +All values different from nil and false are considered true +(in particular, the number 0 and the empty string are also true). + + +

+In the repeatuntil loop, +the inner block does not end at the until keyword, +but only after the condition. +So, the condition can refer to local variables +declared inside the loop block. + + +

+The goto statement transfers the program control to a label. +For syntactical reasons, +labels in Lua are considered statements too: + + + +

+	stat ::= goto Name
+	stat ::= label
+	label ::= ‘::’ Name ‘::’
+
+ +

+A label is visible in the entire block where it is defined, +except +inside nested blocks where a label with the same name is defined and +inside nested functions. +A goto may jump to any visible label as long as it does not +enter into the scope of a local variable. + + +

+Labels and empty statements are called void statements, +as they perform no actions. + + +

+The break statement terminates the execution of a +while, repeat, or for loop, +skipping to the next statement after the loop: + + +

+	stat ::= break
+

+A break ends the innermost enclosing loop. + + +

+The return statement is used to return values +from a function or a chunk +(which is an anonymous function). + +Functions can return more than one value, +so the syntax for the return statement is + +

+	stat ::= return [explist] [‘;’]
+
+ +

+The return statement can only be written +as the last statement of a block. +If it is really necessary to return in the middle of a block, +then an explicit inner block can be used, +as in the idiom do return end, +because now return is the last statement in its (inner) block. + + + + + +

3.3.5 – For Statement

+ +

+ +The for statement has two forms: +one numerical and one generic. + + +

+The numerical for loop repeats a block of code while a +control variable runs through an arithmetic progression. +It has the following syntax: + +

+	stat ::= for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end
+

+The block is repeated for name starting at the value of +the first exp, until it passes the second exp by steps of the +third exp. +More precisely, a for statement like + +

+     for v = e1, e2, e3 do block end
+

+is equivalent to the code: + +

+     do
+       local var, limit, step = tonumber(e1), tonumber(e2), tonumber(e3)
+       if not (var and limit and step) then error() end
+       var = var - step
+       while true do
+         var = var + step
+         if (step >= 0 and var > limit) or (step < 0 and var < limit) then
+           break
+         end
+         local v = var
+         block
+       end
+     end
+
+ +

+Note the following: + +

    + +
  • +All three control expressions are evaluated only once, +before the loop starts. +They must all result in numbers. +
  • + +
  • +var, limit, and step are invisible variables. +The names shown here are for explanatory purposes only. +
  • + +
  • +If the third expression (the step) is absent, +then a step of 1 is used. +
  • + +
  • +You can use break and goto to exit a for loop. +
  • + +
  • +The loop variable v is local to the loop body. +If you need its value after the loop, +assign it to another variable before exiting the loop. +
  • + +
+ +

+The generic for statement works over functions, +called iterators. +On each iteration, the iterator function is called to produce a new value, +stopping when this new value is nil. +The generic for loop has the following syntax: + +

+	stat ::= for namelist in explist do block end
+	namelist ::= Name {‘,’ Name}
+

+A for statement like + +

+     for var_1, ···, var_n in explist do block end
+

+is equivalent to the code: + +

+     do
+       local f, s, var = explist
+       while true do
+         local var_1, ···, var_n = f(s, var)
+         if var_1 == nil then break end
+         var = var_1
+         block
+       end
+     end
+

+Note the following: + +

    + +
  • +explist is evaluated only once. +Its results are an iterator function, +a state, +and an initial value for the first iterator variable. +
  • + +
  • +f, s, and var are invisible variables. +The names are here for explanatory purposes only. +
  • + +
  • +You can use break to exit a for loop. +
  • + +
  • +The loop variables var_i are local to the loop; +you cannot use their values after the for ends. +If you need these values, +then assign them to other variables before breaking or exiting the loop. +
  • + +
+ + + + +

3.3.6 – Function Calls as Statements

+To allow possible side-effects, +function calls can be executed as statements: + +

+	stat ::= functioncall
+

+In this case, all returned values are thrown away. +Function calls are explained in §3.4.10. + + + + + +

3.3.7 – Local Declarations

+Local variables can be declared anywhere inside a block. +The declaration can include an initial assignment: + +

+	stat ::= local namelist [‘=’ explist]
+

+If present, an initial assignment has the same semantics +of a multiple assignment (see §3.3.3). +Otherwise, all variables are initialized with nil. + + +

+A chunk is also a block (see §3.3.2), +and so local variables can be declared in a chunk outside any explicit block. + + +

+The visibility rules for local variables are explained in §3.5. + + + + + + + +

3.4 – Expressions

+ +

+The basic expressions in Lua are the following: + +

+	exp ::= prefixexp
+	exp ::= nil | false | true
+	exp ::= Numeral
+	exp ::= LiteralString
+	exp ::= functiondef
+	exp ::= tableconstructor
+	exp ::= ‘...’
+	exp ::= exp binop exp
+	exp ::= unop exp
+	prefixexp ::= var | functioncall | ‘(’ exp ‘)’
+
+ +

+Numerals and literal strings are explained in §3.1; +variables are explained in §3.2; +function definitions are explained in §3.4.11; +function calls are explained in §3.4.10; +table constructors are explained in §3.4.9. +Vararg expressions, +denoted by three dots ('...'), can only be used when +directly inside a vararg function; +they are explained in §3.4.11. + + +

+Binary operators comprise arithmetic operators (see §3.4.1), +bitwise operators (see §3.4.2), +relational operators (see §3.4.4), logical operators (see §3.4.5), +and the concatenation operator (see §3.4.6). +Unary operators comprise the unary minus (see §3.4.1), +the unary bitwise NOT (see §3.4.2), +the unary logical not (see §3.4.5), +and the unary length operator (see §3.4.7). + + +

+Both function calls and vararg expressions can result in multiple values. +If a function call is used as a statement (see §3.3.6), +then its return list is adjusted to zero elements, +thus discarding all returned values. +If an expression is used as the last (or the only) element +of a list of expressions, +then no adjustment is made +(unless the expression is enclosed in parentheses). +In all other contexts, +Lua adjusts the result list to one element, +either discarding all values except the first one +or adding a single nil if there are no values. + + +

+Here are some examples: + +

+     f()                -- adjusted to 0 results
+     g(f(), x)          -- f() is adjusted to 1 result
+     g(x, f())          -- g gets x plus all results from f()
+     a,b,c = f(), x     -- f() is adjusted to 1 result (c gets nil)
+     a,b = ...          -- a gets the first vararg parameter, b gets
+                        -- the second (both a and b can get nil if there
+                        -- is no corresponding vararg parameter)
+     
+     a,b,c = x, f()     -- f() is adjusted to 2 results
+     a,b,c = f()        -- f() is adjusted to 3 results
+     return f()         -- returns all results from f()
+     return ...         -- returns all received vararg parameters
+     return x,y,f()     -- returns x, y, and all results from f()
+     {f()}              -- creates a list with all results from f()
+     {...}              -- creates a list with all vararg parameters
+     {f(), nil}         -- f() is adjusted to 1 result
+
+ +

+Any expression enclosed in parentheses always results in only one value. +Thus, +(f(x,y,z)) is always a single value, +even if f returns several values. +(The value of (f(x,y,z)) is the first value returned by f +or nil if f does not return any values.) + + + +

3.4.1 – Arithmetic Operators

+Lua supports the following arithmetic operators: + +

    +
  • +: addition
  • +
  • -: subtraction
  • +
  • *: multiplication
  • +
  • /: float division
  • +
  • //: floor division
  • +
  • %: modulo
  • +
  • ^: exponentiation
  • +
  • -: unary minus
  • +
+ +

+With the exception of exponentiation and float division, +the arithmetic operators work as follows: +If both operands are integers, +the operation is performed over integers and the result is an integer. +Otherwise, if both operands are numbers +or strings that can be converted to +numbers (see §3.4.3), +then they are converted to floats, +the operation is performed following the usual rules +for floating-point arithmetic +(usually the IEEE 754 standard), +and the result is a float. + + +

+Exponentiation and float division (/) +always convert their operands to floats +and the result is always a float. +Exponentiation uses the ISO C function pow, +so that it works for non-integer exponents too. + + +

+Floor division (//) is a division +that rounds the quotient towards minus infinity, +that is, the floor of the division of its operands. + + +

+Modulo is defined as the remainder of a division +that rounds the quotient towards minus infinity (floor division). + + +

+In case of overflows in integer arithmetic, +all operations wrap around, +according to the usual rules of two-complement arithmetic. +(In other words, +they return the unique representable integer +that is equal modulo 264 to the mathematical result.) + + + +

3.4.2 – Bitwise Operators

+Lua supports the following bitwise operators: + +

    +
  • &: bitwise AND
  • +
  • |: bitwise OR
  • +
  • ~: bitwise exclusive OR
  • +
  • >>: right shift
  • +
  • <<: left shift
  • +
  • ~: unary bitwise NOT
  • +
+ +

+All bitwise operations convert its operands to integers +(see §3.4.3), +operate on all bits of those integers, +and result in an integer. + + +

+Both right and left shifts fill the vacant bits with zeros. +Negative displacements shift to the other direction; +displacements with absolute values equal to or higher than +the number of bits in an integer +result in zero (as all bits are shifted out). + + + + + +

3.4.3 – Coercions and Conversions

+Lua provides some automatic conversions between some +types and representations at run time. +Bitwise operators always convert float operands to integers. +Exponentiation and float division +always convert integer operands to floats. +All other arithmetic operations applied to mixed numbers +(integers and floats) convert the integer operand to a float; +this is called the usual rule. +The C API also converts both integers to floats and +floats to integers, as needed. +Moreover, string concatenation accepts numbers as arguments, +besides strings. + + +

+Lua also converts strings to numbers, +whenever a number is expected. + + +

+In a conversion from integer to float, +if the integer value has an exact representation as a float, +that is the result. +Otherwise, +the conversion gets the nearest higher or +the nearest lower representable value. +This kind of conversion never fails. + + +

+The conversion from float to integer +checks whether the float has an exact representation as an integer +(that is, the float has an integral value and +it is in the range of integer representation). +If it does, that representation is the result. +Otherwise, the conversion fails. + + +

+The conversion from strings to numbers goes as follows: +First, the string is converted to an integer or a float, +following its syntax and the rules of the Lua lexer. +(The string may have also leading and trailing spaces and a sign.) +Then, the resulting number (float or integer) +is converted to the type (float or integer) required by the context +(e.g., the operation that forced the conversion). + + +

+All conversions from strings to numbers +accept both a dot and the current locale mark +as the radix character. +(The Lua lexer, however, accepts only a dot.) + + +

+The conversion from numbers to strings uses a +non-specified human-readable format. +For complete control over how numbers are converted to strings, +use the format function from the string library +(see string.format). + + + + + +

3.4.4 – Relational Operators

+Lua supports the following relational operators: + +

    +
  • ==: equality
  • +
  • ~=: inequality
  • +
  • <: less than
  • +
  • >: greater than
  • +
  • <=: less or equal
  • +
  • >=: greater or equal
  • +

+These operators always result in false or true. + + +

+Equality (==) first compares the type of its operands. +If the types are different, then the result is false. +Otherwise, the values of the operands are compared. +Strings are compared in the obvious way. +Numbers are equal if they denote the same mathematical value. + + +

+Tables, userdata, and threads +are compared by reference: +two objects are considered equal only if they are the same object. +Every time you create a new object +(a table, userdata, or thread), +this new object is different from any previously existing object. +Closures with the same reference are always equal. +Closures with any detectable difference +(different behavior, different definition) are always different. + + +

+You can change the way that Lua compares tables and userdata +by using the "eq" metamethod (see §2.4). + + +

+Equality comparisons do not convert strings to numbers +or vice versa. +Thus, "0"==0 evaluates to false, +and t[0] and t["0"] denote different +entries in a table. + + +

+The operator ~= is exactly the negation of equality (==). + + +

+The order operators work as follows. +If both arguments are numbers, +then they are compared according to their mathematical values +(regardless of their subtypes). +Otherwise, if both arguments are strings, +then their values are compared according to the current locale. +Otherwise, Lua tries to call the "lt" or the "le" +metamethod (see §2.4). +A comparison a > b is translated to b < a +and a >= b is translated to b <= a. + + +

+Following the IEEE 754 standard, +NaN is considered neither smaller than, +nor equal to, nor greater than any value (including itself). + + + + + +

3.4.5 – Logical Operators

+The logical operators in Lua are +and, or, and not. +Like the control structures (see §3.3.4), +all logical operators consider both false and nil as false +and anything else as true. + + +

+The negation operator not always returns false or true. +The conjunction operator and returns its first argument +if this value is false or nil; +otherwise, and returns its second argument. +The disjunction operator or returns its first argument +if this value is different from nil and false; +otherwise, or returns its second argument. +Both and and or use short-circuit evaluation; +that is, +the second operand is evaluated only if necessary. +Here are some examples: + +

+     10 or 20            --> 10
+     10 or error()       --> 10
+     nil or "a"          --> "a"
+     nil and 10          --> nil
+     false and error()   --> false
+     false and nil       --> false
+     false or nil        --> nil
+     10 and 20           --> 20
+

+(In this manual, +--> indicates the result of the preceding expression.) + + + + + +

3.4.6 – Concatenation

+The string concatenation operator in Lua is +denoted by two dots ('..'). +If both operands are strings or numbers, then they are converted to +strings according to the rules described in §3.4.3. +Otherwise, the __concat metamethod is called (see §2.4). + + + + + +

3.4.7 – The Length Operator

+ +

+The length operator is denoted by the unary prefix operator #. + + +

+The length of a string is its number of bytes +(that is, the usual meaning of string length when each +character is one byte). + + +

+The length operator applied on a table +returns a border in that table. +A border in a table t is any natural number +that satisfies the following condition: + +

+     (border == 0 or t[border] ~= nil) and t[border + 1] == nil
+

+In words, +a border is any (natural) index in a table +where a non-nil value is followed by a nil value +(or zero, when index 1 is nil). + + +

+A table with exactly one border is called a sequence. +For instance, the table {10, 20, 30, 40, 50} is a sequence, +as it has only one border (5). +The table {10, 20, 30, nil, 50} has two borders (3 and 5), +and therefore it is not a sequence. +The table {nil, 20, 30, nil, nil, 60, nil} +has three borders (0, 3, and 6), +so it is not a sequence, too. +The table {} is a sequence with border 0. +Note that non-natural keys do not interfere +with whether a table is a sequence. + + +

+When t is a sequence, +#t returns its only border, +which corresponds to the intuitive notion of the length of the sequence. +When t is not a sequence, +#t can return any of its borders. +(The exact one depends on details of +the internal representation of the table, +which in turn can depend on how the table was populated and +the memory addresses of its non-numeric keys.) + + +

+The computation of the length of a table +has a guaranteed worst time of O(log n), +where n is the largest natural key in the table. + + +

+A program can modify the behavior of the length operator for +any value but strings through the __len metamethod (see §2.4). + + + + + +

3.4.8 – Precedence

+Operator precedence in Lua follows the table below, +from lower to higher priority: + +

+     or
+     and
+     <     >     <=    >=    ~=    ==
+     |
+     ~
+     &
+     <<    >>
+     ..
+     +     -
+     *     /     //    %
+     unary operators (not   #     -     ~)
+     ^
+

+As usual, +you can use parentheses to change the precedences of an expression. +The concatenation ('..') and exponentiation ('^') +operators are right associative. +All other binary operators are left associative. + + + + + +

3.4.9 – Table Constructors

+Table constructors are expressions that create tables. +Every time a constructor is evaluated, a new table is created. +A constructor can be used to create an empty table +or to create a table and initialize some of its fields. +The general syntax for constructors is + +

+	tableconstructor ::= ‘{’ [fieldlist] ‘}’
+	fieldlist ::= field {fieldsep field} [fieldsep]
+	field ::= ‘[’ exp ‘]’ ‘=’ exp | Name ‘=’ exp | exp
+	fieldsep ::= ‘,’ | ‘;’
+
+ +

+Each field of the form [exp1] = exp2 adds to the new table an entry +with key exp1 and value exp2. +A field of the form name = exp is equivalent to +["name"] = exp. +Finally, fields of the form exp are equivalent to +[i] = exp, where i are consecutive integers +starting with 1. +Fields in the other formats do not affect this counting. +For example, + +

+     a = { [f(1)] = g; "x", "y"; x = 1, f(x), [30] = 23; 45 }
+

+is equivalent to + +

+     do
+       local t = {}
+       t[f(1)] = g
+       t[1] = "x"         -- 1st exp
+       t[2] = "y"         -- 2nd exp
+       t.x = 1            -- t["x"] = 1
+       t[3] = f(x)        -- 3rd exp
+       t[30] = 23
+       t[4] = 45          -- 4th exp
+       a = t
+     end
+
+ +

+The order of the assignments in a constructor is undefined. +(This order would be relevant only when there are repeated keys.) + + +

+If the last field in the list has the form exp +and the expression is a function call or a vararg expression, +then all values returned by this expression enter the list consecutively +(see §3.4.10). + + +

+The field list can have an optional trailing separator, +as a convenience for machine-generated code. + + + + + +

3.4.10 – Function Calls

+A function call in Lua has the following syntax: + +

+	functioncall ::= prefixexp args
+

+In a function call, +first prefixexp and args are evaluated. +If the value of prefixexp has type function, +then this function is called +with the given arguments. +Otherwise, the prefixexp "call" metamethod is called, +having as first parameter the value of prefixexp, +followed by the original call arguments +(see §2.4). + + +

+The form + +

+	functioncall ::= prefixexp ‘:’ Name args
+

+can be used to call "methods". +A call v:name(args) +is syntactic sugar for v.name(v,args), +except that v is evaluated only once. + + +

+Arguments have the following syntax: + +

+	args ::= ‘(’ [explist] ‘)’
+	args ::= tableconstructor
+	args ::= LiteralString
+

+All argument expressions are evaluated before the call. +A call of the form f{fields} is +syntactic sugar for f({fields}); +that is, the argument list is a single new table. +A call of the form f'string' +(or f"string" or f[[string]]) +is syntactic sugar for f('string'); +that is, the argument list is a single literal string. + + +

+A call of the form return functioncall is called +a tail call. +Lua implements proper tail calls +(or proper tail recursion): +in a tail call, +the called function reuses the stack entry of the calling function. +Therefore, there is no limit on the number of nested tail calls that +a program can execute. +However, a tail call erases any debug information about the +calling function. +Note that a tail call only happens with a particular syntax, +where the return has one single function call as argument; +this syntax makes the calling function return exactly +the returns of the called function. +So, none of the following examples are tail calls: + +

+     return (f(x))        -- results adjusted to 1
+     return 2 * f(x)
+     return x, f(x)       -- additional results
+     f(x); return         -- results discarded
+     return x or f(x)     -- results adjusted to 1
+
+ + + + +

3.4.11 – Function Definitions

+ +

+The syntax for function definition is + +

+	functiondef ::= function funcbody
+	funcbody ::= ‘(’ [parlist] ‘)’ block end
+
+ +

+The following syntactic sugar simplifies function definitions: + +

+	stat ::= function funcname funcbody
+	stat ::= local function Name funcbody
+	funcname ::= Name {‘.’ Name} [‘:’ Name]
+

+The statement + +

+     function f () body end
+

+translates to + +

+     f = function () body end
+

+The statement + +

+     function t.a.b.c.f () body end
+

+translates to + +

+     t.a.b.c.f = function () body end
+

+The statement + +

+     local function f () body end
+

+translates to + +

+     local f; f = function () body end
+

+not to + +

+     local f = function () body end
+

+(This only makes a difference when the body of the function +contains references to f.) + + +

+A function definition is an executable expression, +whose value has type function. +When Lua precompiles a chunk, +all its function bodies are precompiled too. +Then, whenever Lua executes the function definition, +the function is instantiated (or closed). +This function instance (or closure) +is the final value of the expression. + + +

+Parameters act as local variables that are +initialized with the argument values: + +

+	parlist ::= namelist [‘,’ ‘...’] | ‘...’
+

+When a function is called, +the list of arguments is adjusted to +the length of the list of parameters, +unless the function is a vararg function, +which is indicated by three dots ('...') +at the end of its parameter list. +A vararg function does not adjust its argument list; +instead, it collects all extra arguments and supplies them +to the function through a vararg expression, +which is also written as three dots. +The value of this expression is a list of all actual extra arguments, +similar to a function with multiple results. +If a vararg expression is used inside another expression +or in the middle of a list of expressions, +then its return list is adjusted to one element. +If the expression is used as the last element of a list of expressions, +then no adjustment is made +(unless that last expression is enclosed in parentheses). + + +

+As an example, consider the following definitions: + +

+     function f(a, b) end
+     function g(a, b, ...) end
+     function r() return 1,2,3 end
+

+Then, we have the following mapping from arguments to parameters and +to the vararg expression: + +

+     CALL            PARAMETERS
+     
+     f(3)             a=3, b=nil
+     f(3, 4)          a=3, b=4
+     f(3, 4, 5)       a=3, b=4
+     f(r(), 10)       a=1, b=10
+     f(r())           a=1, b=2
+     
+     g(3)             a=3, b=nil, ... -->  (nothing)
+     g(3, 4)          a=3, b=4,   ... -->  (nothing)
+     g(3, 4, 5, 8)    a=3, b=4,   ... -->  5  8
+     g(5, r())        a=5, b=1,   ... -->  2  3
+
+ +

+Results are returned using the return statement (see §3.3.4). +If control reaches the end of a function +without encountering a return statement, +then the function returns with no results. + + +

+ +There is a system-dependent limit on the number of values +that a function may return. +This limit is guaranteed to be larger than 1000. + + +

+The colon syntax +is used for defining methods, +that is, functions that have an implicit extra parameter self. +Thus, the statement + +

+     function t.a.b.c:f (params) body end
+

+is syntactic sugar for + +

+     t.a.b.c.f = function (self, params) body end
+
+ + + + + + +

3.5 – Visibility Rules

+ +

+ +Lua is a lexically scoped language. +The scope of a local variable begins at the first statement after +its declaration and lasts until the last non-void statement +of the innermost block that includes the declaration. +Consider the following example: + +

+     x = 10                -- global variable
+     do                    -- new block
+       local x = x         -- new 'x', with value 10
+       print(x)            --> 10
+       x = x+1
+       do                  -- another block
+         local x = x+1     -- another 'x'
+         print(x)          --> 12
+       end
+       print(x)            --> 11
+     end
+     print(x)              --> 10  (the global one)
+
+ +

+Notice that, in a declaration like local x = x, +the new x being declared is not in scope yet, +and so the second x refers to the outside variable. + + +

+Because of the lexical scoping rules, +local variables can be freely accessed by functions +defined inside their scope. +A local variable used by an inner function is called +an upvalue, or external local variable, +inside the inner function. + + +

+Notice that each execution of a local statement +defines new local variables. +Consider the following example: + +

+     a = {}
+     local x = 20
+     for i=1,10 do
+       local y = 0
+       a[i] = function () y=y+1; return x+y end
+     end
+

+The loop creates ten closures +(that is, ten instances of the anonymous function). +Each of these closures uses a different y variable, +while all of them share the same x. + + + + + +

4 – The Application Program Interface

+ +

+ +This section describes the C API for Lua, that is, +the set of C functions available to the host program to communicate +with Lua. +All API functions and related types and constants +are declared in the header file lua.h. + + +

+Even when we use the term "function", +any facility in the API may be provided as a macro instead. +Except where stated otherwise, +all such macros use each of their arguments exactly once +(except for the first argument, which is always a Lua state), +and so do not generate any hidden side-effects. + + +

+As in most C libraries, +the Lua API functions do not check their arguments for validity or consistency. +However, you can change this behavior by compiling Lua +with the macro LUA_USE_APICHECK defined. + + +

+The Lua library is fully reentrant: +it has no global variables. +It keeps all information it needs in a dynamic structure, +called the Lua state. + + +

+Each Lua state has one or more threads, +which correspond to independent, cooperative lines of execution. +The type lua_State (despite its name) refers to a thread. +(Indirectly, through the thread, it also refers to the +Lua state associated to the thread.) + + +

+A pointer to a thread must be passed as the first argument to +every function in the library, except to lua_newstate, +which creates a Lua state from scratch and returns a pointer +to the main thread in the new state. + + + +

4.1 – The Stack

+ +

+Lua uses a virtual stack to pass values to and from C. +Each element in this stack represents a Lua value +(nil, number, string, etc.). +Functions in the API can access this stack through the +Lua state parameter that they receive. + + +

+Whenever Lua calls C, the called function gets a new stack, +which is independent of previous stacks and of stacks of +C functions that are still active. +This stack initially contains any arguments to the C function +and it is where the C function can store temporary +Lua values and must push its results +to be returned to the caller (see lua_CFunction). + + +

+For convenience, +most query operations in the API do not follow a strict stack discipline. +Instead, they can refer to any element in the stack +by using an index: +A positive index represents an absolute stack position +(starting at 1); +a negative index represents an offset relative to the top of the stack. +More specifically, if the stack has n elements, +then index 1 represents the first element +(that is, the element that was pushed onto the stack first) +and +index n represents the last element; +index -1 also represents the last element +(that is, the element at the top) +and index -n represents the first element. + + + + + +

4.2 – Stack Size

+ +

+When you interact with the Lua API, +you are responsible for ensuring consistency. +In particular, +you are responsible for controlling stack overflow. +You can use the function lua_checkstack +to ensure that the stack has enough space for pushing new elements. + + +

+Whenever Lua calls C, +it ensures that the stack has space for +at least LUA_MINSTACK extra slots. +LUA_MINSTACK is defined as 20, +so that usually you do not have to worry about stack space +unless your code has loops pushing elements onto the stack. + + +

+When you call a Lua function +without a fixed number of results (see lua_call), +Lua ensures that the stack has enough space for all results, +but it does not ensure any extra space. +So, before pushing anything in the stack after such a call +you should use lua_checkstack. + + + + + +

4.3 – Valid and Acceptable Indices

+ +

+Any function in the API that receives stack indices +works only with valid indices or acceptable indices. + + +

+A valid index is an index that refers to a +position that stores a modifiable Lua value. +It comprises stack indices between 1 and the stack top +(1 ≤ abs(index) ≤ top) + +plus pseudo-indices, +which represent some positions that are accessible to C code +but that are not in the stack. +Pseudo-indices are used to access the registry (see §4.5) +and the upvalues of a C function (see §4.4). + + +

+Functions that do not need a specific mutable position, +but only a value (e.g., query functions), +can be called with acceptable indices. +An acceptable index can be any valid index, +but it also can be any positive index after the stack top +within the space allocated for the stack, +that is, indices up to the stack size. +(Note that 0 is never an acceptable index.) +Except when noted otherwise, +functions in the API work with acceptable indices. + + +

+Acceptable indices serve to avoid extra tests +against the stack top when querying the stack. +For instance, a C function can query its third argument +without the need to first check whether there is a third argument, +that is, without the need to check whether 3 is a valid index. + + +

+For functions that can be called with acceptable indices, +any non-valid index is treated as if it +contains a value of a virtual type LUA_TNONE, +which behaves like a nil value. + + + + + +

4.4 – C Closures

+ +

+When a C function is created, +it is possible to associate some values with it, +thus creating a C closure +(see lua_pushcclosure); +these values are called upvalues and are +accessible to the function whenever it is called. + + +

+Whenever a C function is called, +its upvalues are located at specific pseudo-indices. +These pseudo-indices are produced by the macro +lua_upvalueindex. +The first upvalue associated with a function is at index +lua_upvalueindex(1), and so on. +Any access to lua_upvalueindex(n), +where n is greater than the number of upvalues of the +current function +(but not greater than 256, +which is one plus the maximum number of upvalues in a closure), +produces an acceptable but invalid index. + + + + + +

4.5 – Registry

+ +

+Lua provides a registry, +a predefined table that can be used by any C code to +store whatever Lua values it needs to store. +The registry table is always located at pseudo-index +LUA_REGISTRYINDEX. +Any C library can store data into this table, +but it must take care to choose keys +that are different from those used +by other libraries, to avoid collisions. +Typically, you should use as key a string containing your library name, +or a light userdata with the address of a C object in your code, +or any Lua object created by your code. +As with variable names, +string keys starting with an underscore followed by +uppercase letters are reserved for Lua. + + +

+The integer keys in the registry are used +by the reference mechanism (see luaL_ref) +and by some predefined values. +Therefore, integer keys must not be used for other purposes. + + +

+When you create a new Lua state, +its registry comes with some predefined values. +These predefined values are indexed with integer keys +defined as constants in lua.h. +The following constants are defined: + +

    +
  • LUA_RIDX_MAINTHREAD: At this index the registry has +the main thread of the state. +(The main thread is the one created together with the state.) +
  • + +
  • LUA_RIDX_GLOBALS: At this index the registry has +the global environment. +
  • +
+ + + + +

4.6 – Error Handling in C

+ +

+Internally, Lua uses the C longjmp facility to handle errors. +(Lua will use exceptions if you compile it as C++; +search for LUAI_THROW in the source code for details.) +When Lua faces any error +(such as a memory allocation error or a type error) +it raises an error; +that is, it does a long jump. +A protected environment uses setjmp +to set a recovery point; +any error jumps to the most recent active recovery point. + + +

+Inside a C function you can raise an error by calling lua_error. + + +

+Most functions in the API can raise an error, +for instance due to a memory allocation error. +The documentation for each function indicates whether +it can raise errors. + + +

+If an error happens outside any protected environment, +Lua calls a panic function (see lua_atpanic) +and then calls abort, +thus exiting the host application. +Your panic function can avoid this exit by +never returning +(e.g., doing a long jump to your own recovery point outside Lua). + + +

+The panic function, +as its name implies, +is a mechanism of last resort. +Programs should avoid it. +As a general rule, +when a C function is called by Lua with a Lua state, +it can do whatever it wants on that Lua state, +as it should be already protected. +However, +when C code operates on other Lua states +(e.g., a Lua parameter to the function, +a Lua state stored in the registry, or +the result of lua_newthread), +it should use them only in API calls that cannot raise errors. + + +

+The panic function runs as if it were a message handler (see §2.3); +in particular, the error object is at the top of the stack. +However, there is no guarantee about stack space. +To push anything on the stack, +the panic function must first check the available space (see §4.2). + + + + + +

4.7 – Handling Yields in C

+ +

+Internally, Lua uses the C longjmp facility to yield a coroutine. +Therefore, if a C function foo calls an API function +and this API function yields +(directly or indirectly by calling another function that yields), +Lua cannot return to foo any more, +because the longjmp removes its frame from the C stack. + + +

+To avoid this kind of problem, +Lua raises an error whenever it tries to yield across an API call, +except for three functions: +lua_yieldk, lua_callk, and lua_pcallk. +All those functions receive a continuation function +(as a parameter named k) to continue execution after a yield. + + +

+We need to set some terminology to explain continuations. +We have a C function called from Lua which we will call +the original function. +This original function then calls one of those three functions in the C API, +which we will call the callee function, +that then yields the current thread. +(This can happen when the callee function is lua_yieldk, +or when the callee function is either lua_callk or lua_pcallk +and the function called by them yields.) + + +

+Suppose the running thread yields while executing the callee function. +After the thread resumes, +it eventually will finish running the callee function. +However, +the callee function cannot return to the original function, +because its frame in the C stack was destroyed by the yield. +Instead, Lua calls a continuation function, +which was given as an argument to the callee function. +As the name implies, +the continuation function should continue the task +of the original function. + + +

+As an illustration, consider the following function: + +

+     int original_function (lua_State *L) {
+       ...     /* code 1 */
+       status = lua_pcall(L, n, m, h);  /* calls Lua */
+       ...     /* code 2 */
+     }
+

+Now we want to allow +the Lua code being run by lua_pcall to yield. +First, we can rewrite our function like here: + +

+     int k (lua_State *L, int status, lua_KContext ctx) {
+       ...  /* code 2 */
+     }
+     
+     int original_function (lua_State *L) {
+       ...     /* code 1 */
+       return k(L, lua_pcall(L, n, m, h), ctx);
+     }
+

+In the above code, +the new function k is a +continuation function (with type lua_KFunction), +which should do all the work that the original function +was doing after calling lua_pcall. +Now, we must inform Lua that it must call k if the Lua code +being executed by lua_pcall gets interrupted in some way +(errors or yielding), +so we rewrite the code as here, +replacing lua_pcall by lua_pcallk: + +

+     int original_function (lua_State *L) {
+       ...     /* code 1 */
+       return k(L, lua_pcallk(L, n, m, h, ctx2, k), ctx1);
+     }
+

+Note the external, explicit call to the continuation: +Lua will call the continuation only if needed, that is, +in case of errors or resuming after a yield. +If the called function returns normally without ever yielding, +lua_pcallk (and lua_callk) will also return normally. +(Of course, instead of calling the continuation in that case, +you can do the equivalent work directly inside the original function.) + + +

+Besides the Lua state, +the continuation function has two other parameters: +the final status of the call plus the context value (ctx) that +was passed originally to lua_pcallk. +(Lua does not use this context value; +it only passes this value from the original function to the +continuation function.) +For lua_pcallk, +the status is the same value that would be returned by lua_pcallk, +except that it is LUA_YIELD when being executed after a yield +(instead of LUA_OK). +For lua_yieldk and lua_callk, +the status is always LUA_YIELD when Lua calls the continuation. +(For these two functions, +Lua will not call the continuation in case of errors, +because they do not handle errors.) +Similarly, when using lua_callk, +you should call the continuation function +with LUA_OK as the status. +(For lua_yieldk, there is not much point in calling +directly the continuation function, +because lua_yieldk usually does not return.) + + +

+Lua treats the continuation function as if it were the original function. +The continuation function receives the same Lua stack +from the original function, +in the same state it would be if the callee function had returned. +(For instance, +after a lua_callk the function and its arguments are +removed from the stack and replaced by the results from the call.) +It also has the same upvalues. +Whatever it returns is handled by Lua as if it were the return +of the original function. + + + + + +

4.8 – Functions and Types

+ +

+Here we list all functions and types from the C API in +alphabetical order. +Each function has an indicator like this: +[-o, +p, x] + + +

+The first field, o, +is how many elements the function pops from the stack. +The second field, p, +is how many elements the function pushes onto the stack. +(Any function always pushes its results after popping its arguments.) +A field in the form x|y means the function can push (or pop) +x or y elements, +depending on the situation; +an interrogation mark '?' means that +we cannot know how many elements the function pops/pushes +by looking only at its arguments +(e.g., they may depend on what is on the stack). +The third field, x, +tells whether the function may raise errors: +'-' means the function never raises any error; +'m' means the function may raise out-of-memory errors +and errors running a __gc metamethod; +'e' means the function may raise any errors +(it can run arbitrary Lua code, +either directly or through metamethods); +'v' means the function may raise an error on purpose. + + + +


lua_absindex

+[-0, +0, –] +

int lua_absindex (lua_State *L, int idx);
+ +

+Converts the acceptable index idx +into an equivalent absolute index +(that is, one that does not depend on the stack top). + + + + + +


lua_Alloc

+
typedef void * (*lua_Alloc) (void *ud,
+                             void *ptr,
+                             size_t osize,
+                             size_t nsize);
+ +

+The type of the memory-allocation function used by Lua states. +The allocator function must provide a +functionality similar to realloc, +but not exactly the same. +Its arguments are +ud, an opaque pointer passed to lua_newstate; +ptr, a pointer to the block being allocated/reallocated/freed; +osize, the original size of the block or some code about what +is being allocated; +and nsize, the new size of the block. + + +

+When ptr is not NULL, +osize is the size of the block pointed by ptr, +that is, the size given when it was allocated or reallocated. + + +

+When ptr is NULL, +osize encodes the kind of object that Lua is allocating. +osize is any of +LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, +LUA_TUSERDATA, or LUA_TTHREAD when (and only when) +Lua is creating a new object of that type. +When osize is some other value, +Lua is allocating memory for something else. + + +

+Lua assumes the following behavior from the allocator function: + + +

+When nsize is zero, +the allocator must behave like free +and return NULL. + + +

+When nsize is not zero, +the allocator must behave like realloc. +The allocator returns NULL +if and only if it cannot fulfill the request. +Lua assumes that the allocator never fails when +osize >= nsize. + + +

+Here is a simple implementation for the allocator function. +It is used in the auxiliary library by luaL_newstate. + +

+     static void *l_alloc (void *ud, void *ptr, size_t osize,
+                                                size_t nsize) {
+       (void)ud;  (void)osize;  /* not used */
+       if (nsize == 0) {
+         free(ptr);
+         return NULL;
+       }
+       else
+         return realloc(ptr, nsize);
+     }
+

+Note that Standard C ensures +that free(NULL) has no effect and that +realloc(NULL,size) is equivalent to malloc(size). +This code assumes that realloc does not fail when shrinking a block. +(Although Standard C does not ensure this behavior, +it seems to be a safe assumption.) + + + + + +


lua_arith

+[-(2|1), +1, e] +

void lua_arith (lua_State *L, int op);
+ +

+Performs an arithmetic or bitwise operation over the two values +(or one, in the case of negations) +at the top of the stack, +with the value at the top being the second operand, +pops these values, and pushes the result of the operation. +The function follows the semantics of the corresponding Lua operator +(that is, it may call metamethods). + + +

+The value of op must be one of the following constants: + +

+ + + + +

lua_atpanic

+[-0, +0, –] +

lua_CFunction lua_atpanic (lua_State *L, lua_CFunction panicf);
+ +

+Sets a new panic function and returns the old one (see §4.6). + + + + + +


lua_call

+[-(nargs+1), +nresults, e] +

void lua_call (lua_State *L, int nargs, int nresults);
+ +

+Calls a function. + + +

+To call a function you must use the following protocol: +first, the function to be called is pushed onto the stack; +then, the arguments to the function are pushed +in direct order; +that is, the first argument is pushed first. +Finally you call lua_call; +nargs is the number of arguments that you pushed onto the stack. +All arguments and the function value are popped from the stack +when the function is called. +The function results are pushed onto the stack when the function returns. +The number of results is adjusted to nresults, +unless nresults is LUA_MULTRET. +In this case, all results from the function are pushed; +Lua takes care that the returned values fit into the stack space, +but it does not ensure any extra space in the stack. +The function results are pushed onto the stack in direct order +(the first result is pushed first), +so that after the call the last result is on the top of the stack. + + +

+Any error inside the called function is propagated upwards +(with a longjmp). + + +

+The following example shows how the host program can do the +equivalent to this Lua code: + +

+     a = f("how", t.x, 14)
+

+Here it is in C: + +

+     lua_getglobal(L, "f");                  /* function to be called */
+     lua_pushliteral(L, "how");                       /* 1st argument */
+     lua_getglobal(L, "t");                    /* table to be indexed */
+     lua_getfield(L, -1, "x");        /* push result of t.x (2nd arg) */
+     lua_remove(L, -2);                  /* remove 't' from the stack */
+     lua_pushinteger(L, 14);                          /* 3rd argument */
+     lua_call(L, 3, 1);     /* call 'f' with 3 arguments and 1 result */
+     lua_setglobal(L, "a");                         /* set global 'a' */
+

+Note that the code above is balanced: +at its end, the stack is back to its original configuration. +This is considered good programming practice. + + + + + +


lua_callk

+[-(nargs + 1), +nresults, e] +

void lua_callk (lua_State *L,
+                int nargs,
+                int nresults,
+                lua_KContext ctx,
+                lua_KFunction k);
+ +

+This function behaves exactly like lua_call, +but allows the called function to yield (see §4.7). + + + + + +


lua_CFunction

+
typedef int (*lua_CFunction) (lua_State *L);
+ +

+Type for C functions. + + +

+In order to communicate properly with Lua, +a C function must use the following protocol, +which defines the way parameters and results are passed: +a C function receives its arguments from Lua in its stack +in direct order (the first argument is pushed first). +So, when the function starts, +lua_gettop(L) returns the number of arguments received by the function. +The first argument (if any) is at index 1 +and its last argument is at index lua_gettop(L). +To return values to Lua, a C function just pushes them onto the stack, +in direct order (the first result is pushed first), +and returns the number of results. +Any other value in the stack below the results will be properly +discarded by Lua. +Like a Lua function, a C function called by Lua can also return +many results. + + +

+As an example, the following function receives a variable number +of numeric arguments and returns their average and their sum: + +

+     static int foo (lua_State *L) {
+       int n = lua_gettop(L);    /* number of arguments */
+       lua_Number sum = 0.0;
+       int i;
+       for (i = 1; i <= n; i++) {
+         if (!lua_isnumber(L, i)) {
+           lua_pushliteral(L, "incorrect argument");
+           lua_error(L);
+         }
+         sum += lua_tonumber(L, i);
+       }
+       lua_pushnumber(L, sum/n);        /* first result */
+       lua_pushnumber(L, sum);         /* second result */
+       return 2;                   /* number of results */
+     }
+
+ + + + +

lua_checkstack

+[-0, +0, –] +

int lua_checkstack (lua_State *L, int n);
+ +

+Ensures that the stack has space for at least n extra slots +(that is, that you can safely push up to n values into it). +It returns false if it cannot fulfill the request, +either because it would cause the stack +to be larger than a fixed maximum size +(typically at least several thousand elements) or +because it cannot allocate memory for the extra space. +This function never shrinks the stack; +if the stack already has space for the extra slots, +it is left unchanged. + + + + + +


lua_close

+[-0, +0, –] +

void lua_close (lua_State *L);
+ +

+Destroys all objects in the given Lua state +(calling the corresponding garbage-collection metamethods, if any) +and frees all dynamic memory used by this state. +On several platforms, you may not need to call this function, +because all resources are naturally released when the host program ends. +On the other hand, long-running programs that create multiple states, +such as daemons or web servers, +will probably need to close states as soon as they are not needed. + + + + + +


lua_compare

+[-0, +0, e] +

int lua_compare (lua_State *L, int index1, int index2, int op);
+ +

+Compares two Lua values. +Returns 1 if the value at index index1 satisfies op +when compared with the value at index index2, +following the semantics of the corresponding Lua operator +(that is, it may call metamethods). +Otherwise returns 0. +Also returns 0 if any of the indices is not valid. + + +

+The value of op must be one of the following constants: + +

    + +
  • LUA_OPEQ: compares for equality (==)
  • +
  • LUA_OPLT: compares for less than (<)
  • +
  • LUA_OPLE: compares for less or equal (<=)
  • + +
+ + + + +

lua_concat

+[-n, +1, e] +

void lua_concat (lua_State *L, int n);
+ +

+Concatenates the n values at the top of the stack, +pops them, and leaves the result at the top. +If n is 1, the result is the single value on the stack +(that is, the function does nothing); +if n is 0, the result is the empty string. +Concatenation is performed following the usual semantics of Lua +(see §3.4.6). + + + + + +


lua_copy

+[-0, +0, –] +

void lua_copy (lua_State *L, int fromidx, int toidx);
+ +

+Copies the element at index fromidx +into the valid index toidx, +replacing the value at that position. +Values at other positions are not affected. + + + + + +


lua_createtable

+[-0, +1, m] +

void lua_createtable (lua_State *L, int narr, int nrec);
+ +

+Creates a new empty table and pushes it onto the stack. +Parameter narr is a hint for how many elements the table +will have as a sequence; +parameter nrec is a hint for how many other elements +the table will have. +Lua may use these hints to preallocate memory for the new table. +This preallocation is useful for performance when you know in advance +how many elements the table will have. +Otherwise you can use the function lua_newtable. + + + + + +


lua_dump

+[-0, +0, –] +

int lua_dump (lua_State *L,
+                        lua_Writer writer,
+                        void *data,
+                        int strip);
+ +

+Dumps a function as a binary chunk. +Receives a Lua function on the top of the stack +and produces a binary chunk that, +if loaded again, +results in a function equivalent to the one dumped. +As it produces parts of the chunk, +lua_dump calls function writer (see lua_Writer) +with the given data +to write them. + + +

+If strip is true, +the binary representation may not include all debug information +about the function, +to save space. + + +

+The value returned is the error code returned by the last +call to the writer; +0 means no errors. + + +

+This function does not pop the Lua function from the stack. + + + + + +


lua_error

+[-1, +0, v] +

int lua_error (lua_State *L);
+ +

+Generates a Lua error, +using the value at the top of the stack as the error object. +This function does a long jump, +and therefore never returns +(see luaL_error). + + + + + +


lua_gc

+[-0, +0, m] +

int lua_gc (lua_State *L, int what, int data);
+ +

+Controls the garbage collector. + + +

+This function performs several tasks, +according to the value of the parameter what: + +

    + +
  • LUA_GCSTOP: +stops the garbage collector. +
  • + +
  • LUA_GCRESTART: +restarts the garbage collector. +
  • + +
  • LUA_GCCOLLECT: +performs a full garbage-collection cycle. +
  • + +
  • LUA_GCCOUNT: +returns the current amount of memory (in Kbytes) in use by Lua. +
  • + +
  • LUA_GCCOUNTB: +returns the remainder of dividing the current amount of bytes of +memory in use by Lua by 1024. +
  • + +
  • LUA_GCSTEP: +performs an incremental step of garbage collection. +
  • + +
  • LUA_GCSETPAUSE: +sets data as the new value +for the pause of the collector (see §2.5) +and returns the previous value of the pause. +
  • + +
  • LUA_GCSETSTEPMUL: +sets data as the new value for the step multiplier of +the collector (see §2.5) +and returns the previous value of the step multiplier. +
  • + +
  • LUA_GCISRUNNING: +returns a boolean that tells whether the collector is running +(i.e., not stopped). +
  • + +
+ +

+For more details about these options, +see collectgarbage. + + + + + +


lua_getallocf

+[-0, +0, –] +

lua_Alloc lua_getallocf (lua_State *L, void **ud);
+ +

+Returns the memory-allocation function of a given state. +If ud is not NULL, Lua stores in *ud the +opaque pointer given when the memory-allocator function was set. + + + + + +


lua_getfield

+[-0, +1, e] +

int lua_getfield (lua_State *L, int index, const char *k);
+ +

+Pushes onto the stack the value t[k], +where t is the value at the given index. +As in Lua, this function may trigger a metamethod +for the "index" event (see §2.4). + + +

+Returns the type of the pushed value. + + + + + +


lua_getextraspace

+[-0, +0, –] +

void *lua_getextraspace (lua_State *L);
+ +

+Returns a pointer to a raw memory area associated with the +given Lua state. +The application can use this area for any purpose; +Lua does not use it for anything. + + +

+Each new thread has this area initialized with a copy +of the area of the main thread. + + +

+By default, this area has the size of a pointer to void, +but you can recompile Lua with a different size for this area. +(See LUA_EXTRASPACE in luaconf.h.) + + + + + +


lua_getglobal

+[-0, +1, e] +

int lua_getglobal (lua_State *L, const char *name);
+ +

+Pushes onto the stack the value of the global name. +Returns the type of that value. + + + + + +


lua_geti

+[-0, +1, e] +

int lua_geti (lua_State *L, int index, lua_Integer i);
+ +

+Pushes onto the stack the value t[i], +where t is the value at the given index. +As in Lua, this function may trigger a metamethod +for the "index" event (see §2.4). + + +

+Returns the type of the pushed value. + + + + + +


lua_getmetatable

+[-0, +(0|1), –] +

int lua_getmetatable (lua_State *L, int index);
+ +

+If the value at the given index has a metatable, +the function pushes that metatable onto the stack and returns 1. +Otherwise, +the function returns 0 and pushes nothing on the stack. + + + + + +


lua_gettable

+[-1, +1, e] +

int lua_gettable (lua_State *L, int index);
+ +

+Pushes onto the stack the value t[k], +where t is the value at the given index +and k is the value at the top of the stack. + + +

+This function pops the key from the stack, +pushing the resulting value in its place. +As in Lua, this function may trigger a metamethod +for the "index" event (see §2.4). + + +

+Returns the type of the pushed value. + + + + + +


lua_gettop

+[-0, +0, –] +

int lua_gettop (lua_State *L);
+ +

+Returns the index of the top element in the stack. +Because indices start at 1, +this result is equal to the number of elements in the stack; +in particular, 0 means an empty stack. + + + + + +


lua_getuservalue

+[-0, +1, –] +

int lua_getuservalue (lua_State *L, int index);
+ +

+Pushes onto the stack the Lua value associated with the full userdata +at the given index. + + +

+Returns the type of the pushed value. + + + + + +


lua_insert

+[-1, +1, –] +

void lua_insert (lua_State *L, int index);
+ +

+Moves the top element into the given valid index, +shifting up the elements above this index to open space. +This function cannot be called with a pseudo-index, +because a pseudo-index is not an actual stack position. + + + + + +


lua_Integer

+
typedef ... lua_Integer;
+ +

+The type of integers in Lua. + + +

+By default this type is long long, +(usually a 64-bit two-complement integer), +but that can be changed to long or int +(usually a 32-bit two-complement integer). +(See LUA_INT_TYPE in luaconf.h.) + + +

+Lua also defines the constants +LUA_MININTEGER and LUA_MAXINTEGER, +with the minimum and the maximum values that fit in this type. + + + + + +


lua_isboolean

+[-0, +0, –] +

int lua_isboolean (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a boolean, +and 0 otherwise. + + + + + +


lua_iscfunction

+[-0, +0, –] +

int lua_iscfunction (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a C function, +and 0 otherwise. + + + + + +


lua_isfunction

+[-0, +0, –] +

int lua_isfunction (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a function +(either C or Lua), and 0 otherwise. + + + + + +


lua_isinteger

+[-0, +0, –] +

int lua_isinteger (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is an integer +(that is, the value is a number and is represented as an integer), +and 0 otherwise. + + + + + +


lua_islightuserdata

+[-0, +0, –] +

int lua_islightuserdata (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a light userdata, +and 0 otherwise. + + + + + +


lua_isnil

+[-0, +0, –] +

int lua_isnil (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is nil, +and 0 otherwise. + + + + + +


lua_isnone

+[-0, +0, –] +

int lua_isnone (lua_State *L, int index);
+ +

+Returns 1 if the given index is not valid, +and 0 otherwise. + + + + + +


lua_isnoneornil

+[-0, +0, –] +

int lua_isnoneornil (lua_State *L, int index);
+ +

+Returns 1 if the given index is not valid +or if the value at this index is nil, +and 0 otherwise. + + + + + +


lua_isnumber

+[-0, +0, –] +

int lua_isnumber (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a number +or a string convertible to a number, +and 0 otherwise. + + + + + +


lua_isstring

+[-0, +0, –] +

int lua_isstring (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a string +or a number (which is always convertible to a string), +and 0 otherwise. + + + + + +


lua_istable

+[-0, +0, –] +

int lua_istable (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a table, +and 0 otherwise. + + + + + +


lua_isthread

+[-0, +0, –] +

int lua_isthread (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a thread, +and 0 otherwise. + + + + + +


lua_isuserdata

+[-0, +0, –] +

int lua_isuserdata (lua_State *L, int index);
+ +

+Returns 1 if the value at the given index is a userdata +(either full or light), and 0 otherwise. + + + + + +


lua_isyieldable

+[-0, +0, –] +

int lua_isyieldable (lua_State *L);
+ +

+Returns 1 if the given coroutine can yield, +and 0 otherwise. + + + + + +


lua_KContext

+
typedef ... lua_KContext;
+ +

+The type for continuation-function contexts. +It must be a numeric type. +This type is defined as intptr_t +when intptr_t is available, +so that it can store pointers too. +Otherwise, it is defined as ptrdiff_t. + + + + + +


lua_KFunction

+
typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx);
+ +

+Type for continuation functions (see §4.7). + + + + + +


lua_len

+[-0, +1, e] +

void lua_len (lua_State *L, int index);
+ +

+Returns the length of the value at the given index. +It is equivalent to the '#' operator in Lua (see §3.4.7) and +may trigger a metamethod for the "length" event (see §2.4). +The result is pushed on the stack. + + + + + +


lua_load

+[-0, +1, –] +

int lua_load (lua_State *L,
+              lua_Reader reader,
+              void *data,
+              const char *chunkname,
+              const char *mode);
+ +

+Loads a Lua chunk without running it. +If there are no errors, +lua_load pushes the compiled chunk as a Lua +function on top of the stack. +Otherwise, it pushes an error message. + + +

+The return values of lua_load are: + +

    + +
  • LUA_OK: no errors;
  • + +
  • LUA_ERRSYNTAX: +syntax error during precompilation;
  • + +
  • LUA_ERRMEM: +memory allocation (out-of-memory) error;
  • + +
  • LUA_ERRGCMM: +error while running a __gc metamethod. +(This error has no relation with the chunk being loaded. +It is generated by the garbage collector.) +
  • + +
+ +

+The lua_load function uses a user-supplied reader function +to read the chunk (see lua_Reader). +The data argument is an opaque value passed to the reader function. + + +

+The chunkname argument gives a name to the chunk, +which is used for error messages and in debug information (see §4.9). + + +

+lua_load automatically detects whether the chunk is text or binary +and loads it accordingly (see program luac). +The string mode works as in function load, +with the addition that +a NULL value is equivalent to the string "bt". + + +

+lua_load uses the stack internally, +so the reader function must always leave the stack +unmodified when returning. + + +

+If the resulting function has upvalues, +its first upvalue is set to the value of the global environment +stored at index LUA_RIDX_GLOBALS in the registry (see §4.5). +When loading main chunks, +this upvalue will be the _ENV variable (see §2.2). +Other upvalues are initialized with nil. + + + + + +


lua_newstate

+[-0, +0, –] +

lua_State *lua_newstate (lua_Alloc f, void *ud);
+ +

+Creates a new thread running in a new, independent state. +Returns NULL if it cannot create the thread or the state +(due to lack of memory). +The argument f is the allocator function; +Lua does all memory allocation for this state +through this function (see lua_Alloc). +The second argument, ud, is an opaque pointer that Lua +passes to the allocator in every call. + + + + + +


lua_newtable

+[-0, +1, m] +

void lua_newtable (lua_State *L);
+ +

+Creates a new empty table and pushes it onto the stack. +It is equivalent to lua_createtable(L, 0, 0). + + + + + +


lua_newthread

+[-0, +1, m] +

lua_State *lua_newthread (lua_State *L);
+ +

+Creates a new thread, pushes it on the stack, +and returns a pointer to a lua_State that represents this new thread. +The new thread returned by this function shares with the original thread +its global environment, +but has an independent execution stack. + + +

+There is no explicit function to close or to destroy a thread. +Threads are subject to garbage collection, +like any Lua object. + + + + + +


lua_newuserdata

+[-0, +1, m] +

void *lua_newuserdata (lua_State *L, size_t size);
+ +

+This function allocates a new block of memory with the given size, +pushes onto the stack a new full userdata with the block address, +and returns this address. +The host program can freely use this memory. + + + + + +


lua_next

+[-1, +(2|0), e] +

int lua_next (lua_State *L, int index);
+ +

+Pops a key from the stack, +and pushes a key–value pair from the table at the given index +(the "next" pair after the given key). +If there are no more elements in the table, +then lua_next returns 0 (and pushes nothing). + + +

+A typical traversal looks like this: + +

+     /* table is in the stack at index 't' */
+     lua_pushnil(L);  /* first key */
+     while (lua_next(L, t) != 0) {
+       /* uses 'key' (at index -2) and 'value' (at index -1) */
+       printf("%s - %s\n",
+              lua_typename(L, lua_type(L, -2)),
+              lua_typename(L, lua_type(L, -1)));
+       /* removes 'value'; keeps 'key' for next iteration */
+       lua_pop(L, 1);
+     }
+
+ +

+While traversing a table, +do not call lua_tolstring directly on a key, +unless you know that the key is actually a string. +Recall that lua_tolstring may change +the value at the given index; +this confuses the next call to lua_next. + + +

+See function next for the caveats of modifying +the table during its traversal. + + + + + +


lua_Number

+
typedef ... lua_Number;
+ +

+The type of floats in Lua. + + +

+By default this type is double, +but that can be changed to a single float or a long double. +(See LUA_FLOAT_TYPE in luaconf.h.) + + + + + +


lua_numbertointeger

+
int lua_numbertointeger (lua_Number n, lua_Integer *p);
+ +

+Converts a Lua float to a Lua integer. +This macro assumes that n has an integral value. +If that value is within the range of Lua integers, +it is converted to an integer and assigned to *p. +The macro results in a boolean indicating whether the +conversion was successful. +(Note that this range test can be tricky to do +correctly without this macro, +due to roundings.) + + +

+This macro may evaluate its arguments more than once. + + + + + +


lua_pcall

+[-(nargs + 1), +(nresults|1), –] +

int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);
+ +

+Calls a function in protected mode. + + +

+Both nargs and nresults have the same meaning as +in lua_call. +If there are no errors during the call, +lua_pcall behaves exactly like lua_call. +However, if there is any error, +lua_pcall catches it, +pushes a single value on the stack (the error object), +and returns an error code. +Like lua_call, +lua_pcall always removes the function +and its arguments from the stack. + + +

+If msgh is 0, +then the error object returned on the stack +is exactly the original error object. +Otherwise, msgh is the stack index of a +message handler. +(This index cannot be a pseudo-index.) +In case of runtime errors, +this function will be called with the error object +and its return value will be the object +returned on the stack by lua_pcall. + + +

+Typically, the message handler is used to add more debug +information to the error object, such as a stack traceback. +Such information cannot be gathered after the return of lua_pcall, +since by then the stack has unwound. + + +

+The lua_pcall function returns one of the following constants +(defined in lua.h): + +

    + +
  • LUA_OK (0): +success.
  • + +
  • LUA_ERRRUN: +a runtime error. +
  • + +
  • LUA_ERRMEM: +memory allocation error. +For such errors, Lua does not call the message handler. +
  • + +
  • LUA_ERRERR: +error while running the message handler. +
  • + +
  • LUA_ERRGCMM: +error while running a __gc metamethod. +For such errors, Lua does not call the message handler +(as this kind of error typically has no relation +with the function being called). +
  • + +
+ + + + +

lua_pcallk

+[-(nargs + 1), +(nresults|1), –] +

int lua_pcallk (lua_State *L,
+                int nargs,
+                int nresults,
+                int msgh,
+                lua_KContext ctx,
+                lua_KFunction k);
+ +

+This function behaves exactly like lua_pcall, +but allows the called function to yield (see §4.7). + + + + + +


lua_pop

+[-n, +0, –] +

void lua_pop (lua_State *L, int n);
+ +

+Pops n elements from the stack. + + + + + +


lua_pushboolean

+[-0, +1, –] +

void lua_pushboolean (lua_State *L, int b);
+ +

+Pushes a boolean value with value b onto the stack. + + + + + +


lua_pushcclosure

+[-n, +1, m] +

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);
+ +

+Pushes a new C closure onto the stack. + + +

+When a C function is created, +it is possible to associate some values with it, +thus creating a C closure (see §4.4); +these values are then accessible to the function whenever it is called. +To associate values with a C function, +first these values must be pushed onto the stack +(when there are multiple values, the first value is pushed first). +Then lua_pushcclosure +is called to create and push the C function onto the stack, +with the argument n telling how many values will be +associated with the function. +lua_pushcclosure also pops these values from the stack. + + +

+The maximum value for n is 255. + + +

+When n is zero, +this function creates a light C function, +which is just a pointer to the C function. +In that case, it never raises a memory error. + + + + + +


lua_pushcfunction

+[-0, +1, –] +

void lua_pushcfunction (lua_State *L, lua_CFunction f);
+ +

+Pushes a C function onto the stack. +This function receives a pointer to a C function +and pushes onto the stack a Lua value of type function that, +when called, invokes the corresponding C function. + + +

+Any function to be callable by Lua must +follow the correct protocol to receive its parameters +and return its results (see lua_CFunction). + + + + + +


lua_pushfstring

+[-0, +1, e] +

const char *lua_pushfstring (lua_State *L, const char *fmt, ...);
+ +

+Pushes onto the stack a formatted string +and returns a pointer to this string. +It is similar to the ISO C function sprintf, +but has some important differences: + +

    + +
  • +You do not have to allocate space for the result: +the result is a Lua string and Lua takes care of memory allocation +(and deallocation, through garbage collection). +
  • + +
  • +The conversion specifiers are quite restricted. +There are no flags, widths, or precisions. +The conversion specifiers can only be +'%%' (inserts the character '%'), +'%s' (inserts a zero-terminated string, with no size restrictions), +'%f' (inserts a lua_Number), +'%I' (inserts a lua_Integer), +'%p' (inserts a pointer as a hexadecimal numeral), +'%d' (inserts an int), +'%c' (inserts an int as a one-byte character), and +'%U' (inserts a long int as a UTF-8 byte sequence). +
  • + +
+ +

+Unlike other push functions, +this function checks for the stack space it needs, +including the slot for its result. + + + + + +


lua_pushglobaltable

+[-0, +1, –] +

void lua_pushglobaltable (lua_State *L);
+ +

+Pushes the global environment onto the stack. + + + + + +


lua_pushinteger

+[-0, +1, –] +

void lua_pushinteger (lua_State *L, lua_Integer n);
+ +

+Pushes an integer with value n onto the stack. + + + + + +


lua_pushlightuserdata

+[-0, +1, –] +

void lua_pushlightuserdata (lua_State *L, void *p);
+ +

+Pushes a light userdata onto the stack. + + +

+Userdata represent C values in Lua. +A light userdata represents a pointer, a void*. +It is a value (like a number): +you do not create it, it has no individual metatable, +and it is not collected (as it was never created). +A light userdata is equal to "any" +light userdata with the same C address. + + + + + +


lua_pushliteral

+[-0, +1, m] +

const char *lua_pushliteral (lua_State *L, const char *s);
+ +

+This macro is equivalent to lua_pushstring, +but should be used only when s is a literal string. + + + + + +


lua_pushlstring

+[-0, +1, m] +

const char *lua_pushlstring (lua_State *L, const char *s, size_t len);
+ +

+Pushes the string pointed to by s with size len +onto the stack. +Lua makes (or reuses) an internal copy of the given string, +so the memory at s can be freed or reused immediately after +the function returns. +The string can contain any binary data, +including embedded zeros. + + +

+Returns a pointer to the internal copy of the string. + + + + + +


lua_pushnil

+[-0, +1, –] +

void lua_pushnil (lua_State *L);
+ +

+Pushes a nil value onto the stack. + + + + + +


lua_pushnumber

+[-0, +1, –] +

void lua_pushnumber (lua_State *L, lua_Number n);
+ +

+Pushes a float with value n onto the stack. + + + + + +


lua_pushstring

+[-0, +1, m] +

const char *lua_pushstring (lua_State *L, const char *s);
+ +

+Pushes the zero-terminated string pointed to by s +onto the stack. +Lua makes (or reuses) an internal copy of the given string, +so the memory at s can be freed or reused immediately after +the function returns. + + +

+Returns a pointer to the internal copy of the string. + + +

+If s is NULL, pushes nil and returns NULL. + + + + + +


lua_pushthread

+[-0, +1, –] +

int lua_pushthread (lua_State *L);
+ +

+Pushes the thread represented by L onto the stack. +Returns 1 if this thread is the main thread of its state. + + + + + +


lua_pushvalue

+[-0, +1, –] +

void lua_pushvalue (lua_State *L, int index);
+ +

+Pushes a copy of the element at the given index +onto the stack. + + + + + +


lua_pushvfstring

+[-0, +1, m] +

const char *lua_pushvfstring (lua_State *L,
+                              const char *fmt,
+                              va_list argp);
+ +

+Equivalent to lua_pushfstring, except that it receives a va_list +instead of a variable number of arguments. + + + + + +


lua_rawequal

+[-0, +0, –] +

int lua_rawequal (lua_State *L, int index1, int index2);
+ +

+Returns 1 if the two values in indices index1 and +index2 are primitively equal +(that is, without calling the __eq metamethod). +Otherwise returns 0. +Also returns 0 if any of the indices are not valid. + + + + + +


lua_rawget

+[-1, +1, –] +

int lua_rawget (lua_State *L, int index);
+ +

+Similar to lua_gettable, but does a raw access +(i.e., without metamethods). + + + + + +


lua_rawgeti

+[-0, +1, –] +

int lua_rawgeti (lua_State *L, int index, lua_Integer n);
+ +

+Pushes onto the stack the value t[n], +where t is the table at the given index. +The access is raw, +that is, it does not invoke the __index metamethod. + + +

+Returns the type of the pushed value. + + + + + +


lua_rawgetp

+[-0, +1, –] +

int lua_rawgetp (lua_State *L, int index, const void *p);
+ +

+Pushes onto the stack the value t[k], +where t is the table at the given index and +k is the pointer p represented as a light userdata. +The access is raw; +that is, it does not invoke the __index metamethod. + + +

+Returns the type of the pushed value. + + + + + +


lua_rawlen

+[-0, +0, –] +

size_t lua_rawlen (lua_State *L, int index);
+ +

+Returns the raw "length" of the value at the given index: +for strings, this is the string length; +for tables, this is the result of the length operator ('#') +with no metamethods; +for userdata, this is the size of the block of memory allocated +for the userdata; +for other values, it is 0. + + + + + +


lua_rawset

+[-2, +0, m] +

void lua_rawset (lua_State *L, int index);
+ +

+Similar to lua_settable, but does a raw assignment +(i.e., without metamethods). + + + + + +


lua_rawseti

+[-1, +0, m] +

void lua_rawseti (lua_State *L, int index, lua_Integer i);
+ +

+Does the equivalent of t[i] = v, +where t is the table at the given index +and v is the value at the top of the stack. + + +

+This function pops the value from the stack. +The assignment is raw, +that is, it does not invoke the __newindex metamethod. + + + + + +


lua_rawsetp

+[-1, +0, m] +

void lua_rawsetp (lua_State *L, int index, const void *p);
+ +

+Does the equivalent of t[p] = v, +where t is the table at the given index, +p is encoded as a light userdata, +and v is the value at the top of the stack. + + +

+This function pops the value from the stack. +The assignment is raw, +that is, it does not invoke __newindex metamethod. + + + + + +


lua_Reader

+
typedef const char * (*lua_Reader) (lua_State *L,
+                                    void *data,
+                                    size_t *size);
+ +

+The reader function used by lua_load. +Every time it needs another piece of the chunk, +lua_load calls the reader, +passing along its data parameter. +The reader must return a pointer to a block of memory +with a new piece of the chunk +and set size to the block size. +The block must exist until the reader function is called again. +To signal the end of the chunk, +the reader must return NULL or set size to zero. +The reader function may return pieces of any size greater than zero. + + + + + +


lua_register

+[-0, +0, e] +

void lua_register (lua_State *L, const char *name, lua_CFunction f);
+ +

+Sets the C function f as the new value of global name. +It is defined as a macro: + +

+     #define lua_register(L,n,f) \
+            (lua_pushcfunction(L, f), lua_setglobal(L, n))
+
+ + + + +

lua_remove

+[-1, +0, –] +

void lua_remove (lua_State *L, int index);
+ +

+Removes the element at the given valid index, +shifting down the elements above this index to fill the gap. +This function cannot be called with a pseudo-index, +because a pseudo-index is not an actual stack position. + + + + + +


lua_replace

+[-1, +0, –] +

void lua_replace (lua_State *L, int index);
+ +

+Moves the top element into the given valid index +without shifting any element +(therefore replacing the value at that given index), +and then pops the top element. + + + + + +


lua_resume

+[-?, +?, –] +

int lua_resume (lua_State *L, lua_State *from, int nargs);
+ +

+Starts and resumes a coroutine in the given thread L. + + +

+To start a coroutine, +you push onto the thread stack the main function plus any arguments; +then you call lua_resume, +with nargs being the number of arguments. +This call returns when the coroutine suspends or finishes its execution. +When it returns, the stack contains all values passed to lua_yield, +or all values returned by the body function. +lua_resume returns +LUA_YIELD if the coroutine yields, +LUA_OK if the coroutine finishes its execution +without errors, +or an error code in case of errors (see lua_pcall). + + +

+In case of errors, +the stack is not unwound, +so you can use the debug API over it. +The error object is on the top of the stack. + + +

+To resume a coroutine, +you remove any results from the last lua_yield, +put on its stack only the values to +be passed as results from yield, +and then call lua_resume. + + +

+The parameter from represents the coroutine that is resuming L. +If there is no such coroutine, +this parameter can be NULL. + + + + + +


lua_rotate

+[-0, +0, –] +

void lua_rotate (lua_State *L, int idx, int n);
+ +

+Rotates the stack elements between the valid index idx +and the top of the stack. +The elements are rotated n positions in the direction of the top, +for a positive n, +or -n positions in the direction of the bottom, +for a negative n. +The absolute value of n must not be greater than the size +of the slice being rotated. +This function cannot be called with a pseudo-index, +because a pseudo-index is not an actual stack position. + + + + + +


lua_setallocf

+[-0, +0, –] +

void lua_setallocf (lua_State *L, lua_Alloc f, void *ud);
+ +

+Changes the allocator function of a given state to f +with user data ud. + + + + + +


lua_setfield

+[-1, +0, e] +

void lua_setfield (lua_State *L, int index, const char *k);
+ +

+Does the equivalent to t[k] = v, +where t is the value at the given index +and v is the value at the top of the stack. + + +

+This function pops the value from the stack. +As in Lua, this function may trigger a metamethod +for the "newindex" event (see §2.4). + + + + + +


lua_setglobal

+[-1, +0, e] +

void lua_setglobal (lua_State *L, const char *name);
+ +

+Pops a value from the stack and +sets it as the new value of global name. + + + + + +


lua_seti

+[-1, +0, e] +

void lua_seti (lua_State *L, int index, lua_Integer n);
+ +

+Does the equivalent to t[n] = v, +where t is the value at the given index +and v is the value at the top of the stack. + + +

+This function pops the value from the stack. +As in Lua, this function may trigger a metamethod +for the "newindex" event (see §2.4). + + + + + +


lua_setmetatable

+[-1, +0, –] +

void lua_setmetatable (lua_State *L, int index);
+ +

+Pops a table from the stack and +sets it as the new metatable for the value at the given index. + + + + + +


lua_settable

+[-2, +0, e] +

void lua_settable (lua_State *L, int index);
+ +

+Does the equivalent to t[k] = v, +where t is the value at the given index, +v is the value at the top of the stack, +and k is the value just below the top. + + +

+This function pops both the key and the value from the stack. +As in Lua, this function may trigger a metamethod +for the "newindex" event (see §2.4). + + + + + +


lua_settop

+[-?, +?, –] +

void lua_settop (lua_State *L, int index);
+ +

+Accepts any index, or 0, +and sets the stack top to this index. +If the new top is larger than the old one, +then the new elements are filled with nil. +If index is 0, then all stack elements are removed. + + + + + +


lua_setuservalue

+[-1, +0, –] +

void lua_setuservalue (lua_State *L, int index);
+ +

+Pops a value from the stack and sets it as +the new value associated to the full userdata at the given index. + + + + + +


lua_State

+
typedef struct lua_State lua_State;
+ +

+An opaque structure that points to a thread and indirectly +(through the thread) to the whole state of a Lua interpreter. +The Lua library is fully reentrant: +it has no global variables. +All information about a state is accessible through this structure. + + +

+A pointer to this structure must be passed as the first argument to +every function in the library, except to lua_newstate, +which creates a Lua state from scratch. + + + + + +


lua_status

+[-0, +0, –] +

int lua_status (lua_State *L);
+ +

+Returns the status of the thread L. + + +

+The status can be 0 (LUA_OK) for a normal thread, +an error code if the thread finished the execution +of a lua_resume with an error, +or LUA_YIELD if the thread is suspended. + + +

+You can only call functions in threads with status LUA_OK. +You can resume threads with status LUA_OK +(to start a new coroutine) or LUA_YIELD +(to resume a coroutine). + + + + + +


lua_stringtonumber

+[-0, +1, –] +

size_t lua_stringtonumber (lua_State *L, const char *s);
+ +

+Converts the zero-terminated string s to a number, +pushes that number into the stack, +and returns the total size of the string, +that is, its length plus one. +The conversion can result in an integer or a float, +according to the lexical conventions of Lua (see §3.1). +The string may have leading and trailing spaces and a sign. +If the string is not a valid numeral, +returns 0 and pushes nothing. +(Note that the result can be used as a boolean, +true if the conversion succeeds.) + + + + + +


lua_toboolean

+[-0, +0, –] +

int lua_toboolean (lua_State *L, int index);
+ +

+Converts the Lua value at the given index to a C boolean +value (0 or 1). +Like all tests in Lua, +lua_toboolean returns true for any Lua value +different from false and nil; +otherwise it returns false. +(If you want to accept only actual boolean values, +use lua_isboolean to test the value's type.) + + + + + +


lua_tocfunction

+[-0, +0, –] +

lua_CFunction lua_tocfunction (lua_State *L, int index);
+ +

+Converts a value at the given index to a C function. +That value must be a C function; +otherwise, returns NULL. + + + + + +


lua_tointeger

+[-0, +0, –] +

lua_Integer lua_tointeger (lua_State *L, int index);
+ +

+Equivalent to lua_tointegerx with isnum equal to NULL. + + + + + +


lua_tointegerx

+[-0, +0, –] +

lua_Integer lua_tointegerx (lua_State *L, int index, int *isnum);
+ +

+Converts the Lua value at the given index +to the signed integral type lua_Integer. +The Lua value must be an integer, +or a number or string convertible to an integer (see §3.4.3); +otherwise, lua_tointegerx returns 0. + + +

+If isnum is not NULL, +its referent is assigned a boolean value that +indicates whether the operation succeeded. + + + + + +


lua_tolstring

+[-0, +0, m] +

const char *lua_tolstring (lua_State *L, int index, size_t *len);
+ +

+Converts the Lua value at the given index to a C string. +If len is not NULL, +it sets *len with the string length. +The Lua value must be a string or a number; +otherwise, the function returns NULL. +If the value is a number, +then lua_tolstring also +changes the actual value in the stack to a string. +(This change confuses lua_next +when lua_tolstring is applied to keys during a table traversal.) + + +

+lua_tolstring returns a pointer +to a string inside the Lua state. +This string always has a zero ('\0') +after its last character (as in C), +but can contain other zeros in its body. + + +

+Because Lua has garbage collection, +there is no guarantee that the pointer returned by lua_tolstring +will be valid after the corresponding Lua value is removed from the stack. + + + + + +


lua_tonumber

+[-0, +0, –] +

lua_Number lua_tonumber (lua_State *L, int index);
+ +

+Equivalent to lua_tonumberx with isnum equal to NULL. + + + + + +


lua_tonumberx

+[-0, +0, –] +

lua_Number lua_tonumberx (lua_State *L, int index, int *isnum);
+ +

+Converts the Lua value at the given index +to the C type lua_Number (see lua_Number). +The Lua value must be a number or a string convertible to a number +(see §3.4.3); +otherwise, lua_tonumberx returns 0. + + +

+If isnum is not NULL, +its referent is assigned a boolean value that +indicates whether the operation succeeded. + + + + + +


lua_topointer

+[-0, +0, –] +

const void *lua_topointer (lua_State *L, int index);
+ +

+Converts the value at the given index to a generic +C pointer (void*). +The value can be a userdata, a table, a thread, or a function; +otherwise, lua_topointer returns NULL. +Different objects will give different pointers. +There is no way to convert the pointer back to its original value. + + +

+Typically this function is used only for hashing and debug information. + + + + + +


lua_tostring

+[-0, +0, m] +

const char *lua_tostring (lua_State *L, int index);
+ +

+Equivalent to lua_tolstring with len equal to NULL. + + + + + +


lua_tothread

+[-0, +0, –] +

lua_State *lua_tothread (lua_State *L, int index);
+ +

+Converts the value at the given index to a Lua thread +(represented as lua_State*). +This value must be a thread; +otherwise, the function returns NULL. + + + + + +


lua_touserdata

+[-0, +0, –] +

void *lua_touserdata (lua_State *L, int index);
+ +

+If the value at the given index is a full userdata, +returns its block address. +If the value is a light userdata, +returns its pointer. +Otherwise, returns NULL. + + + + + +


lua_type

+[-0, +0, –] +

int lua_type (lua_State *L, int index);
+ +

+Returns the type of the value in the given valid index, +or LUA_TNONE for a non-valid (but acceptable) index. +The types returned by lua_type are coded by the following constants +defined in lua.h: +LUA_TNIL (0), +LUA_TNUMBER, +LUA_TBOOLEAN, +LUA_TSTRING, +LUA_TTABLE, +LUA_TFUNCTION, +LUA_TUSERDATA, +LUA_TTHREAD, +and +LUA_TLIGHTUSERDATA. + + + + + +


lua_typename

+[-0, +0, –] +

const char *lua_typename (lua_State *L, int tp);
+ +

+Returns the name of the type encoded by the value tp, +which must be one the values returned by lua_type. + + + + + +


lua_Unsigned

+
typedef ... lua_Unsigned;
+ +

+The unsigned version of lua_Integer. + + + + + +


lua_upvalueindex

+[-0, +0, –] +

int lua_upvalueindex (int i);
+ +

+Returns the pseudo-index that represents the i-th upvalue of +the running function (see §4.4). + + + + + +


lua_version

+[-0, +0, –] +

const lua_Number *lua_version (lua_State *L);
+ +

+Returns the address of the version number +(a C static variable) +stored in the Lua core. +When called with a valid lua_State, +returns the address of the version used to create that state. +When called with NULL, +returns the address of the version running the call. + + + + + +


lua_Writer

+
typedef int (*lua_Writer) (lua_State *L,
+                           const void* p,
+                           size_t sz,
+                           void* ud);
+ +

+The type of the writer function used by lua_dump. +Every time it produces another piece of chunk, +lua_dump calls the writer, +passing along the buffer to be written (p), +its size (sz), +and the data parameter supplied to lua_dump. + + +

+The writer returns an error code: +0 means no errors; +any other value means an error and stops lua_dump from +calling the writer again. + + + + + +


lua_xmove

+[-?, +?, –] +

void lua_xmove (lua_State *from, lua_State *to, int n);
+ +

+Exchange values between different threads of the same state. + + +

+This function pops n values from the stack from, +and pushes them onto the stack to. + + + + + +


lua_yield

+[-?, +?, e] +

int lua_yield (lua_State *L, int nresults);
+ +

+This function is equivalent to lua_yieldk, +but it has no continuation (see §4.7). +Therefore, when the thread resumes, +it continues the function that called +the function calling lua_yield. + + + + + +


lua_yieldk

+[-?, +?, e] +

int lua_yieldk (lua_State *L,
+                int nresults,
+                lua_KContext ctx,
+                lua_KFunction k);
+ +

+Yields a coroutine (thread). + + +

+When a C function calls lua_yieldk, +the running coroutine suspends its execution, +and the call to lua_resume that started this coroutine returns. +The parameter nresults is the number of values from the stack +that will be passed as results to lua_resume. + + +

+When the coroutine is resumed again, +Lua calls the given continuation function k to continue +the execution of the C function that yielded (see §4.7). +This continuation function receives the same stack +from the previous function, +with the n results removed and +replaced by the arguments passed to lua_resume. +Moreover, +the continuation function receives the value ctx +that was passed to lua_yieldk. + + +

+Usually, this function does not return; +when the coroutine eventually resumes, +it continues executing the continuation function. +However, there is one special case, +which is when this function is called +from inside a line or a count hook (see §4.9). +In that case, lua_yieldk should be called with no continuation +(probably in the form of lua_yield) and no results, +and the hook should return immediately after the call. +Lua will yield and, +when the coroutine resumes again, +it will continue the normal execution +of the (Lua) function that triggered the hook. + + +

+This function can raise an error if it is called from a thread +with a pending C call with no continuation function, +or it is called from a thread that is not running inside a resume +(e.g., the main thread). + + + + + + + +

4.9 – The Debug Interface

+ +

+Lua has no built-in debugging facilities. +Instead, it offers a special interface +by means of functions and hooks. +This interface allows the construction of different +kinds of debuggers, profilers, and other tools +that need "inside information" from the interpreter. + + + +


lua_Debug

+
typedef struct lua_Debug {
+  int event;
+  const char *name;           /* (n) */
+  const char *namewhat;       /* (n) */
+  const char *what;           /* (S) */
+  const char *source;         /* (S) */
+  int currentline;            /* (l) */
+  int linedefined;            /* (S) */
+  int lastlinedefined;        /* (S) */
+  unsigned char nups;         /* (u) number of upvalues */
+  unsigned char nparams;      /* (u) number of parameters */
+  char isvararg;              /* (u) */
+  char istailcall;            /* (t) */
+  char short_src[LUA_IDSIZE]; /* (S) */
+  /* private part */
+  other fields
+} lua_Debug;
+ +

+A structure used to carry different pieces of +information about a function or an activation record. +lua_getstack fills only the private part +of this structure, for later use. +To fill the other fields of lua_Debug with useful information, +call lua_getinfo. + + +

+The fields of lua_Debug have the following meaning: + +

    + +
  • source: +the name of the chunk that created the function. +If source starts with a '@', +it means that the function was defined in a file where +the file name follows the '@'. +If source starts with a '=', +the remainder of its contents describe the source in a user-dependent manner. +Otherwise, +the function was defined in a string where +source is that string. +
  • + +
  • short_src: +a "printable" version of source, to be used in error messages. +
  • + +
  • linedefined: +the line number where the definition of the function starts. +
  • + +
  • lastlinedefined: +the line number where the definition of the function ends. +
  • + +
  • what: +the string "Lua" if the function is a Lua function, +"C" if it is a C function, +"main" if it is the main part of a chunk. +
  • + +
  • currentline: +the current line where the given function is executing. +When no line information is available, +currentline is set to -1. +
  • + +
  • name: +a reasonable name for the given function. +Because functions in Lua are first-class values, +they do not have a fixed name: +some functions can be the value of multiple global variables, +while others can be stored only in a table field. +The lua_getinfo function checks how the function was +called to find a suitable name. +If it cannot find a name, +then name is set to NULL. +
  • + +
  • namewhat: +explains the name field. +The value of namewhat can be +"global", "local", "method", +"field", "upvalue", or "" (the empty string), +according to how the function was called. +(Lua uses the empty string when no other option seems to apply.) +
  • + +
  • istailcall: +true if this function invocation was called by a tail call. +In this case, the caller of this level is not in the stack. +
  • + +
  • nups: +the number of upvalues of the function. +
  • + +
  • nparams: +the number of fixed parameters of the function +(always 0 for C functions). +
  • + +
  • isvararg: +true if the function is a vararg function +(always true for C functions). +
  • + +
+ + + + +

lua_gethook

+[-0, +0, –] +

lua_Hook lua_gethook (lua_State *L);
+ +

+Returns the current hook function. + + + + + +


lua_gethookcount

+[-0, +0, –] +

int lua_gethookcount (lua_State *L);
+ +

+Returns the current hook count. + + + + + +


lua_gethookmask

+[-0, +0, –] +

int lua_gethookmask (lua_State *L);
+ +

+Returns the current hook mask. + + + + + +


lua_getinfo

+[-(0|1), +(0|1|2), e] +

int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar);
+ +

+Gets information about a specific function or function invocation. + + +

+To get information about a function invocation, +the parameter ar must be a valid activation record that was +filled by a previous call to lua_getstack or +given as argument to a hook (see lua_Hook). + + +

+To get information about a function you push it onto the stack +and start the what string with the character '>'. +(In that case, +lua_getinfo pops the function from the top of the stack.) +For instance, to know in which line a function f was defined, +you can write the following code: + +

+     lua_Debug ar;
+     lua_getglobal(L, "f");  /* get global 'f' */
+     lua_getinfo(L, ">S", &ar);
+     printf("%d\n", ar.linedefined);
+
+ +

+Each character in the string what +selects some fields of the structure ar to be filled or +a value to be pushed on the stack: + +

    + +
  • 'n': fills in the field name and namewhat; +
  • + +
  • 'S': +fills in the fields source, short_src, +linedefined, lastlinedefined, and what; +
  • + +
  • 'l': fills in the field currentline; +
  • + +
  • 't': fills in the field istailcall; +
  • + +
  • 'u': fills in the fields +nups, nparams, and isvararg; +
  • + +
  • 'f': +pushes onto the stack the function that is +running at the given level; +
  • + +
  • 'L': +pushes onto the stack a table whose indices are the +numbers of the lines that are valid on the function. +(A valid line is a line with some associated code, +that is, a line where you can put a break point. +Non-valid lines include empty lines and comments.) + + +

    +If this option is given together with option 'f', +its table is pushed after the function. +

  • + +
+ +

+This function returns 0 on error +(for instance, an invalid option in what). + + + + + +


lua_getlocal

+[-0, +(0|1), –] +

const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n);
+ +

+Gets information about a local variable of +a given activation record or a given function. + + +

+In the first case, +the parameter ar must be a valid activation record that was +filled by a previous call to lua_getstack or +given as argument to a hook (see lua_Hook). +The index n selects which local variable to inspect; +see debug.getlocal for details about variable indices +and names. + + +

+lua_getlocal pushes the variable's value onto the stack +and returns its name. + + +

+In the second case, ar must be NULL and the function +to be inspected must be at the top of the stack. +In this case, only parameters of Lua functions are visible +(as there is no information about what variables are active) +and no values are pushed onto the stack. + + +

+Returns NULL (and pushes nothing) +when the index is greater than +the number of active local variables. + + + + + +


lua_getstack

+[-0, +0, –] +

int lua_getstack (lua_State *L, int level, lua_Debug *ar);
+ +

+Gets information about the interpreter runtime stack. + + +

+This function fills parts of a lua_Debug structure with +an identification of the activation record +of the function executing at a given level. +Level 0 is the current running function, +whereas level n+1 is the function that has called level n +(except for tail calls, which do not count on the stack). +When there are no errors, lua_getstack returns 1; +when called with a level greater than the stack depth, +it returns 0. + + + + + +


lua_getupvalue

+[-0, +(0|1), –] +

const char *lua_getupvalue (lua_State *L, int funcindex, int n);
+ +

+Gets information about the n-th upvalue +of the closure at index funcindex. +It pushes the upvalue's value onto the stack +and returns its name. +Returns NULL (and pushes nothing) +when the index n is greater than the number of upvalues. + + +

+For C functions, this function uses the empty string "" +as a name for all upvalues. +(For Lua functions, +upvalues are the external local variables that the function uses, +and that are consequently included in its closure.) + + +

+Upvalues have no particular order, +as they are active through the whole function. +They are numbered in an arbitrary order. + + + + + +


lua_Hook

+
typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);
+ +

+Type for debugging hook functions. + + +

+Whenever a hook is called, its ar argument has its field +event set to the specific event that triggered the hook. +Lua identifies these events with the following constants: +LUA_HOOKCALL, LUA_HOOKRET, +LUA_HOOKTAILCALL, LUA_HOOKLINE, +and LUA_HOOKCOUNT. +Moreover, for line events, the field currentline is also set. +To get the value of any other field in ar, +the hook must call lua_getinfo. + + +

+For call events, event can be LUA_HOOKCALL, +the normal value, or LUA_HOOKTAILCALL, for a tail call; +in this case, there will be no corresponding return event. + + +

+While Lua is running a hook, it disables other calls to hooks. +Therefore, if a hook calls back Lua to execute a function or a chunk, +this execution occurs without any calls to hooks. + + +

+Hook functions cannot have continuations, +that is, they cannot call lua_yieldk, +lua_pcallk, or lua_callk with a non-null k. + + +

+Hook functions can yield under the following conditions: +Only count and line events can yield; +to yield, a hook function must finish its execution +calling lua_yield with nresults equal to zero +(that is, with no values). + + + + + +


lua_sethook

+[-0, +0, –] +

void lua_sethook (lua_State *L, lua_Hook f, int mask, int count);
+ +

+Sets the debugging hook function. + + +

+Argument f is the hook function. +mask specifies on which events the hook will be called: +it is formed by a bitwise OR of the constants +LUA_MASKCALL, +LUA_MASKRET, +LUA_MASKLINE, +and LUA_MASKCOUNT. +The count argument is only meaningful when the mask +includes LUA_MASKCOUNT. +For each event, the hook is called as explained below: + +

    + +
  • The call hook: is called when the interpreter calls a function. +The hook is called just after Lua enters the new function, +before the function gets its arguments. +
  • + +
  • The return hook: is called when the interpreter returns from a function. +The hook is called just before Lua leaves the function. +There is no standard way to access the values +to be returned by the function. +
  • + +
  • The line hook: is called when the interpreter is about to +start the execution of a new line of code, +or when it jumps back in the code (even to the same line). +(This event only happens while Lua is executing a Lua function.) +
  • + +
  • The count hook: is called after the interpreter executes every +count instructions. +(This event only happens while Lua is executing a Lua function.) +
  • + +
+ +

+A hook is disabled by setting mask to zero. + + + + + +


lua_setlocal

+[-(0|1), +0, –] +

const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n);
+ +

+Sets the value of a local variable of a given activation record. +It assigns the value at the top of the stack +to the variable and returns its name. +It also pops the value from the stack. + + +

+Returns NULL (and pops nothing) +when the index is greater than +the number of active local variables. + + +

+Parameters ar and n are as in function lua_getlocal. + + + + + +


lua_setupvalue

+[-(0|1), +0, –] +

const char *lua_setupvalue (lua_State *L, int funcindex, int n);
+ +

+Sets the value of a closure's upvalue. +It assigns the value at the top of the stack +to the upvalue and returns its name. +It also pops the value from the stack. + + +

+Returns NULL (and pops nothing) +when the index n is greater than the number of upvalues. + + +

+Parameters funcindex and n are as in function lua_getupvalue. + + + + + +


lua_upvalueid

+[-0, +0, –] +

void *lua_upvalueid (lua_State *L, int funcindex, int n);
+ +

+Returns a unique identifier for the upvalue numbered n +from the closure at index funcindex. + + +

+These unique identifiers allow a program to check whether different +closures share upvalues. +Lua closures that share an upvalue +(that is, that access a same external local variable) +will return identical ids for those upvalue indices. + + +

+Parameters funcindex and n are as in function lua_getupvalue, +but n cannot be greater than the number of upvalues. + + + + + +


lua_upvaluejoin

+[-0, +0, –] +

void lua_upvaluejoin (lua_State *L, int funcindex1, int n1,
+                                    int funcindex2, int n2);
+ +

+Make the n1-th upvalue of the Lua closure at index funcindex1 +refer to the n2-th upvalue of the Lua closure at index funcindex2. + + + + + + + +

5 – The Auxiliary Library

+ +

+ +The auxiliary library provides several convenient functions +to interface C with Lua. +While the basic API provides the primitive functions for all +interactions between C and Lua, +the auxiliary library provides higher-level functions for some +common tasks. + + +

+All functions and types from the auxiliary library +are defined in header file lauxlib.h and +have a prefix luaL_. + + +

+All functions in the auxiliary library are built on +top of the basic API, +and so they provide nothing that cannot be done with that API. +Nevertheless, the use of the auxiliary library ensures +more consistency to your code. + + +

+Several functions in the auxiliary library use internally some +extra stack slots. +When a function in the auxiliary library uses less than five slots, +it does not check the stack size; +it simply assumes that there are enough slots. + + +

+Several functions in the auxiliary library are used to +check C function arguments. +Because the error message is formatted for arguments +(e.g., "bad argument #1"), +you should not use these functions for other stack values. + + +

+Functions called luaL_check* +always raise an error if the check is not satisfied. + + + +

5.1 – Functions and Types

+ +

+Here we list all functions and types from the auxiliary library +in alphabetical order. + + + +


luaL_addchar

+[-?, +?, m] +

void luaL_addchar (luaL_Buffer *B, char c);
+ +

+Adds the byte c to the buffer B +(see luaL_Buffer). + + + + + +


luaL_addlstring

+[-?, +?, m] +

void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l);
+ +

+Adds the string pointed to by s with length l to +the buffer B +(see luaL_Buffer). +The string can contain embedded zeros. + + + + + +


luaL_addsize

+[-?, +?, –] +

void luaL_addsize (luaL_Buffer *B, size_t n);
+ +

+Adds to the buffer B (see luaL_Buffer) +a string of length n previously copied to the +buffer area (see luaL_prepbuffer). + + + + + +


luaL_addstring

+[-?, +?, m] +

void luaL_addstring (luaL_Buffer *B, const char *s);
+ +

+Adds the zero-terminated string pointed to by s +to the buffer B +(see luaL_Buffer). + + + + + +


luaL_addvalue

+[-1, +?, m] +

void luaL_addvalue (luaL_Buffer *B);
+ +

+Adds the value at the top of the stack +to the buffer B +(see luaL_Buffer). +Pops the value. + + +

+This is the only function on string buffers that can (and must) +be called with an extra element on the stack, +which is the value to be added to the buffer. + + + + + +


luaL_argcheck

+[-0, +0, v] +

void luaL_argcheck (lua_State *L,
+                    int cond,
+                    int arg,
+                    const char *extramsg);
+ +

+Checks whether cond is true. +If it is not, raises an error with a standard message (see luaL_argerror). + + + + + +


luaL_argerror

+[-0, +0, v] +

int luaL_argerror (lua_State *L, int arg, const char *extramsg);
+ +

+Raises an error reporting a problem with argument arg +of the C function that called it, +using a standard message +that includes extramsg as a comment: + +

+     bad argument #arg to 'funcname' (extramsg)
+

+This function never returns. + + + + + +


luaL_Buffer

+
typedef struct luaL_Buffer luaL_Buffer;
+ +

+Type for a string buffer. + + +

+A string buffer allows C code to build Lua strings piecemeal. +Its pattern of use is as follows: + +

    + +
  • First declare a variable b of type luaL_Buffer.
  • + +
  • Then initialize it with a call luaL_buffinit(L, &b).
  • + +
  • +Then add string pieces to the buffer calling any of +the luaL_add* functions. +
  • + +
  • +Finish by calling luaL_pushresult(&b). +This call leaves the final string on the top of the stack. +
  • + +
+ +

+If you know beforehand the total size of the resulting string, +you can use the buffer like this: + +

    + +
  • First declare a variable b of type luaL_Buffer.
  • + +
  • Then initialize it and preallocate a space of +size sz with a call luaL_buffinitsize(L, &b, sz).
  • + +
  • Then copy the string into that space.
  • + +
  • +Finish by calling luaL_pushresultsize(&b, sz), +where sz is the total size of the resulting string +copied into that space. +
  • + +
+ +

+During its normal operation, +a string buffer uses a variable number of stack slots. +So, while using a buffer, you cannot assume that you know where +the top of the stack is. +You can use the stack between successive calls to buffer operations +as long as that use is balanced; +that is, +when you call a buffer operation, +the stack is at the same level +it was immediately after the previous buffer operation. +(The only exception to this rule is luaL_addvalue.) +After calling luaL_pushresult the stack is back to its +level when the buffer was initialized, +plus the final string on its top. + + + + + +


luaL_buffinit

+[-0, +0, –] +

void luaL_buffinit (lua_State *L, luaL_Buffer *B);
+ +

+Initializes a buffer B. +This function does not allocate any space; +the buffer must be declared as a variable +(see luaL_Buffer). + + + + + +


luaL_buffinitsize

+[-?, +?, m] +

char *luaL_buffinitsize (lua_State *L, luaL_Buffer *B, size_t sz);
+ +

+Equivalent to the sequence +luaL_buffinit, luaL_prepbuffsize. + + + + + +


luaL_callmeta

+[-0, +(0|1), e] +

int luaL_callmeta (lua_State *L, int obj, const char *e);
+ +

+Calls a metamethod. + + +

+If the object at index obj has a metatable and this +metatable has a field e, +this function calls this field passing the object as its only argument. +In this case this function returns true and pushes onto the +stack the value returned by the call. +If there is no metatable or no metamethod, +this function returns false (without pushing any value on the stack). + + + + + +


luaL_checkany

+[-0, +0, v] +

void luaL_checkany (lua_State *L, int arg);
+ +

+Checks whether the function has an argument +of any type (including nil) at position arg. + + + + + +


luaL_checkinteger

+[-0, +0, v] +

lua_Integer luaL_checkinteger (lua_State *L, int arg);
+ +

+Checks whether the function argument arg is an integer +(or can be converted to an integer) +and returns this integer cast to a lua_Integer. + + + + + +


luaL_checklstring

+[-0, +0, v] +

const char *luaL_checklstring (lua_State *L, int arg, size_t *l);
+ +

+Checks whether the function argument arg is a string +and returns this string; +if l is not NULL fills *l +with the string's length. + + +

+This function uses lua_tolstring to get its result, +so all conversions and caveats of that function apply here. + + + + + +


luaL_checknumber

+[-0, +0, v] +

lua_Number luaL_checknumber (lua_State *L, int arg);
+ +

+Checks whether the function argument arg is a number +and returns this number. + + + + + +


luaL_checkoption

+[-0, +0, v] +

int luaL_checkoption (lua_State *L,
+                      int arg,
+                      const char *def,
+                      const char *const lst[]);
+ +

+Checks whether the function argument arg is a string and +searches for this string in the array lst +(which must be NULL-terminated). +Returns the index in the array where the string was found. +Raises an error if the argument is not a string or +if the string cannot be found. + + +

+If def is not NULL, +the function uses def as a default value when +there is no argument arg or when this argument is nil. + + +

+This is a useful function for mapping strings to C enums. +(The usual convention in Lua libraries is +to use strings instead of numbers to select options.) + + + + + +


luaL_checkstack

+[-0, +0, v] +

void luaL_checkstack (lua_State *L, int sz, const char *msg);
+ +

+Grows the stack size to top + sz elements, +raising an error if the stack cannot grow to that size. +msg is an additional text to go into the error message +(or NULL for no additional text). + + + + + +


luaL_checkstring

+[-0, +0, v] +

const char *luaL_checkstring (lua_State *L, int arg);
+ +

+Checks whether the function argument arg is a string +and returns this string. + + +

+This function uses lua_tolstring to get its result, +so all conversions and caveats of that function apply here. + + + + + +


luaL_checktype

+[-0, +0, v] +

void luaL_checktype (lua_State *L, int arg, int t);
+ +

+Checks whether the function argument arg has type t. +See lua_type for the encoding of types for t. + + + + + +


luaL_checkudata

+[-0, +0, v] +

void *luaL_checkudata (lua_State *L, int arg, const char *tname);
+ +

+Checks whether the function argument arg is a userdata +of the type tname (see luaL_newmetatable) and +returns the userdata address (see lua_touserdata). + + + + + +


luaL_checkversion

+[-0, +0, v] +

void luaL_checkversion (lua_State *L);
+ +

+Checks whether the core running the call, +the core that created the Lua state, +and the code making the call are all using the same version of Lua. +Also checks whether the core running the call +and the core that created the Lua state +are using the same address space. + + + + + +


luaL_dofile

+[-0, +?, e] +

int luaL_dofile (lua_State *L, const char *filename);
+ +

+Loads and runs the given file. +It is defined as the following macro: + +

+     (luaL_loadfile(L, filename) || lua_pcall(L, 0, LUA_MULTRET, 0))
+

+It returns false if there are no errors +or true in case of errors. + + + + + +


luaL_dostring

+[-0, +?, –] +

int luaL_dostring (lua_State *L, const char *str);
+ +

+Loads and runs the given string. +It is defined as the following macro: + +

+     (luaL_loadstring(L, str) || lua_pcall(L, 0, LUA_MULTRET, 0))
+

+It returns false if there are no errors +or true in case of errors. + + + + + +


luaL_error

+[-0, +0, v] +

int luaL_error (lua_State *L, const char *fmt, ...);
+ +

+Raises an error. +The error message format is given by fmt +plus any extra arguments, +following the same rules of lua_pushfstring. +It also adds at the beginning of the message the file name and +the line number where the error occurred, +if this information is available. + + +

+This function never returns, +but it is an idiom to use it in C functions +as return luaL_error(args). + + + + + +


luaL_execresult

+[-0, +3, m] +

int luaL_execresult (lua_State *L, int stat);
+ +

+This function produces the return values for +process-related functions in the standard library +(os.execute and io.close). + + + + + +


luaL_fileresult

+[-0, +(1|3), m] +

int luaL_fileresult (lua_State *L, int stat, const char *fname);
+ +

+This function produces the return values for +file-related functions in the standard library +(io.open, os.rename, file:seek, etc.). + + + + + +


luaL_getmetafield

+[-0, +(0|1), m] +

int luaL_getmetafield (lua_State *L, int obj, const char *e);
+ +

+Pushes onto the stack the field e from the metatable +of the object at index obj and returns the type of pushed value. +If the object does not have a metatable, +or if the metatable does not have this field, +pushes nothing and returns LUA_TNIL. + + + + + +


luaL_getmetatable

+[-0, +1, m] +

int luaL_getmetatable (lua_State *L, const char *tname);
+ +

+Pushes onto the stack the metatable associated with name tname +in the registry (see luaL_newmetatable) +(nil if there is no metatable associated with that name). +Returns the type of the pushed value. + + + + + +


luaL_getsubtable

+[-0, +1, e] +

int luaL_getsubtable (lua_State *L, int idx, const char *fname);
+ +

+Ensures that the value t[fname], +where t is the value at index idx, +is a table, +and pushes that table onto the stack. +Returns true if it finds a previous table there +and false if it creates a new table. + + + + + +


luaL_gsub

+[-0, +1, m] +

const char *luaL_gsub (lua_State *L,
+                       const char *s,
+                       const char *p,
+                       const char *r);
+ +

+Creates a copy of string s by replacing +any occurrence of the string p +with the string r. +Pushes the resulting string on the stack and returns it. + + + + + +


luaL_len

+[-0, +0, e] +

lua_Integer luaL_len (lua_State *L, int index);
+ +

+Returns the "length" of the value at the given index +as a number; +it is equivalent to the '#' operator in Lua (see §3.4.7). +Raises an error if the result of the operation is not an integer. +(This case only can happen through metamethods.) + + + + + +


luaL_loadbuffer

+[-0, +1, –] +

int luaL_loadbuffer (lua_State *L,
+                     const char *buff,
+                     size_t sz,
+                     const char *name);
+ +

+Equivalent to luaL_loadbufferx with mode equal to NULL. + + + + + +


luaL_loadbufferx

+[-0, +1, –] +

int luaL_loadbufferx (lua_State *L,
+                      const char *buff,
+                      size_t sz,
+                      const char *name,
+                      const char *mode);
+ +

+Loads a buffer as a Lua chunk. +This function uses lua_load to load the chunk in the +buffer pointed to by buff with size sz. + + +

+This function returns the same results as lua_load. +name is the chunk name, +used for debug information and error messages. +The string mode works as in function lua_load. + + + + + +


luaL_loadfile

+[-0, +1, m] +

int luaL_loadfile (lua_State *L, const char *filename);
+ +

+Equivalent to luaL_loadfilex with mode equal to NULL. + + + + + +


luaL_loadfilex

+[-0, +1, m] +

int luaL_loadfilex (lua_State *L, const char *filename,
+                                            const char *mode);
+ +

+Loads a file as a Lua chunk. +This function uses lua_load to load the chunk in the file +named filename. +If filename is NULL, +then it loads from the standard input. +The first line in the file is ignored if it starts with a #. + + +

+The string mode works as in function lua_load. + + +

+This function returns the same results as lua_load, +but it has an extra error code LUA_ERRFILE +for file-related errors +(e.g., it cannot open or read the file). + + +

+As lua_load, this function only loads the chunk; +it does not run it. + + + + + +


luaL_loadstring

+[-0, +1, –] +

int luaL_loadstring (lua_State *L, const char *s);
+ +

+Loads a string as a Lua chunk. +This function uses lua_load to load the chunk in +the zero-terminated string s. + + +

+This function returns the same results as lua_load. + + +

+Also as lua_load, this function only loads the chunk; +it does not run it. + + + + + +


luaL_newlib

+[-0, +1, m] +

void luaL_newlib (lua_State *L, const luaL_Reg l[]);
+ +

+Creates a new table and registers there +the functions in list l. + + +

+It is implemented as the following macro: + +

+     (luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
+

+The array l must be the actual array, +not a pointer to it. + + + + + +


luaL_newlibtable

+[-0, +1, m] +

void luaL_newlibtable (lua_State *L, const luaL_Reg l[]);
+ +

+Creates a new table with a size optimized +to store all entries in the array l +(but does not actually store them). +It is intended to be used in conjunction with luaL_setfuncs +(see luaL_newlib). + + +

+It is implemented as a macro. +The array l must be the actual array, +not a pointer to it. + + + + + +


luaL_newmetatable

+[-0, +1, m] +

int luaL_newmetatable (lua_State *L, const char *tname);
+ +

+If the registry already has the key tname, +returns 0. +Otherwise, +creates a new table to be used as a metatable for userdata, +adds to this new table the pair __name = tname, +adds to the registry the pair [tname] = new table, +and returns 1. +(The entry __name is used by some error-reporting functions.) + + +

+In both cases pushes onto the stack the final value associated +with tname in the registry. + + + + + +


luaL_newstate

+[-0, +0, –] +

lua_State *luaL_newstate (void);
+ +

+Creates a new Lua state. +It calls lua_newstate with an +allocator based on the standard C realloc function +and then sets a panic function (see §4.6) that prints +an error message to the standard error output in case of fatal +errors. + + +

+Returns the new state, +or NULL if there is a memory allocation error. + + + + + +


luaL_openlibs

+[-0, +0, e] +

void luaL_openlibs (lua_State *L);
+ +

+Opens all standard Lua libraries into the given state. + + + + + +


luaL_opt

+[-0, +0, e] +

T luaL_opt (L, func, arg, dflt);
+ +

+This macro is defined as follows: + +

+     (lua_isnoneornil(L,(arg)) ? (dflt) : func(L,(arg)))
+

+In words, if the argument arg is nil or absent, +the macro results in the default dflt. +Otherwise, it results in the result of calling func +with the state L and the argument index arg as +parameters. +Note that it evaluates the expression dflt only if needed. + + + + + +


luaL_optinteger

+[-0, +0, v] +

lua_Integer luaL_optinteger (lua_State *L,
+                             int arg,
+                             lua_Integer d);
+ +

+If the function argument arg is an integer +(or convertible to an integer), +returns this integer. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_optlstring

+[-0, +0, v] +

const char *luaL_optlstring (lua_State *L,
+                             int arg,
+                             const char *d,
+                             size_t *l);
+ +

+If the function argument arg is a string, +returns this string. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + +

+If l is not NULL, +fills the position *l with the result's length. +If the result is NULL +(only possible when returning d and d == NULL), +its length is considered zero. + + +

+This function uses lua_tolstring to get its result, +so all conversions and caveats of that function apply here. + + + + + +


luaL_optnumber

+[-0, +0, v] +

lua_Number luaL_optnumber (lua_State *L, int arg, lua_Number d);
+ +

+If the function argument arg is a number, +returns this number. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_optstring

+[-0, +0, v] +

const char *luaL_optstring (lua_State *L,
+                            int arg,
+                            const char *d);
+ +

+If the function argument arg is a string, +returns this string. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_prepbuffer

+[-?, +?, m] +

char *luaL_prepbuffer (luaL_Buffer *B);
+ +

+Equivalent to luaL_prepbuffsize +with the predefined size LUAL_BUFFERSIZE. + + + + + +


luaL_prepbuffsize

+[-?, +?, m] +

char *luaL_prepbuffsize (luaL_Buffer *B, size_t sz);
+ +

+Returns an address to a space of size sz +where you can copy a string to be added to buffer B +(see luaL_Buffer). +After copying the string into this space you must call +luaL_addsize with the size of the string to actually add +it to the buffer. + + + + + +


luaL_pushresult

+[-?, +1, m] +

void luaL_pushresult (luaL_Buffer *B);
+ +

+Finishes the use of buffer B leaving the final string on +the top of the stack. + + + + + +


luaL_pushresultsize

+[-?, +1, m] +

void luaL_pushresultsize (luaL_Buffer *B, size_t sz);
+ +

+Equivalent to the sequence luaL_addsize, luaL_pushresult. + + + + + +


luaL_ref

+[-1, +0, m] +

int luaL_ref (lua_State *L, int t);
+ +

+Creates and returns a reference, +in the table at index t, +for the object at the top of the stack (and pops the object). + + +

+A reference is a unique integer key. +As long as you do not manually add integer keys into table t, +luaL_ref ensures the uniqueness of the key it returns. +You can retrieve an object referred by reference r +by calling lua_rawgeti(L, t, r). +Function luaL_unref frees a reference and its associated object. + + +

+If the object at the top of the stack is nil, +luaL_ref returns the constant LUA_REFNIL. +The constant LUA_NOREF is guaranteed to be different +from any reference returned by luaL_ref. + + + + + +


luaL_Reg

+
typedef struct luaL_Reg {
+  const char *name;
+  lua_CFunction func;
+} luaL_Reg;
+ +

+Type for arrays of functions to be registered by +luaL_setfuncs. +name is the function name and func is a pointer to +the function. +Any array of luaL_Reg must end with a sentinel entry +in which both name and func are NULL. + + + + + +


luaL_requiref

+[-0, +1, e] +

void luaL_requiref (lua_State *L, const char *modname,
+                    lua_CFunction openf, int glb);
+ +

+If modname is not already present in package.loaded, +calls function openf with string modname as an argument +and sets the call result in package.loaded[modname], +as if that function has been called through require. + + +

+If glb is true, +also stores the module into global modname. + + +

+Leaves a copy of the module on the stack. + + + + + +


luaL_setfuncs

+[-nup, +0, m] +

void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup);
+ +

+Registers all functions in the array l +(see luaL_Reg) into the table on the top of the stack +(below optional upvalues, see next). + + +

+When nup is not zero, +all functions are created sharing nup upvalues, +which must be previously pushed on the stack +on top of the library table. +These values are popped from the stack after the registration. + + + + + +


luaL_setmetatable

+[-0, +0, –] +

void luaL_setmetatable (lua_State *L, const char *tname);
+ +

+Sets the metatable of the object at the top of the stack +as the metatable associated with name tname +in the registry (see luaL_newmetatable). + + + + + +


luaL_Stream

+
typedef struct luaL_Stream {
+  FILE *f;
+  lua_CFunction closef;
+} luaL_Stream;
+ +

+The standard representation for file handles, +which is used by the standard I/O library. + + +

+A file handle is implemented as a full userdata, +with a metatable called LUA_FILEHANDLE +(where LUA_FILEHANDLE is a macro with the actual metatable's name). +The metatable is created by the I/O library +(see luaL_newmetatable). + + +

+This userdata must start with the structure luaL_Stream; +it can contain other data after this initial structure. +Field f points to the corresponding C stream +(or it can be NULL to indicate an incompletely created handle). +Field closef points to a Lua function +that will be called to close the stream +when the handle is closed or collected; +this function receives the file handle as its sole argument and +must return either true (in case of success) +or nil plus an error message (in case of error). +Once Lua calls this field, +it changes the field value to NULL +to signal that the handle is closed. + + + + + +


luaL_testudata

+[-0, +0, m] +

void *luaL_testudata (lua_State *L, int arg, const char *tname);
+ +

+This function works like luaL_checkudata, +except that, when the test fails, +it returns NULL instead of raising an error. + + + + + +


luaL_tolstring

+[-0, +1, e] +

const char *luaL_tolstring (lua_State *L, int idx, size_t *len);
+ +

+Converts any Lua value at the given index to a C string +in a reasonable format. +The resulting string is pushed onto the stack and also +returned by the function. +If len is not NULL, +the function also sets *len with the string length. + + +

+If the value has a metatable with a __tostring field, +then luaL_tolstring calls the corresponding metamethod +with the value as argument, +and uses the result of the call as its result. + + + + + +


luaL_traceback

+[-0, +1, m] +

void luaL_traceback (lua_State *L, lua_State *L1, const char *msg,
+                     int level);
+ +

+Creates and pushes a traceback of the stack L1. +If msg is not NULL it is appended +at the beginning of the traceback. +The level parameter tells at which level +to start the traceback. + + + + + +


luaL_typename

+[-0, +0, –] +

const char *luaL_typename (lua_State *L, int index);
+ +

+Returns the name of the type of the value at the given index. + + + + + +


luaL_unref

+[-0, +0, –] +

void luaL_unref (lua_State *L, int t, int ref);
+ +

+Releases reference ref from the table at index t +(see luaL_ref). +The entry is removed from the table, +so that the referred object can be collected. +The reference ref is also freed to be used again. + + +

+If ref is LUA_NOREF or LUA_REFNIL, +luaL_unref does nothing. + + + + + +


luaL_where

+[-0, +1, m] +

void luaL_where (lua_State *L, int lvl);
+ +

+Pushes onto the stack a string identifying the current position +of the control at level lvl in the call stack. +Typically this string has the following format: + +

+     chunkname:currentline:
+

+Level 0 is the running function, +level 1 is the function that called the running function, +etc. + + +

+This function is used to build a prefix for error messages. + + + + + + + +

6 – Standard Libraries

+ +

+The standard Lua libraries provide useful functions +that are implemented directly through the C API. +Some of these functions provide essential services to the language +(e.g., type and getmetatable); +others provide access to "outside" services (e.g., I/O); +and others could be implemented in Lua itself, +but are quite useful or have critical performance requirements that +deserve an implementation in C (e.g., table.sort). + + +

+All libraries are implemented through the official C API +and are provided as separate C modules. +Currently, Lua has the following standard libraries: + +

    + +
  • basic library (§6.1);
  • + +
  • coroutine library (§6.2);
  • + +
  • package library (§6.3);
  • + +
  • string manipulation (§6.4);
  • + +
  • basic UTF-8 support (§6.5);
  • + +
  • table manipulation (§6.6);
  • + +
  • mathematical functions (§6.7) (sin, log, etc.);
  • + +
  • input and output (§6.8);
  • + +
  • operating system facilities (§6.9);
  • + +
  • debug facilities (§6.10).
  • + +

+Except for the basic and the package libraries, +each library provides all its functions as fields of a global table +or as methods of its objects. + + +

+To have access to these libraries, +the C host program should call the luaL_openlibs function, +which opens all standard libraries. +Alternatively, +the host program can open them individually by using +luaL_requiref to call +luaopen_base (for the basic library), +luaopen_package (for the package library), +luaopen_coroutine (for the coroutine library), +luaopen_string (for the string library), +luaopen_utf8 (for the UTF8 library), +luaopen_table (for the table library), +luaopen_math (for the mathematical library), +luaopen_io (for the I/O library), +luaopen_os (for the operating system library), +and luaopen_debug (for the debug library). +These functions are declared in lualib.h. + + + +

6.1 – Basic Functions

+ +

+The basic library provides core functions to Lua. +If you do not include this library in your application, +you should check carefully whether you need to provide +implementations for some of its facilities. + + +

+


assert (v [, message])

+ + +

+Calls error if +the value of its argument v is false (i.e., nil or false); +otherwise, returns all its arguments. +In case of error, +message is the error object; +when absent, it defaults to "assertion failed!" + + + + +

+


collectgarbage ([opt [, arg]])

+ + +

+This function is a generic interface to the garbage collector. +It performs different functions according to its first argument, opt: + +

    + +
  • "collect": +performs a full garbage-collection cycle. +This is the default option. +
  • + +
  • "stop": +stops automatic execution of the garbage collector. +The collector will run only when explicitly invoked, +until a call to restart it. +
  • + +
  • "restart": +restarts automatic execution of the garbage collector. +
  • + +
  • "count": +returns the total memory in use by Lua in Kbytes. +The value has a fractional part, +so that it multiplied by 1024 +gives the exact number of bytes in use by Lua +(except for overflows). +
  • + +
  • "step": +performs a garbage-collection step. +The step "size" is controlled by arg. +With a zero value, +the collector will perform one basic (indivisible) step. +For non-zero values, +the collector will perform as if that amount of memory +(in KBytes) had been allocated by Lua. +Returns true if the step finished a collection cycle. +
  • + +
  • "setpause": +sets arg as the new value for the pause of +the collector (see §2.5). +Returns the previous value for pause. +
  • + +
  • "setstepmul": +sets arg as the new value for the step multiplier of +the collector (see §2.5). +Returns the previous value for step. +
  • + +
  • "isrunning": +returns a boolean that tells whether the collector is running +(i.e., not stopped). +
  • + +
+ + + +

+


dofile ([filename])

+Opens the named file and executes its contents as a Lua chunk. +When called without arguments, +dofile executes the contents of the standard input (stdin). +Returns all values returned by the chunk. +In case of errors, dofile propagates the error +to its caller (that is, dofile does not run in protected mode). + + + + +

+


error (message [, level])

+Terminates the last protected function called +and returns message as the error object. +Function error never returns. + + +

+Usually, error adds some information about the error position +at the beginning of the message, if the message is a string. +The level argument specifies how to get the error position. +With level 1 (the default), the error position is where the +error function was called. +Level 2 points the error to where the function +that called error was called; and so on. +Passing a level 0 avoids the addition of error position information +to the message. + + + + +

+


_G

+A global variable (not a function) that +holds the global environment (see §2.2). +Lua itself does not use this variable; +changing its value does not affect any environment, +nor vice versa. + + + + +

+


getmetatable (object)

+ + +

+If object does not have a metatable, returns nil. +Otherwise, +if the object's metatable has a __metatable field, +returns the associated value. +Otherwise, returns the metatable of the given object. + + + + +

+


ipairs (t)

+ + +

+Returns three values (an iterator function, the table t, and 0) +so that the construction + +

+     for i,v in ipairs(t) do body end
+

+will iterate over the key–value pairs +(1,t[1]), (2,t[2]), ..., +up to the first nil value. + + + + +

+


load (chunk [, chunkname [, mode [, env]]])

+ + +

+Loads a chunk. + + +

+If chunk is a string, the chunk is this string. +If chunk is a function, +load calls it repeatedly to get the chunk pieces. +Each call to chunk must return a string that concatenates +with previous results. +A return of an empty string, nil, or no value signals the end of the chunk. + + +

+If there are no syntactic errors, +returns the compiled chunk as a function; +otherwise, returns nil plus the error message. + + +

+If the resulting function has upvalues, +the first upvalue is set to the value of env, +if that parameter is given, +or to the value of the global environment. +Other upvalues are initialized with nil. +(When you load a main chunk, +the resulting function will always have exactly one upvalue, +the _ENV variable (see §2.2). +However, +when you load a binary chunk created from a function (see string.dump), +the resulting function can have an arbitrary number of upvalues.) +All upvalues are fresh, that is, +they are not shared with any other function. + + +

+chunkname is used as the name of the chunk for error messages +and debug information (see §4.9). +When absent, +it defaults to chunk, if chunk is a string, +or to "=(load)" otherwise. + + +

+The string mode controls whether the chunk can be text or binary +(that is, a precompiled chunk). +It may be the string "b" (only binary chunks), +"t" (only text chunks), +or "bt" (both binary and text). +The default is "bt". + + +

+Lua does not check the consistency of binary chunks. +Maliciously crafted binary chunks can crash +the interpreter. + + + + +

+


loadfile ([filename [, mode [, env]]])

+ + +

+Similar to load, +but gets the chunk from file filename +or from the standard input, +if no file name is given. + + + + +

+


next (table [, index])

+ + +

+Allows a program to traverse all fields of a table. +Its first argument is a table and its second argument +is an index in this table. +next returns the next index of the table +and its associated value. +When called with nil as its second argument, +next returns an initial index +and its associated value. +When called with the last index, +or with nil in an empty table, +next returns nil. +If the second argument is absent, then it is interpreted as nil. +In particular, +you can use next(t) to check whether a table is empty. + + +

+The order in which the indices are enumerated is not specified, +even for numeric indices. +(To traverse a table in numerical order, +use a numerical for.) + + +

+The behavior of next is undefined if, +during the traversal, +you assign any value to a non-existent field in the table. +You may however modify existing fields. +In particular, you may clear existing fields. + + + + +

+


pairs (t)

+ + +

+If t has a metamethod __pairs, +calls it with t as argument and returns the first three +results from the call. + + +

+Otherwise, +returns three values: the next function, the table t, and nil, +so that the construction + +

+     for k,v in pairs(t) do body end
+

+will iterate over all key–value pairs of table t. + + +

+See function next for the caveats of modifying +the table during its traversal. + + + + +

+


pcall (f [, arg1, ···])

+ + +

+Calls function f with +the given arguments in protected mode. +This means that any error inside f is not propagated; +instead, pcall catches the error +and returns a status code. +Its first result is the status code (a boolean), +which is true if the call succeeds without errors. +In such case, pcall also returns all results from the call, +after this first result. +In case of any error, pcall returns false plus the error message. + + + + +

+


print (···)

+Receives any number of arguments +and prints their values to stdout, +using the tostring function to convert each argument to a string. +print is not intended for formatted output, +but only as a quick way to show a value, +for instance for debugging. +For complete control over the output, +use string.format and io.write. + + + + +

+


rawequal (v1, v2)

+Checks whether v1 is equal to v2, +without invoking the __eq metamethod. +Returns a boolean. + + + + +

+


rawget (table, index)

+Gets the real value of table[index], +without invoking the __index metamethod. +table must be a table; +index may be any value. + + + + +

+


rawlen (v)

+Returns the length of the object v, +which must be a table or a string, +without invoking the __len metamethod. +Returns an integer. + + + + +

+


rawset (table, index, value)

+Sets the real value of table[index] to value, +without invoking the __newindex metamethod. +table must be a table, +index any value different from nil and NaN, +and value any Lua value. + + +

+This function returns table. + + + + +

+


select (index, ···)

+ + +

+If index is a number, +returns all arguments after argument number index; +a negative number indexes from the end (-1 is the last argument). +Otherwise, index must be the string "#", +and select returns the total number of extra arguments it received. + + + + +

+


setmetatable (table, metatable)

+ + +

+Sets the metatable for the given table. +(To change the metatable of other types from Lua code, +you must use the debug library (§6.10).) +If metatable is nil, +removes the metatable of the given table. +If the original metatable has a __metatable field, +raises an error. + + +

+This function returns table. + + + + +

+


tonumber (e [, base])

+ + +

+When called with no base, +tonumber tries to convert its argument to a number. +If the argument is already a number or +a string convertible to a number, +then tonumber returns this number; +otherwise, it returns nil. + + +

+The conversion of strings can result in integers or floats, +according to the lexical conventions of Lua (see §3.1). +(The string may have leading and trailing spaces and a sign.) + + +

+When called with base, +then e must be a string to be interpreted as +an integer numeral in that base. +The base may be any integer between 2 and 36, inclusive. +In bases above 10, the letter 'A' (in either upper or lower case) +represents 10, 'B' represents 11, and so forth, +with 'Z' representing 35. +If the string e is not a valid numeral in the given base, +the function returns nil. + + + + +

+


tostring (v)

+Receives a value of any type and +converts it to a string in a human-readable format. +(For complete control of how numbers are converted, +use string.format.) + + +

+If the metatable of v has a __tostring field, +then tostring calls the corresponding value +with v as argument, +and uses the result of the call as its result. + + + + +

+


type (v)

+Returns the type of its only argument, coded as a string. +The possible results of this function are +"nil" (a string, not the value nil), +"number", +"string", +"boolean", +"table", +"function", +"thread", +and "userdata". + + + + +

+


_VERSION

+ + +

+A global variable (not a function) that +holds a string containing the running Lua version. +The current value of this variable is "Lua 5.3". + + + + +

+


xpcall (f, msgh [, arg1, ···])

+ + +

+This function is similar to pcall, +except that it sets a new message handler msgh. + + + + + + + +

6.2 – Coroutine Manipulation

+ +

+This library comprises the operations to manipulate coroutines, +which come inside the table coroutine. +See §2.6 for a general description of coroutines. + + +

+


coroutine.create (f)

+ + +

+Creates a new coroutine, with body f. +f must be a function. +Returns this new coroutine, +an object with type "thread". + + + + +

+


coroutine.isyieldable ()

+ + +

+Returns true when the running coroutine can yield. + + +

+A running coroutine is yieldable if it is not the main thread and +it is not inside a non-yieldable C function. + + + + +

+


coroutine.resume (co [, val1, ···])

+ + +

+Starts or continues the execution of coroutine co. +The first time you resume a coroutine, +it starts running its body. +The values val1, ... are passed +as the arguments to the body function. +If the coroutine has yielded, +resume restarts it; +the values val1, ... are passed +as the results from the yield. + + +

+If the coroutine runs without any errors, +resume returns true plus any values passed to yield +(when the coroutine yields) or any values returned by the body function +(when the coroutine terminates). +If there is any error, +resume returns false plus the error message. + + + + +

+


coroutine.running ()

+ + +

+Returns the running coroutine plus a boolean, +true when the running coroutine is the main one. + + + + +

+


coroutine.status (co)

+ + +

+Returns the status of coroutine co, as a string: +"running", +if the coroutine is running (that is, it called status); +"suspended", if the coroutine is suspended in a call to yield, +or if it has not started running yet; +"normal" if the coroutine is active but not running +(that is, it has resumed another coroutine); +and "dead" if the coroutine has finished its body function, +or if it has stopped with an error. + + + + +

+


coroutine.wrap (f)

+ + +

+Creates a new coroutine, with body f. +f must be a function. +Returns a function that resumes the coroutine each time it is called. +Any arguments passed to the function behave as the +extra arguments to resume. +Returns the same values returned by resume, +except the first boolean. +In case of error, propagates the error. + + + + +

+


coroutine.yield (···)

+ + +

+Suspends the execution of the calling coroutine. +Any arguments to yield are passed as extra results to resume. + + + + + + + +

6.3 – Modules

+ +

+The package library provides basic +facilities for loading modules in Lua. +It exports one function directly in the global environment: +require. +Everything else is exported in a table package. + + +

+


require (modname)

+ + +

+Loads the given module. +The function starts by looking into the package.loaded table +to determine whether modname is already loaded. +If it is, then require returns the value stored +at package.loaded[modname]. +Otherwise, it tries to find a loader for the module. + + +

+To find a loader, +require is guided by the package.searchers sequence. +By changing this sequence, +we can change how require looks for a module. +The following explanation is based on the default configuration +for package.searchers. + + +

+First require queries package.preload[modname]. +If it has a value, +this value (which must be a function) is the loader. +Otherwise require searches for a Lua loader using the +path stored in package.path. +If that also fails, it searches for a C loader using the +path stored in package.cpath. +If that also fails, +it tries an all-in-one loader (see package.searchers). + + +

+Once a loader is found, +require calls the loader with two arguments: +modname and an extra value dependent on how it got the loader. +(If the loader came from a file, +this extra value is the file name.) +If the loader returns any non-nil value, +require assigns the returned value to package.loaded[modname]. +If the loader does not return a non-nil value and +has not assigned any value to package.loaded[modname], +then require assigns true to this entry. +In any case, require returns the +final value of package.loaded[modname]. + + +

+If there is any error loading or running the module, +or if it cannot find any loader for the module, +then require raises an error. + + + + +

+


package.config

+ + +

+A string describing some compile-time configurations for packages. +This string is a sequence of lines: + +

    + +
  • The first line is the directory separator string. +Default is '\' for Windows and '/' for all other systems.
  • + +
  • The second line is the character that separates templates in a path. +Default is ';'.
  • + +
  • The third line is the string that marks the +substitution points in a template. +Default is '?'.
  • + +
  • The fourth line is a string that, in a path in Windows, +is replaced by the executable's directory. +Default is '!'.
  • + +
  • The fifth line is a mark to ignore all text after it +when building the luaopen_ function name. +Default is '-'.
  • + +
+ + + +

+


package.cpath

+ + +

+The path used by require to search for a C loader. + + +

+Lua initializes the C path package.cpath in the same way +it initializes the Lua path package.path, +using the environment variable LUA_CPATH_5_3, +or the environment variable LUA_CPATH, +or a default path defined in luaconf.h. + + + + +

+


package.loaded

+ + +

+A table used by require to control which +modules are already loaded. +When you require a module modname and +package.loaded[modname] is not false, +require simply returns the value stored there. + + +

+This variable is only a reference to the real table; +assignments to this variable do not change the +table used by require. + + + + +

+


package.loadlib (libname, funcname)

+ + +

+Dynamically links the host program with the C library libname. + + +

+If funcname is "*", +then it only links with the library, +making the symbols exported by the library +available to other dynamically linked libraries. +Otherwise, +it looks for a function funcname inside the library +and returns this function as a C function. +So, funcname must follow the lua_CFunction prototype +(see lua_CFunction). + + +

+This is a low-level function. +It completely bypasses the package and module system. +Unlike require, +it does not perform any path searching and +does not automatically adds extensions. +libname must be the complete file name of the C library, +including if necessary a path and an extension. +funcname must be the exact name exported by the C library +(which may depend on the C compiler and linker used). + + +

+This function is not supported by Standard C. +As such, it is only available on some platforms +(Windows, Linux, Mac OS X, Solaris, BSD, +plus other Unix systems that support the dlfcn standard). + + + + +

+


package.path

+ + +

+The path used by require to search for a Lua loader. + + +

+At start-up, Lua initializes this variable with +the value of the environment variable LUA_PATH_5_3 or +the environment variable LUA_PATH or +with a default path defined in luaconf.h, +if those environment variables are not defined. +Any ";;" in the value of the environment variable +is replaced by the default path. + + + + +

+


package.preload

+ + +

+A table to store loaders for specific modules +(see require). + + +

+This variable is only a reference to the real table; +assignments to this variable do not change the +table used by require. + + + + +

+


package.searchers

+ + +

+A table used by require to control how to load modules. + + +

+Each entry in this table is a searcher function. +When looking for a module, +require calls each of these searchers in ascending order, +with the module name (the argument given to require) as its +sole parameter. +The function can return another function (the module loader) +plus an extra value that will be passed to that loader, +or a string explaining why it did not find that module +(or nil if it has nothing to say). + + +

+Lua initializes this table with four searcher functions. + + +

+The first searcher simply looks for a loader in the +package.preload table. + + +

+The second searcher looks for a loader as a Lua library, +using the path stored at package.path. +The search is done as described in function package.searchpath. + + +

+The third searcher looks for a loader as a C library, +using the path given by the variable package.cpath. +Again, +the search is done as described in function package.searchpath. +For instance, +if the C path is the string + +

+     "./?.so;./?.dll;/usr/local/?/init.so"
+

+the searcher for module foo +will try to open the files ./foo.so, ./foo.dll, +and /usr/local/foo/init.so, in that order. +Once it finds a C library, +this searcher first uses a dynamic link facility to link the +application with the library. +Then it tries to find a C function inside the library to +be used as the loader. +The name of this C function is the string "luaopen_" +concatenated with a copy of the module name where each dot +is replaced by an underscore. +Moreover, if the module name has a hyphen, +its suffix after (and including) the first hyphen is removed. +For instance, if the module name is a.b.c-v2.1, +the function name will be luaopen_a_b_c. + + +

+The fourth searcher tries an all-in-one loader. +It searches the C path for a library for +the root name of the given module. +For instance, when requiring a.b.c, +it will search for a C library for a. +If found, it looks into it for an open function for +the submodule; +in our example, that would be luaopen_a_b_c. +With this facility, a package can pack several C submodules +into one single library, +with each submodule keeping its original open function. + + +

+All searchers except the first one (preload) return as the extra value +the file name where the module was found, +as returned by package.searchpath. +The first searcher returns no extra value. + + + + +

+


package.searchpath (name, path [, sep [, rep]])

+ + +

+Searches for the given name in the given path. + + +

+A path is a string containing a sequence of +templates separated by semicolons. +For each template, +the function replaces each interrogation mark (if any) +in the template with a copy of name +wherein all occurrences of sep +(a dot, by default) +were replaced by rep +(the system's directory separator, by default), +and then tries to open the resulting file name. + + +

+For instance, if the path is the string + +

+     "./?.lua;./?.lc;/usr/local/?/init.lua"
+

+the search for the name foo.a +will try to open the files +./foo/a.lua, ./foo/a.lc, and +/usr/local/foo/a/init.lua, in that order. + + +

+Returns the resulting name of the first file that it can +open in read mode (after closing the file), +or nil plus an error message if none succeeds. +(This error message lists all file names it tried to open.) + + + + + + + +

6.4 – String Manipulation

+ +

+This library provides generic functions for string manipulation, +such as finding and extracting substrings, and pattern matching. +When indexing a string in Lua, the first character is at position 1 +(not at 0, as in C). +Indices are allowed to be negative and are interpreted as indexing backwards, +from the end of the string. +Thus, the last character is at position -1, and so on. + + +

+The string library provides all its functions inside the table +string. +It also sets a metatable for strings +where the __index field points to the string table. +Therefore, you can use the string functions in object-oriented style. +For instance, string.byte(s,i) +can be written as s:byte(i). + + +

+The string library assumes one-byte character encodings. + + +

+


string.byte (s [, i [, j]])

+Returns the internal numeric codes of the characters s[i], +s[i+1], ..., s[j]. +The default value for i is 1; +the default value for j is i. +These indices are corrected +following the same rules of function string.sub. + + +

+Numeric codes are not necessarily portable across platforms. + + + + +

+


string.char (···)

+Receives zero or more integers. +Returns a string with length equal to the number of arguments, +in which each character has the internal numeric code equal +to its corresponding argument. + + +

+Numeric codes are not necessarily portable across platforms. + + + + +

+


string.dump (function [, strip])

+ + +

+Returns a string containing a binary representation +(a binary chunk) +of the given function, +so that a later load on this string returns +a copy of the function (but with new upvalues). +If strip is a true value, +the binary representation may not include all debug information +about the function, +to save space. + + +

+Functions with upvalues have only their number of upvalues saved. +When (re)loaded, +those upvalues receive fresh instances containing nil. +(You can use the debug library to serialize +and reload the upvalues of a function +in a way adequate to your needs.) + + + + +

+


string.find (s, pattern [, init [, plain]])

+ + +

+Looks for the first match of +pattern (see §6.4.1) in the string s. +If it finds a match, then find returns the indices of s +where this occurrence starts and ends; +otherwise, it returns nil. +A third, optional numeric argument init specifies +where to start the search; +its default value is 1 and can be negative. +A value of true as a fourth, optional argument plain +turns off the pattern matching facilities, +so the function does a plain "find substring" operation, +with no characters in pattern being considered magic. +Note that if plain is given, then init must be given as well. + + +

+If the pattern has captures, +then in a successful match +the captured values are also returned, +after the two indices. + + + + +

+


string.format (formatstring, ···)

+ + +

+Returns a formatted version of its variable number of arguments +following the description given in its first argument (which must be a string). +The format string follows the same rules as the ISO C function sprintf. +The only differences are that the options/modifiers +*, h, L, l, n, +and p are not supported +and that there is an extra option, q. + + +

+The q option formats a string between double quotes, +using escape sequences when necessary to ensure that +it can safely be read back by the Lua interpreter. +For instance, the call + +

+     string.format('%q', 'a string with "quotes" and \n new line')
+

+may produce the string: + +

+     "a string with \"quotes\" and \
+      new line"
+
+ +

+Options +A, a, E, e, f, +G, and g all expect a number as argument. +Options c, d, +i, o, u, X, and x +expect an integer. +When Lua is compiled with a C89 compiler, +options A and a (hexadecimal floats) +do not support any modifier (flags, width, length). + + +

+Option s expects a string; +if its argument is not a string, +it is converted to one following the same rules of tostring. +If the option has any modifier (flags, width, length), +the string argument should not contain embedded zeros. + + + + +

+


string.gmatch (s, pattern)

+Returns an iterator function that, +each time it is called, +returns the next captures from pattern (see §6.4.1) +over the string s. +If pattern specifies no captures, +then the whole match is produced in each call. + + +

+As an example, the following loop +will iterate over all the words from string s, +printing one per line: + +

+     s = "hello world from Lua"
+     for w in string.gmatch(s, "%a+") do
+       print(w)
+     end
+

+The next example collects all pairs key=value from the +given string into a table: + +

+     t = {}
+     s = "from=world, to=Lua"
+     for k, v in string.gmatch(s, "(%w+)=(%w+)") do
+       t[k] = v
+     end
+
+ +

+For this function, a caret '^' at the start of a pattern does not +work as an anchor, as this would prevent the iteration. + + + + +

+


string.gsub (s, pattern, repl [, n])

+Returns a copy of s +in which all (or the first n, if given) +occurrences of the pattern (see §6.4.1) have been +replaced by a replacement string specified by repl, +which can be a string, a table, or a function. +gsub also returns, as its second value, +the total number of matches that occurred. +The name gsub comes from Global SUBstitution. + + +

+If repl is a string, then its value is used for replacement. +The character % works as an escape character: +any sequence in repl of the form %d, +with d between 1 and 9, +stands for the value of the d-th captured substring. +The sequence %0 stands for the whole match. +The sequence %% stands for a single %. + + +

+If repl is a table, then the table is queried for every match, +using the first capture as the key. + + +

+If repl is a function, then this function is called every time a +match occurs, with all captured substrings passed as arguments, +in order. + + +

+In any case, +if the pattern specifies no captures, +then it behaves as if the whole pattern was inside a capture. + + +

+If the value returned by the table query or by the function call +is a string or a number, +then it is used as the replacement string; +otherwise, if it is false or nil, +then there is no replacement +(that is, the original match is kept in the string). + + +

+Here are some examples: + +

+     x = string.gsub("hello world", "(%w+)", "%1 %1")
+     --> x="hello hello world world"
+     
+     x = string.gsub("hello world", "%w+", "%0 %0", 1)
+     --> x="hello hello world"
+     
+     x = string.gsub("hello world from Lua", "(%w+)%s*(%w+)", "%2 %1")
+     --> x="world hello Lua from"
+     
+     x = string.gsub("home = $HOME, user = $USER", "%$(%w+)", os.getenv)
+     --> x="home = /home/roberto, user = roberto"
+     
+     x = string.gsub("4+5 = $return 4+5$", "%$(.-)%$", function (s)
+           return load(s)()
+         end)
+     --> x="4+5 = 9"
+     
+     local t = {name="lua", version="5.3"}
+     x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t)
+     --> x="lua-5.3.tar.gz"
+
+ + + +

+


string.len (s)

+Receives a string and returns its length. +The empty string "" has length 0. +Embedded zeros are counted, +so "a\000bc\000" has length 5. + + + + +

+


string.lower (s)

+Receives a string and returns a copy of this string with all +uppercase letters changed to lowercase. +All other characters are left unchanged. +The definition of what an uppercase letter is depends on the current locale. + + + + +

+


string.match (s, pattern [, init])

+Looks for the first match of +pattern (see §6.4.1) in the string s. +If it finds one, then match returns +the captures from the pattern; +otherwise it returns nil. +If pattern specifies no captures, +then the whole match is returned. +A third, optional numeric argument init specifies +where to start the search; +its default value is 1 and can be negative. + + + + +

+


string.pack (fmt, v1, v2, ···)

+ + +

+Returns a binary string containing the values v1, v2, etc. +packed (that is, serialized in binary form) +according to the format string fmt (see §6.4.2). + + + + +

+


string.packsize (fmt)

+ + +

+Returns the size of a string resulting from string.pack +with the given format. +The format string cannot have the variable-length options +'s' or 'z' (see §6.4.2). + + + + +

+


string.rep (s, n [, sep])

+Returns a string that is the concatenation of n copies of +the string s separated by the string sep. +The default value for sep is the empty string +(that is, no separator). +Returns the empty string if n is not positive. + + +

+(Note that it is very easy to exhaust the memory of your machine +with a single call to this function.) + + + + +

+


string.reverse (s)

+Returns a string that is the string s reversed. + + + + +

+


string.sub (s, i [, j])

+Returns the substring of s that +starts at i and continues until j; +i and j can be negative. +If j is absent, then it is assumed to be equal to -1 +(which is the same as the string length). +In particular, +the call string.sub(s,1,j) returns a prefix of s +with length j, +and string.sub(s, -i) (for a positive i) +returns a suffix of s +with length i. + + +

+If, after the translation of negative indices, +i is less than 1, +it is corrected to 1. +If j is greater than the string length, +it is corrected to that length. +If, after these corrections, +i is greater than j, +the function returns the empty string. + + + + +

+


string.unpack (fmt, s [, pos])

+ + +

+Returns the values packed in string s (see string.pack) +according to the format string fmt (see §6.4.2). +An optional pos marks where +to start reading in s (default is 1). +After the read values, +this function also returns the index of the first unread byte in s. + + + + +

+


string.upper (s)

+Receives a string and returns a copy of this string with all +lowercase letters changed to uppercase. +All other characters are left unchanged. +The definition of what a lowercase letter is depends on the current locale. + + + + + +

6.4.1 – Patterns

+ +

+Patterns in Lua are described by regular strings, +which are interpreted as patterns by the pattern-matching functions +string.find, +string.gmatch, +string.gsub, +and string.match. +This section describes the syntax and the meaning +(that is, what they match) of these strings. + + + +

Character Class:

+A character class is used to represent a set of characters. +The following combinations are allowed in describing a character class: + +

    + +
  • x: +(where x is not one of the magic characters +^$()%.[]*+-?) +represents the character x itself. +
  • + +
  • .: (a dot) represents all characters.
  • + +
  • %a: represents all letters.
  • + +
  • %c: represents all control characters.
  • + +
  • %d: represents all digits.
  • + +
  • %g: represents all printable characters except space.
  • + +
  • %l: represents all lowercase letters.
  • + +
  • %p: represents all punctuation characters.
  • + +
  • %s: represents all space characters.
  • + +
  • %u: represents all uppercase letters.
  • + +
  • %w: represents all alphanumeric characters.
  • + +
  • %x: represents all hexadecimal digits.
  • + +
  • %x: (where x is any non-alphanumeric character) +represents the character x. +This is the standard way to escape the magic characters. +Any non-alphanumeric character +(including all punctuation characters, even the non-magical) +can be preceded by a '%' +when used to represent itself in a pattern. +
  • + +
  • [set]: +represents the class which is the union of all +characters in set. +A range of characters can be specified by +separating the end characters of the range, +in ascending order, with a '-'. +All classes %x described above can also be used as +components in set. +All other characters in set represent themselves. +For example, [%w_] (or [_%w]) +represents all alphanumeric characters plus the underscore, +[0-7] represents the octal digits, +and [0-7%l%-] represents the octal digits plus +the lowercase letters plus the '-' character. + + +

    +You can put a closing square bracket in a set +by positioning it as the first character in the set. +You can put an hyphen in a set +by positioning it as the first or the last character in the set. +(You can also use an escape for both cases.) + + +

    +The interaction between ranges and classes is not defined. +Therefore, patterns like [%a-z] or [a-%%] +have no meaning. +

  • + +
  • [^set]: +represents the complement of set, +where set is interpreted as above. +
  • + +

+For all classes represented by single letters (%a, %c, etc.), +the corresponding uppercase letter represents the complement of the class. +For instance, %S represents all non-space characters. + + +

+The definitions of letter, space, and other character groups +depend on the current locale. +In particular, the class [a-z] may not be equivalent to %l. + + + + + +

Pattern Item:

+A pattern item can be + +

    + +
  • +a single character class, +which matches any single character in the class; +
  • + +
  • +a single character class followed by '*', +which matches zero or more repetitions of characters in the class. +These repetition items will always match the longest possible sequence; +
  • + +
  • +a single character class followed by '+', +which matches one or more repetitions of characters in the class. +These repetition items will always match the longest possible sequence; +
  • + +
  • +a single character class followed by '-', +which also matches zero or more repetitions of characters in the class. +Unlike '*', +these repetition items will always match the shortest possible sequence; +
  • + +
  • +a single character class followed by '?', +which matches zero or one occurrence of a character in the class. +It always matches one occurrence if possible; +
  • + +
  • +%n, for n between 1 and 9; +such item matches a substring equal to the n-th captured string +(see below); +
  • + +
  • +%bxy, where x and y are two distinct characters; +such item matches strings that start with x, end with y, +and where the x and y are balanced. +This means that, if one reads the string from left to right, +counting +1 for an x and -1 for a y, +the ending y is the first y where the count reaches 0. +For instance, the item %b() matches expressions with +balanced parentheses. +
  • + +
  • +%f[set], a frontier pattern; +such item matches an empty string at any position such that +the next character belongs to set +and the previous character does not belong to set. +The set set is interpreted as previously described. +The beginning and the end of the subject are handled as if +they were the character '\0'. +
  • + +
+ + + + +

Pattern:

+A pattern is a sequence of pattern items. +A caret '^' at the beginning of a pattern anchors the match at the +beginning of the subject string. +A '$' at the end of a pattern anchors the match at the +end of the subject string. +At other positions, +'^' and '$' have no special meaning and represent themselves. + + + + + +

Captures:

+A pattern can contain sub-patterns enclosed in parentheses; +they describe captures. +When a match succeeds, the substrings of the subject string +that match captures are stored (captured) for future use. +Captures are numbered according to their left parentheses. +For instance, in the pattern "(a*(.)%w(%s*))", +the part of the string matching "a*(.)%w(%s*)" is +stored as the first capture (and therefore has number 1); +the character matching "." is captured with number 2, +and the part matching "%s*" has number 3. + + +

+As a special case, the empty capture () captures +the current string position (a number). +For instance, if we apply the pattern "()aa()" on the +string "flaaap", there will be two captures: 3 and 5. + + + + + + + +

6.4.2 – Format Strings for Pack and Unpack

+ +

+The first argument to string.pack, +string.packsize, and string.unpack +is a format string, +which describes the layout of the structure being created or read. + + +

+A format string is a sequence of conversion options. +The conversion options are as follows: + +

    +
  • <: sets little endian
  • +
  • >: sets big endian
  • +
  • =: sets native endian
  • +
  • ![n]: sets maximum alignment to n +(default is native alignment)
  • +
  • b: a signed byte (char)
  • +
  • B: an unsigned byte (char)
  • +
  • h: a signed short (native size)
  • +
  • H: an unsigned short (native size)
  • +
  • l: a signed long (native size)
  • +
  • L: an unsigned long (native size)
  • +
  • j: a lua_Integer
  • +
  • J: a lua_Unsigned
  • +
  • T: a size_t (native size)
  • +
  • i[n]: a signed int with n bytes +(default is native size)
  • +
  • I[n]: an unsigned int with n bytes +(default is native size)
  • +
  • f: a float (native size)
  • +
  • d: a double (native size)
  • +
  • n: a lua_Number
  • +
  • cn: a fixed-sized string with n bytes
  • +
  • z: a zero-terminated string
  • +
  • s[n]: a string preceded by its length +coded as an unsigned integer with n bytes +(default is a size_t)
  • +
  • x: one byte of padding
  • +
  • Xop: an empty item that aligns +according to option op +(which is otherwise ignored)
  • +
  • ' ': (empty space) ignored
  • +

+(A "[n]" means an optional integral numeral.) +Except for padding, spaces, and configurations +(options "xX <=>!"), +each option corresponds to an argument (in string.pack) +or a result (in string.unpack). + + +

+For options "!n", "sn", "in", and "In", +n can be any integer between 1 and 16. +All integral options check overflows; +string.pack checks whether the given value fits in the given size; +string.unpack checks whether the read value fits in a Lua integer. + + +

+Any format string starts as if prefixed by "!1=", +that is, +with maximum alignment of 1 (no alignment) +and native endianness. + + +

+Alignment works as follows: +For each option, +the format gets extra padding until the data starts +at an offset that is a multiple of the minimum between the +option size and the maximum alignment; +this minimum must be a power of 2. +Options "c" and "z" are not aligned; +option "s" follows the alignment of its starting integer. + + +

+All padding is filled with zeros by string.pack +(and ignored by string.unpack). + + + + + + + +

6.5 – UTF-8 Support

+ +

+This library provides basic support for UTF-8 encoding. +It provides all its functions inside the table utf8. +This library does not provide any support for Unicode other +than the handling of the encoding. +Any operation that needs the meaning of a character, +such as character classification, is outside its scope. + + +

+Unless stated otherwise, +all functions that expect a byte position as a parameter +assume that the given position is either the start of a byte sequence +or one plus the length of the subject string. +As in the string library, +negative indices count from the end of the string. + + +

+


utf8.char (···)

+Receives zero or more integers, +converts each one to its corresponding UTF-8 byte sequence +and returns a string with the concatenation of all these sequences. + + + + +

+


utf8.charpattern

+The pattern (a string, not a function) "[\0-\x7F\xC2-\xF4][\x80-\xBF]*" +(see §6.4.1), +which matches exactly one UTF-8 byte sequence, +assuming that the subject is a valid UTF-8 string. + + + + +

+


utf8.codes (s)

+ + +

+Returns values so that the construction + +

+     for p, c in utf8.codes(s) do body end
+

+will iterate over all characters in string s, +with p being the position (in bytes) and c the code point +of each character. +It raises an error if it meets any invalid byte sequence. + + + + +

+


utf8.codepoint (s [, i [, j]])

+Returns the codepoints (as integers) from all characters in s +that start between byte position i and j (both included). +The default for i is 1 and for j is i. +It raises an error if it meets any invalid byte sequence. + + + + +

+


utf8.len (s [, i [, j]])

+Returns the number of UTF-8 characters in string s +that start between positions i and j (both inclusive). +The default for i is 1 and for j is -1. +If it finds any invalid byte sequence, +returns a false value plus the position of the first invalid byte. + + + + +

+


utf8.offset (s, n [, i])

+Returns the position (in bytes) where the encoding of the +n-th character of s +(counting from position i) starts. +A negative n gets characters before position i. +The default for i is 1 when n is non-negative +and #s + 1 otherwise, +so that utf8.offset(s, -n) gets the offset of the +n-th character from the end of the string. +If the specified character is neither in the subject +nor right after its end, +the function returns nil. + + +

+As a special case, +when n is 0 the function returns the start of the encoding +of the character that contains the i-th byte of s. + + +

+This function assumes that s is a valid UTF-8 string. + + + + + + + +

6.6 – Table Manipulation

+ +

+This library provides generic functions for table manipulation. +It provides all its functions inside the table table. + + +

+Remember that, whenever an operation needs the length of a table, +all caveats about the length operator apply (see §3.4.7). +All functions ignore non-numeric keys +in the tables given as arguments. + + +

+


table.concat (list [, sep [, i [, j]]])

+ + +

+Given a list where all elements are strings or numbers, +returns the string list[i]..sep..list[i+1] ··· sep..list[j]. +The default value for sep is the empty string, +the default for i is 1, +and the default for j is #list. +If i is greater than j, returns the empty string. + + + + +

+


table.insert (list, [pos,] value)

+ + +

+Inserts element value at position pos in list, +shifting up the elements +list[pos], list[pos+1], ···, list[#list]. +The default value for pos is #list+1, +so that a call table.insert(t,x) inserts x at the end +of list t. + + + + +

+


table.move (a1, f, e, t [,a2])

+ + +

+Moves elements from table a1 to table a2, +performing the equivalent to the following +multiple assignment: +a2[t],··· = a1[f],···,a1[e]. +The default for a2 is a1. +The destination range can overlap with the source range. +The number of elements to be moved must fit in a Lua integer. + + +

+Returns the destination table a2. + + + + +

+


table.pack (···)

+ + +

+Returns a new table with all parameters stored into keys 1, 2, etc. +and with a field "n" with the total number of parameters. +Note that the resulting table may not be a sequence. + + + + +

+


table.remove (list [, pos])

+ + +

+Removes from list the element at position pos, +returning the value of the removed element. +When pos is an integer between 1 and #list, +it shifts down the elements +list[pos+1], list[pos+2], ···, list[#list] +and erases element list[#list]; +The index pos can also be 0 when #list is 0, +or #list + 1; +in those cases, the function erases the element list[pos]. + + +

+The default value for pos is #list, +so that a call table.remove(l) removes the last element +of list l. + + + + +

+


table.sort (list [, comp])

+ + +

+Sorts list elements in a given order, in-place, +from list[1] to list[#list]. +If comp is given, +then it must be a function that receives two list elements +and returns true when the first element must come +before the second in the final order +(so that, after the sort, +i < j implies not comp(list[j],list[i])). +If comp is not given, +then the standard Lua operator < is used instead. + + +

+Note that the comp function must define +a strict partial order over the elements in the list; +that is, it must be asymmetric and transitive. +Otherwise, no valid sort may be possible. + + +

+The sort algorithm is not stable: +elements considered equal by the given order +may have their relative positions changed by the sort. + + + + +

+


table.unpack (list [, i [, j]])

+ + +

+Returns the elements from the given list. +This function is equivalent to + +

+     return list[i], list[i+1], ···, list[j]
+

+By default, i is 1 and j is #list. + + + + + + + +

6.7 – Mathematical Functions

+ +

+This library provides basic mathematical functions. +It provides all its functions and constants inside the table math. +Functions with the annotation "integer/float" give +integer results for integer arguments +and float results for float (or mixed) arguments. +Rounding functions +(math.ceil, math.floor, and math.modf) +return an integer when the result fits in the range of an integer, +or a float otherwise. + + +

+


math.abs (x)

+ + +

+Returns the absolute value of x. (integer/float) + + + + +

+


math.acos (x)

+ + +

+Returns the arc cosine of x (in radians). + + + + +

+


math.asin (x)

+ + +

+Returns the arc sine of x (in radians). + + + + +

+


math.atan (y [, x])

+ + +

+ +Returns the arc tangent of y/x (in radians), +but uses the signs of both parameters to find the +quadrant of the result. +(It also handles correctly the case of x being zero.) + + +

+The default value for x is 1, +so that the call math.atan(y) +returns the arc tangent of y. + + + + +

+


math.ceil (x)

+ + +

+Returns the smallest integral value larger than or equal to x. + + + + +

+


math.cos (x)

+ + +

+Returns the cosine of x (assumed to be in radians). + + + + +

+


math.deg (x)

+ + +

+Converts the angle x from radians to degrees. + + + + +

+


math.exp (x)

+ + +

+Returns the value ex +(where e is the base of natural logarithms). + + + + +

+


math.floor (x)

+ + +

+Returns the largest integral value smaller than or equal to x. + + + + +

+


math.fmod (x, y)

+ + +

+Returns the remainder of the division of x by y +that rounds the quotient towards zero. (integer/float) + + + + +

+


math.huge

+ + +

+The float value HUGE_VAL, +a value larger than any other numeric value. + + + + +

+


math.log (x [, base])

+ + +

+Returns the logarithm of x in the given base. +The default for base is e +(so that the function returns the natural logarithm of x). + + + + +

+


math.max (x, ···)

+ + +

+Returns the argument with the maximum value, +according to the Lua operator <. (integer/float) + + + + +

+


math.maxinteger

+An integer with the maximum value for an integer. + + + + +

+


math.min (x, ···)

+ + +

+Returns the argument with the minimum value, +according to the Lua operator <. (integer/float) + + + + +

+


math.mininteger

+An integer with the minimum value for an integer. + + + + +

+


math.modf (x)

+ + +

+Returns the integral part of x and the fractional part of x. +Its second result is always a float. + + + + +

+


math.pi

+ + +

+The value of π. + + + + +

+


math.rad (x)

+ + +

+Converts the angle x from degrees to radians. + + + + +

+


math.random ([m [, n]])

+ + +

+When called without arguments, +returns a pseudo-random float with uniform distribution +in the range [0,1). +When called with two integers m and n, +math.random returns a pseudo-random integer +with uniform distribution in the range [m, n]. +(The value n-m cannot be negative and must fit in a Lua integer.) +The call math.random(n) is equivalent to math.random(1,n). + + +

+This function is an interface to the underling +pseudo-random generator function provided by C. + + + + +

+


math.randomseed (x)

+ + +

+Sets x as the "seed" +for the pseudo-random generator: +equal seeds produce equal sequences of numbers. + + + + +

+


math.sin (x)

+ + +

+Returns the sine of x (assumed to be in radians). + + + + +

+


math.sqrt (x)

+ + +

+Returns the square root of x. +(You can also use the expression x^0.5 to compute this value.) + + + + +

+


math.tan (x)

+ + +

+Returns the tangent of x (assumed to be in radians). + + + + +

+


math.tointeger (x)

+ + +

+If the value x is convertible to an integer, +returns that integer. +Otherwise, returns nil. + + + + +

+


math.type (x)

+ + +

+Returns "integer" if x is an integer, +"float" if it is a float, +or nil if x is not a number. + + + + +

+


math.ult (m, n)

+ + +

+Returns a boolean, +true if and only if integer m is below integer n when +they are compared as unsigned integers. + + + + + + + +

6.8 – Input and Output Facilities

+ +

+The I/O library provides two different styles for file manipulation. +The first one uses implicit file handles; +that is, there are operations to set a default input file and a +default output file, +and all input/output operations are over these default files. +The second style uses explicit file handles. + + +

+When using implicit file handles, +all operations are supplied by table io. +When using explicit file handles, +the operation io.open returns a file handle +and then all operations are supplied as methods of the file handle. + + +

+The table io also provides +three predefined file handles with their usual meanings from C: +io.stdin, io.stdout, and io.stderr. +The I/O library never closes these files. + + +

+Unless otherwise stated, +all I/O functions return nil on failure +(plus an error message as a second result and +a system-dependent error code as a third result) +and some value different from nil on success. +On non-POSIX systems, +the computation of the error message and error code +in case of errors +may be not thread safe, +because they rely on the global C variable errno. + + +

+


io.close ([file])

+ + +

+Equivalent to file:close(). +Without a file, closes the default output file. + + + + +

+


io.flush ()

+ + +

+Equivalent to io.output():flush(). + + + + +

+


io.input ([file])

+ + +

+When called with a file name, it opens the named file (in text mode), +and sets its handle as the default input file. +When called with a file handle, +it simply sets this file handle as the default input file. +When called without parameters, +it returns the current default input file. + + +

+In case of errors this function raises the error, +instead of returning an error code. + + + + +

+


io.lines ([filename, ···])

+ + +

+Opens the given file name in read mode +and returns an iterator function that +works like file:lines(···) over the opened file. +When the iterator function detects the end of file, +it returns no values (to finish the loop) and automatically closes the file. + + +

+The call io.lines() (with no file name) is equivalent +to io.input():lines("*l"); +that is, it iterates over the lines of the default input file. +In this case it does not close the file when the loop ends. + + +

+In case of errors this function raises the error, +instead of returning an error code. + + + + +

+


io.open (filename [, mode])

+ + +

+This function opens a file, +in the mode specified in the string mode. +In case of success, +it returns a new file handle. + + +

+The mode string can be any of the following: + +

    +
  • "r": read mode (the default);
  • +
  • "w": write mode;
  • +
  • "a": append mode;
  • +
  • "r+": update mode, all previous data is preserved;
  • +
  • "w+": update mode, all previous data is erased;
  • +
  • "a+": append update mode, previous data is preserved, + writing is only allowed at the end of file.
  • +

+The mode string can also have a 'b' at the end, +which is needed in some systems to open the file in binary mode. + + + + +

+


io.output ([file])

+ + +

+Similar to io.input, but operates over the default output file. + + + + +

+


io.popen (prog [, mode])

+ + +

+This function is system dependent and is not available +on all platforms. + + +

+Starts program prog in a separated process and returns +a file handle that you can use to read data from this program +(if mode is "r", the default) +or to write data to this program +(if mode is "w"). + + + + +

+


io.read (···)

+ + +

+Equivalent to io.input():read(···). + + + + +

+


io.tmpfile ()

+ + +

+In case of success, +returns a handle for a temporary file. +This file is opened in update mode +and it is automatically removed when the program ends. + + + + +

+


io.type (obj)

+ + +

+Checks whether obj is a valid file handle. +Returns the string "file" if obj is an open file handle, +"closed file" if obj is a closed file handle, +or nil if obj is not a file handle. + + + + +

+


io.write (···)

+ + +

+Equivalent to io.output():write(···). + + + + +

+


file:close ()

+ + +

+Closes file. +Note that files are automatically closed when +their handles are garbage collected, +but that takes an unpredictable amount of time to happen. + + +

+When closing a file handle created with io.popen, +file:close returns the same values +returned by os.execute. + + + + +

+


file:flush ()

+ + +

+Saves any written data to file. + + + + +

+


file:lines (···)

+ + +

+Returns an iterator function that, +each time it is called, +reads the file according to the given formats. +When no format is given, +uses "l" as a default. +As an example, the construction + +

+     for c in file:lines(1) do body end
+

+will iterate over all characters of the file, +starting at the current position. +Unlike io.lines, this function does not close the file +when the loop ends. + + +

+In case of errors this function raises the error, +instead of returning an error code. + + + + +

+


file:read (···)

+ + +

+Reads the file file, +according to the given formats, which specify what to read. +For each format, +the function returns a string or a number with the characters read, +or nil if it cannot read data with the specified format. +(In this latter case, +the function does not read subsequent formats.) +When called without formats, +it uses a default format that reads the next line +(see below). + + +

+The available formats are + +

    + +
  • "n": +reads a numeral and returns it as a float or an integer, +following the lexical conventions of Lua. +(The numeral may have leading spaces and a sign.) +This format always reads the longest input sequence that +is a valid prefix for a numeral; +if that prefix does not form a valid numeral +(e.g., an empty string, "0x", or "3.4e-"), +it is discarded and the function returns nil. +
  • + +
  • "a": +reads the whole file, starting at the current position. +On end of file, it returns the empty string. +
  • + +
  • "l": +reads the next line skipping the end of line, +returning nil on end of file. +This is the default format. +
  • + +
  • "L": +reads the next line keeping the end-of-line character (if present), +returning nil on end of file. +
  • + +
  • number: +reads a string with up to this number of bytes, +returning nil on end of file. +If number is zero, +it reads nothing and returns an empty string, +or nil on end of file. +
  • + +

+The formats "l" and "L" should be used only for text files. + + + + +

+


file:seek ([whence [, offset]])

+ + +

+Sets and gets the file position, +measured from the beginning of the file, +to the position given by offset plus a base +specified by the string whence, as follows: + +

    +
  • "set": base is position 0 (beginning of the file);
  • +
  • "cur": base is current position;
  • +
  • "end": base is end of file;
  • +

+In case of success, seek returns the final file position, +measured in bytes from the beginning of the file. +If seek fails, it returns nil, +plus a string describing the error. + + +

+The default value for whence is "cur", +and for offset is 0. +Therefore, the call file:seek() returns the current +file position, without changing it; +the call file:seek("set") sets the position to the +beginning of the file (and returns 0); +and the call file:seek("end") sets the position to the +end of the file, and returns its size. + + + + +

+


file:setvbuf (mode [, size])

+ + +

+Sets the buffering mode for an output file. +There are three available modes: + +

    + +
  • "no": +no buffering; the result of any output operation appears immediately. +
  • + +
  • "full": +full buffering; output operation is performed only +when the buffer is full or when +you explicitly flush the file (see io.flush). +
  • + +
  • "line": +line buffering; output is buffered until a newline is output +or there is any input from some special files +(such as a terminal device). +
  • + +

+For the last two cases, size +specifies the size of the buffer, in bytes. +The default is an appropriate size. + + + + +

+


file:write (···)

+ + +

+Writes the value of each of its arguments to file. +The arguments must be strings or numbers. + + +

+In case of success, this function returns file. +Otherwise it returns nil plus a string describing the error. + + + + + + + +

6.9 – Operating System Facilities

+ +

+This library is implemented through table os. + + +

+


os.clock ()

+ + +

+Returns an approximation of the amount in seconds of CPU time +used by the program. + + + + +

+


os.date ([format [, time]])

+ + +

+Returns a string or a table containing date and time, +formatted according to the given string format. + + +

+If the time argument is present, +this is the time to be formatted +(see the os.time function for a description of this value). +Otherwise, date formats the current time. + + +

+If format starts with '!', +then the date is formatted in Coordinated Universal Time. +After this optional character, +if format is the string "*t", +then date returns a table with the following fields: +year, month (1–12), day (1–31), +hour (0–23), min (0–59), sec (0–61), +wday (weekday, 1–7, Sunday is 1), +yday (day of the year, 1–366), +and isdst (daylight saving flag, a boolean). +This last field may be absent +if the information is not available. + + +

+If format is not "*t", +then date returns the date as a string, +formatted according to the same rules as the ISO C function strftime. + + +

+When called without arguments, +date returns a reasonable date and time representation that depends on +the host system and on the current locale. +(More specifically, os.date() is equivalent to os.date("%c").) + + +

+On non-POSIX systems, +this function may be not thread safe +because of its reliance on C function gmtime and C function localtime. + + + + +

+


os.difftime (t2, t1)

+ + +

+Returns the difference, in seconds, +from time t1 to time t2 +(where the times are values returned by os.time). +In POSIX, Windows, and some other systems, +this value is exactly t2-t1. + + + + +

+


os.execute ([command])

+ + +

+This function is equivalent to the ISO C function system. +It passes command to be executed by an operating system shell. +Its first result is true +if the command terminated successfully, +or nil otherwise. +After this first result +the function returns a string plus a number, +as follows: + +

    + +
  • "exit": +the command terminated normally; +the following number is the exit status of the command. +
  • + +
  • "signal": +the command was terminated by a signal; +the following number is the signal that terminated the command. +
  • + +
+ +

+When called without a command, +os.execute returns a boolean that is true if a shell is available. + + + + +

+


os.exit ([code [, close]])

+ + +

+Calls the ISO C function exit to terminate the host program. +If code is true, +the returned status is EXIT_SUCCESS; +if code is false, +the returned status is EXIT_FAILURE; +if code is a number, +the returned status is this number. +The default value for code is true. + + +

+If the optional second argument close is true, +closes the Lua state before exiting. + + + + +

+


os.getenv (varname)

+ + +

+Returns the value of the process environment variable varname, +or nil if the variable is not defined. + + + + +

+


os.remove (filename)

+ + +

+Deletes the file (or empty directory, on POSIX systems) +with the given name. +If this function fails, it returns nil, +plus a string describing the error and the error code. +Otherwise, it returns true. + + + + +

+


os.rename (oldname, newname)

+ + +

+Renames the file or directory named oldname to newname. +If this function fails, it returns nil, +plus a string describing the error and the error code. +Otherwise, it returns true. + + + + +

+


os.setlocale (locale [, category])

+ + +

+Sets the current locale of the program. +locale is a system-dependent string specifying a locale; +category is an optional string describing which category to change: +"all", "collate", "ctype", +"monetary", "numeric", or "time"; +the default category is "all". +The function returns the name of the new locale, +or nil if the request cannot be honored. + + +

+If locale is the empty string, +the current locale is set to an implementation-defined native locale. +If locale is the string "C", +the current locale is set to the standard C locale. + + +

+When called with nil as the first argument, +this function only returns the name of the current locale +for the given category. + + +

+This function may be not thread safe +because of its reliance on C function setlocale. + + + + +

+


os.time ([table])

+ + +

+Returns the current time when called without arguments, +or a time representing the local date and time specified by the given table. +This table must have fields year, month, and day, +and may have fields +hour (default is 12), +min (default is 0), +sec (default is 0), +and isdst (default is nil). +Other fields are ignored. +For a description of these fields, see the os.date function. + + +

+The values in these fields do not need to be inside their valid ranges. +For instance, if sec is -10, +it means -10 seconds from the time specified by the other fields; +if hour is 1000, +it means +1000 hours from the time specified by the other fields. + + +

+The returned value is a number, whose meaning depends on your system. +In POSIX, Windows, and some other systems, +this number counts the number +of seconds since some given start time (the "epoch"). +In other systems, the meaning is not specified, +and the number returned by time can be used only as an argument to +os.date and os.difftime. + + + + +

+


os.tmpname ()

+ + +

+Returns a string with a file name that can +be used for a temporary file. +The file must be explicitly opened before its use +and explicitly removed when no longer needed. + + +

+On POSIX systems, +this function also creates a file with that name, +to avoid security risks. +(Someone else might create the file with wrong permissions +in the time between getting the name and creating the file.) +You still have to open the file to use it +and to remove it (even if you do not use it). + + +

+When possible, +you may prefer to use io.tmpfile, +which automatically removes the file when the program ends. + + + + + + + +

6.10 – The Debug Library

+ +

+This library provides +the functionality of the debug interface (§4.9) to Lua programs. +You should exert care when using this library. +Several of its functions +violate basic assumptions about Lua code +(e.g., that variables local to a function +cannot be accessed from outside; +that userdata metatables cannot be changed by Lua code; +that Lua programs do not crash) +and therefore can compromise otherwise secure code. +Moreover, some functions in this library may be slow. + + +

+All functions in this library are provided +inside the debug table. +All functions that operate over a thread +have an optional first argument which is the +thread to operate over. +The default is always the current thread. + + +

+


debug.debug ()

+ + +

+Enters an interactive mode with the user, +running each string that the user enters. +Using simple commands and other debug facilities, +the user can inspect global and local variables, +change their values, evaluate expressions, and so on. +A line containing only the word cont finishes this function, +so that the caller continues its execution. + + +

+Note that commands for debug.debug are not lexically nested +within any function and so have no direct access to local variables. + + + + +

+


debug.gethook ([thread])

+ + +

+Returns the current hook settings of the thread, as three values: +the current hook function, the current hook mask, +and the current hook count +(as set by the debug.sethook function). + + + + +

+


debug.getinfo ([thread,] f [, what])

+ + +

+Returns a table with information about a function. +You can give the function directly +or you can give a number as the value of f, +which means the function running at level f of the call stack +of the given thread: +level 0 is the current function (getinfo itself); +level 1 is the function that called getinfo +(except for tail calls, which do not count on the stack); +and so on. +If f is a number larger than the number of active functions, +then getinfo returns nil. + + +

+The returned table can contain all the fields returned by lua_getinfo, +with the string what describing which fields to fill in. +The default for what is to get all information available, +except the table of valid lines. +If present, +the option 'f' +adds a field named func with the function itself. +If present, +the option 'L' +adds a field named activelines with the table of +valid lines. + + +

+For instance, the expression debug.getinfo(1,"n").name returns +a name for the current function, +if a reasonable name can be found, +and the expression debug.getinfo(print) +returns a table with all available information +about the print function. + + + + +

+


debug.getlocal ([thread,] f, local)

+ + +

+This function returns the name and the value of the local variable +with index local of the function at level f of the stack. +This function accesses not only explicit local variables, +but also parameters, temporaries, etc. + + +

+The first parameter or local variable has index 1, and so on, +following the order that they are declared in the code, +counting only the variables that are active +in the current scope of the function. +Negative indices refer to vararg parameters; +-1 is the first vararg parameter. +The function returns nil if there is no variable with the given index, +and raises an error when called with a level out of range. +(You can call debug.getinfo to check whether the level is valid.) + + +

+Variable names starting with '(' (open parenthesis) +represent variables with no known names +(internal variables such as loop control variables, +and variables from chunks saved without debug information). + + +

+The parameter f may also be a function. +In that case, getlocal returns only the name of function parameters. + + + + +

+


debug.getmetatable (value)

+ + +

+Returns the metatable of the given value +or nil if it does not have a metatable. + + + + +

+


debug.getregistry ()

+ + +

+Returns the registry table (see §4.5). + + + + +

+


debug.getupvalue (f, up)

+ + +

+This function returns the name and the value of the upvalue +with index up of the function f. +The function returns nil if there is no upvalue with the given index. + + +

+Variable names starting with '(' (open parenthesis) +represent variables with no known names +(variables from chunks saved without debug information). + + + + +

+


debug.getuservalue (u)

+ + +

+Returns the Lua value associated to u. +If u is not a full userdata, +returns nil. + + + + +

+


debug.sethook ([thread,] hook, mask [, count])

+ + +

+Sets the given function as a hook. +The string mask and the number count describe +when the hook will be called. +The string mask may have any combination of the following characters, +with the given meaning: + +

    +
  • 'c': the hook is called every time Lua calls a function;
  • +
  • 'r': the hook is called every time Lua returns from a function;
  • +
  • 'l': the hook is called every time Lua enters a new line of code.
  • +

+Moreover, +with a count different from zero, +the hook is called also after every count instructions. + + +

+When called without arguments, +debug.sethook turns off the hook. + + +

+When the hook is called, its first parameter is a string +describing the event that has triggered its call: +"call" (or "tail call"), +"return", +"line", and "count". +For line events, +the hook also gets the new line number as its second parameter. +Inside a hook, +you can call getinfo with level 2 to get more information about +the running function +(level 0 is the getinfo function, +and level 1 is the hook function). + + + + +

+


debug.setlocal ([thread,] level, local, value)

+ + +

+This function assigns the value value to the local variable +with index local of the function at level level of the stack. +The function returns nil if there is no local +variable with the given index, +and raises an error when called with a level out of range. +(You can call getinfo to check whether the level is valid.) +Otherwise, it returns the name of the local variable. + + +

+See debug.getlocal for more information about +variable indices and names. + + + + +

+


debug.setmetatable (value, table)

+ + +

+Sets the metatable for the given value to the given table +(which can be nil). +Returns value. + + + + +

+


debug.setupvalue (f, up, value)

+ + +

+This function assigns the value value to the upvalue +with index up of the function f. +The function returns nil if there is no upvalue +with the given index. +Otherwise, it returns the name of the upvalue. + + + + +

+


debug.setuservalue (udata, value)

+ + +

+Sets the given value as +the Lua value associated to the given udata. +udata must be a full userdata. + + +

+Returns udata. + + + + +

+


debug.traceback ([thread,] [message [, level]])

+ + +

+If message is present but is neither a string nor nil, +this function returns message without further processing. +Otherwise, +it returns a string with a traceback of the call stack. +The optional message string is appended +at the beginning of the traceback. +An optional level number tells at which level +to start the traceback +(default is 1, the function calling traceback). + + + + +

+


debug.upvalueid (f, n)

+ + +

+Returns a unique identifier (as a light userdata) +for the upvalue numbered n +from the given function. + + +

+These unique identifiers allow a program to check whether different +closures share upvalues. +Lua closures that share an upvalue +(that is, that access a same external local variable) +will return identical ids for those upvalue indices. + + + + +

+


debug.upvaluejoin (f1, n1, f2, n2)

+ + +

+Make the n1-th upvalue of the Lua closure f1 +refer to the n2-th upvalue of the Lua closure f2. + + + + + + + +

7 – Lua Standalone

+ +

+Although Lua has been designed as an extension language, +to be embedded in a host C program, +it is also frequently used as a standalone language. +An interpreter for Lua as a standalone language, +called simply lua, +is provided with the standard distribution. +The standalone interpreter includes +all standard libraries, including the debug library. +Its usage is: + +

+     lua [options] [script [args]]
+

+The options are: + +

    +
  • -e stat: executes string stat;
  • +
  • -l mod: "requires" mod;
  • +
  • -i: enters interactive mode after running script;
  • +
  • -v: prints version information;
  • +
  • -E: ignores environment variables;
  • +
  • --: stops handling options;
  • +
  • -: executes stdin as a file and stops handling options.
  • +

+After handling its options, lua runs the given script. +When called without arguments, +lua behaves as lua -v -i +when the standard input (stdin) is a terminal, +and as lua - otherwise. + + +

+When called without option -E, +the interpreter checks for an environment variable LUA_INIT_5_3 +(or LUA_INIT if the versioned name is not defined) +before running any argument. +If the variable content has the format @filename, +then lua executes the file. +Otherwise, lua executes the string itself. + + +

+When called with option -E, +besides ignoring LUA_INIT, +Lua also ignores +the values of LUA_PATH and LUA_CPATH, +setting the values of +package.path and package.cpath +with the default paths defined in luaconf.h. + + +

+All options are handled in order, except -i and -E. +For instance, an invocation like + +

+     $ lua -e'a=1' -e 'print(a)' script.lua
+

+will first set a to 1, then print the value of a, +and finally run the file script.lua with no arguments. +(Here $ is the shell prompt. Your prompt may be different.) + + +

+Before running any code, +lua collects all command-line arguments +in a global table called arg. +The script name goes to index 0, +the first argument after the script name goes to index 1, +and so on. +Any arguments before the script name +(that is, the interpreter name plus its options) +go to negative indices. +For instance, in the call + +

+     $ lua -la b.lua t1 t2
+

+the table is like this: + +

+     arg = { [-2] = "lua", [-1] = "-la",
+             [0] = "b.lua",
+             [1] = "t1", [2] = "t2" }
+

+If there is no script in the call, +the interpreter name goes to index 0, +followed by the other arguments. +For instance, the call + +

+     $ lua -e "print(arg[1])"
+

+will print "-e". +If there is a script, +the script is called with parameters +arg[1], ···, arg[#arg]. +(Like all chunks in Lua, +the script is compiled as a vararg function.) + + +

+In interactive mode, +Lua repeatedly prompts and waits for a line. +After reading a line, +Lua first try to interpret the line as an expression. +If it succeeds, it prints its value. +Otherwise, it interprets the line as a statement. +If you write an incomplete statement, +the interpreter waits for its completion +by issuing a different prompt. + + +

+If the global variable _PROMPT contains a string, +then its value is used as the prompt. +Similarly, if the global variable _PROMPT2 contains a string, +its value is used as the secondary prompt +(issued during incomplete statements). + + +

+In case of unprotected errors in the script, +the interpreter reports the error to the standard error stream. +If the error object is not a string but +has a metamethod __tostring, +the interpreter calls this metamethod to produce the final message. +Otherwise, the interpreter converts the error object to a string +and adds a stack traceback to it. + + +

+When finishing normally, +the interpreter closes its main Lua state +(see lua_close). +The script can avoid this step by +calling os.exit to terminate. + + +

+To allow the use of Lua as a +script interpreter in Unix systems, +the standalone interpreter skips +the first line of a chunk if it starts with #. +Therefore, Lua scripts can be made into executable programs +by using chmod +x and the #! form, +as in + +

+     #!/usr/local/bin/lua
+

+(Of course, +the location of the Lua interpreter may be different in your machine. +If lua is in your PATH, +then + +

+     #!/usr/bin/env lua
+

+is a more portable solution.) + + + +

8 – Incompatibilities with the Previous Version

+ +

+Here we list the incompatibilities that you may find when moving a program +from Lua 5.2 to Lua 5.3. +You can avoid some incompatibilities by compiling Lua with +appropriate options (see file luaconf.h). +However, +all these compatibility options will be removed in the future. + + +

+Lua versions can always change the C API in ways that +do not imply source-code changes in a program, +such as the numeric values for constants +or the implementation of functions as macros. +Therefore, +you should not assume that binaries are compatible between +different Lua versions. +Always recompile clients of the Lua API when +using a new version. + + +

+Similarly, Lua versions can always change the internal representation +of precompiled chunks; +precompiled chunks are not compatible between different Lua versions. + + +

+The standard paths in the official distribution may +change between versions. + + + +

8.1 – Changes in the Language

+
    + +
  • +The main difference between Lua 5.2 and Lua 5.3 is the +introduction of an integer subtype for numbers. +Although this change should not affect "normal" computations, +some computations +(mainly those that involve some kind of overflow) +can give different results. + + +

    +You can fix these differences by forcing a number to be a float +(in Lua 5.2 all numbers were float), +in particular writing constants with an ending .0 +or using x = x + 0.0 to convert a variable. +(This recommendation is only for a quick fix +for an occasional incompatibility; +it is not a general guideline for good programming. +For good programming, +use floats where you need floats +and integers where you need integers.) +

  • + +
  • +The conversion of a float to a string now adds a .0 suffix +to the result if it looks like an integer. +(For instance, the float 2.0 will be printed as 2.0, +not as 2.) +You should always use an explicit format +when you need a specific format for numbers. + + +

    +(Formally this is not an incompatibility, +because Lua does not specify how numbers are formatted as strings, +but some programs assumed a specific format.) +

  • + +
  • +The generational mode for the garbage collector was removed. +(It was an experimental feature in Lua 5.2.) +
  • + +
+ + + + +

8.2 – Changes in the Libraries

+
    + +
  • +The bit32 library has been deprecated. +It is easy to require a compatible external library or, +better yet, to replace its functions with appropriate bitwise operations. +(Keep in mind that bit32 operates on 32-bit integers, +while the bitwise operators in Lua 5.3 operate on Lua integers, +which by default have 64 bits.) +
  • + +
  • +The Table library now respects metamethods +for setting and getting elements. +
  • + +
  • +The ipairs iterator now respects metamethods and +its __ipairs metamethod has been deprecated. +
  • + +
  • +Option names in io.read do not have a starting '*' anymore. +For compatibility, Lua will continue to accept (and ignore) this character. +
  • + +
  • +The following functions were deprecated in the mathematical library: +atan2, cosh, sinh, tanh, pow, +frexp, and ldexp. +You can replace math.pow(x,y) with x^y; +you can replace math.atan2 with math.atan, +which now accepts one or two parameters; +you can replace math.ldexp(x,exp) with x * 2.0^exp. +For the other operations, +you can either use an external library or +implement them in Lua. +
  • + +
  • +The searcher for C loaders used by require +changed the way it handles versioned names. +Now, the version should come after the module name +(as is usual in most other tools). +For compatibility, that searcher still tries the old format +if it cannot find an open function according to the new style. +(Lua 5.2 already worked that way, +but it did not document the change.) +
  • + +
  • +The call collectgarbage("count") now returns only one result. +(You can compute that second result from the fractional part +of the first result.) +
  • + +
+ + + + +

8.3 – Changes in the API

+ + +
    + +
  • +Continuation functions now receive as parameters what they needed +to get through lua_getctx, +so lua_getctx has been removed. +Adapt your code accordingly. +
  • + +
  • +Function lua_dump has an extra parameter, strip. +Use 0 as the value of this parameter to get the old behavior. +
  • + +
  • +Functions to inject/project unsigned integers +(lua_pushunsigned, lua_tounsigned, lua_tounsignedx, +luaL_checkunsigned, luaL_optunsigned) +were deprecated. +Use their signed equivalents with a type cast. +
  • + +
  • +Macros to project non-default integer types +(luaL_checkint, luaL_optint, luaL_checklong, luaL_optlong) +were deprecated. +Use their equivalent over lua_Integer with a type cast +(or, when possible, use lua_Integer in your code). +
  • + +
+ + + + +

9 – The Complete Syntax of Lua

+ +

+Here is the complete syntax of Lua in extended BNF. +As usual in extended BNF, +{A} means 0 or more As, +and [A] means an optional A. +(For operator precedences, see §3.4.8; +for a description of the terminals +Name, Numeral, +and LiteralString, see §3.1.) + + + + +

+
+	chunk ::= block
+
+	block ::= {stat} [retstat]
+
+	stat ::=  ‘;’ | 
+		 varlist ‘=’ explist | 
+		 functioncall | 
+		 label | 
+		 break | 
+		 goto Name | 
+		 do block end | 
+		 while exp do block end | 
+		 repeat block until exp | 
+		 if exp then block {elseif exp then block} [else block] end | 
+		 for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end | 
+		 for namelist in explist do block end | 
+		 function funcname funcbody | 
+		 local function Name funcbody | 
+		 local namelist [‘=’ explist] 
+
+	retstat ::= return [explist] [‘;’]
+
+	label ::= ‘::’ Name ‘::’
+
+	funcname ::= Name {‘.’ Name} [‘:’ Name]
+
+	varlist ::= var {‘,’ var}
+
+	var ::=  Name | prefixexp ‘[’ exp ‘]’ | prefixexp ‘.’ Name 
+
+	namelist ::= Name {‘,’ Name}
+
+	explist ::= exp {‘,’ exp}
+
+	exp ::=  nil | false | true | Numeral | LiteralString | ‘...’ | functiondef | 
+		 prefixexp | tableconstructor | exp binop exp | unop exp 
+
+	prefixexp ::= var | functioncall | ‘(’ exp ‘)’
+
+	functioncall ::=  prefixexp args | prefixexp ‘:’ Name args 
+
+	args ::=  ‘(’ [explist] ‘)’ | tableconstructor | LiteralString 
+
+	functiondef ::= function funcbody
+
+	funcbody ::= ‘(’ [parlist] ‘)’ block end
+
+	parlist ::= namelist [‘,’ ‘...’] | ‘...’
+
+	tableconstructor ::= ‘{’ [fieldlist] ‘}’
+
+	fieldlist ::= field {fieldsep field} [fieldsep]
+
+	field ::= ‘[’ exp ‘]’ ‘=’ exp | Name ‘=’ exp | exp
+
+	fieldsep ::= ‘,’ | ‘;’
+
+	binop ::=  ‘+’ | ‘-’ | ‘*’ | ‘/’ | ‘//’ | ‘^’ | ‘%’ | 
+		 ‘&’ | ‘~’ | ‘|’ | ‘>>’ | ‘<<’ | ‘..’ | 
+		 ‘<’ | ‘<=’ | ‘>’ | ‘>=’ | ‘==’ | ‘~=’ | 
+		 and | or
+
+	unop ::= ‘-’ | not | ‘#’ | ‘~’
+
+
+ +

+ + + + + + + +

+ + + + diff --git a/src/rcheevos/test/lua/doc/osi-certified-72x60.png b/src/rcheevos/test/lua/doc/osi-certified-72x60.png new file mode 100644 index 000000000..07df5f6ee Binary files /dev/null and b/src/rcheevos/test/lua/doc/osi-certified-72x60.png differ diff --git a/src/rcheevos/test/lua/doc/readme.html b/src/rcheevos/test/lua/doc/readme.html new file mode 100644 index 000000000..96a9386e2 --- /dev/null +++ b/src/rcheevos/test/lua/doc/readme.html @@ -0,0 +1,365 @@ + + + +Lua 5.3 readme + + + + + + + +

+Lua +Welcome to Lua 5.3 +

+ + + +

About Lua

+

+Lua is a powerful, fast, lightweight, embeddable scripting language +developed by a +team +at +PUC-Rio, +the Pontifical Catholic University of Rio de Janeiro in Brazil. +Lua is +free software +used in many products and projects around the world. + +

+Lua's +official web site +provides complete information +about Lua, +including +an +executive summary +and +updated +documentation, +especially the +reference manual, +which may differ slightly from the +local copy +distributed in this package. + +

Installing Lua

+

+Lua is distributed in +source +form. +You need to build it before using it. +Building Lua should be straightforward +because +Lua is implemented in pure ANSI C and compiles unmodified in all known +platforms that have an ANSI C compiler. +Lua also compiles unmodified as C++. +The instructions given below for building Lua are for Unix-like platforms. +See also +instructions for other systems +and +customization options. + +

+If you don't have the time or the inclination to compile Lua yourself, +get a binary from +LuaBinaries. +Try also +LuaDist, +a multi-platform distribution of Lua that includes batteries. + +

Building Lua

+

+In most Unix-like platforms, simply do "make" with a suitable target. +Here are the details. + +

    +
  1. +Open a terminal window and move to +the top-level directory, which is named lua-5.3.x. +The Makefile there controls both the build process and the installation process. +

    +

  2. + Do "make" and see if your platform is listed. + The platforms currently supported are: +

    +

    + aix bsd c89 freebsd generic linux macosx mingw posix solaris +

    +

    + If your platform is listed, just do "make xxx", where xxx + is your platform name. +

    + If your platform is not listed, try the closest one or posix, generic, + c89, in this order. +

    +

  3. +The compilation takes only a few moments +and produces three files in the src directory: +lua (the interpreter), +luac (the compiler), +and liblua.a (the library). +

    +

  4. + To check that Lua has been built correctly, do "make test" + after building Lua. This will run the interpreter and print its version. +
+

+If you're running Linux and get compilation errors, +make sure you have installed the readline development package +(which is probably named libreadline-dev or readline-devel). +If you get link errors after that, +then try "make linux MYLIBS=-ltermcap". + +

Installing Lua

+

+ Once you have built Lua, you may want to install it in an official + place in your system. In this case, do "make install". The official + place and the way to install files are defined in the Makefile. You'll + probably need the right permissions to install files. + +

+ To build and install Lua in one step, do "make xxx install", + where xxx is your platform name. + +

+ To install Lua locally, do "make local". + This will create a directory install with subdirectories + bin, include, lib, man, share, + and install Lua as listed below. + + To install Lua locally, but in some other directory, do + "make install INSTALL_TOP=xxx", where xxx is your chosen directory. + The installation starts in the src and doc directories, + so take care if INSTALL_TOP is not an absolute path. + +

+
+ bin: +
+ lua luac +
+ include: +
+ lua.h luaconf.h lualib.h lauxlib.h lua.hpp +
+ lib: +
+ liblua.a +
+ man/man1: +
+ lua.1 luac.1 +
+ +

+ These are the only directories you need for development. + If you only want to run Lua programs, + you only need the files in bin and man. + The files in include and lib are needed for + embedding Lua in C or C++ programs. + +

Customization

+

+ Three kinds of things can be customized by editing a file: +

    +
  • Where and how to install Lua — edit Makefile. +
  • How to build Lua — edit src/Makefile. +
  • Lua features — edit src/luaconf.h. +
+ +

+ You don't actually need to edit the Makefiles because you may set the + relevant variables in the command line when invoking make. + Nevertheless, it's probably best to edit and save the Makefiles to + record the changes you've made. + +

+ On the other hand, if you need to customize some Lua features, you'll need + to edit src/luaconf.h before building and installing Lua. + The edited file will be the one installed, and + it will be used by any Lua clients that you build, to ensure consistency. + Further customization is available to experts by editing the Lua sources. + +

Building Lua on other systems

+

+ If you're not using the usual Unix tools, then the instructions for + building Lua depend on the compiler you use. You'll need to create + projects (or whatever your compiler uses) for building the library, + the interpreter, and the compiler, as follows: + +

+
+library: +
+lapi.c lcode.c lctype.c ldebug.c ldo.c ldump.c lfunc.c lgc.c llex.c +lmem.c lobject.c lopcodes.c lparser.c lstate.c lstring.c ltable.c +ltm.c lundump.c lvm.c lzio.c +lauxlib.c lbaselib.c lbitlib.c lcorolib.c ldblib.c liolib.c +lmathlib.c loslib.c lstrlib.c ltablib.c lutf8lib.c loadlib.c linit.c +
+interpreter: +
+ library, lua.c +
+compiler: +
+ library, luac.c +
+ +

+ To use Lua as a library in your own programs you'll need to know how to + create and use libraries with your compiler. Moreover, to dynamically load + C libraries for Lua you'll need to know how to create dynamic libraries + and you'll need to make sure that the Lua API functions are accessible to + those dynamic libraries — but don't link the Lua library + into each dynamic library. For Unix, we recommend that the Lua library + be linked statically into the host program and its symbols exported for + dynamic linking; src/Makefile does this for the Lua interpreter. + For Windows, we recommend that the Lua library be a DLL. + In all cases, the compiler luac should be linked statically. + +

+ As mentioned above, you may edit src/luaconf.h to customize + some features before building Lua. + +

Changes since Lua 5.2

+

+Here are the main changes introduced in Lua 5.3. +The +reference manual +lists the +incompatibilities that had to be introduced. + +

Main changes

+
    +
  • integers (64-bit by default) +
  • official support for 32-bit numbers +
  • bitwise operators +
  • basic utf-8 support +
  • functions for packing and unpacking values + +
+ +Here are the other changes introduced in Lua 5.3: +

Language

+
    +
  • userdata can have any Lua value as uservalue +
  • floor division +
  • more flexible rules for some metamethods +
+ +

Libraries

+
    +
  • ipairs and the table library respect metamethods +
  • strip option in string.dump +
  • table library respects metamethods +
  • new function table.move +
  • new function string.pack +
  • new function string.unpack +
  • new function string.packsize +
+ +

C API

+
    +
  • simpler API for continuation functions in C +
  • lua_gettable and similar functions return type of resulted value +
  • strip option in lua_dump +
  • new function: lua_geti +
  • new function: lua_seti +
  • new function: lua_isyieldable +
  • new function: lua_numbertointeger +
  • new function: lua_rotate +
  • new function: lua_stringtonumber +
+ +

Lua standalone interpreter

+
    +
  • can be used as calculator; no need to prefix with '=' +
  • arg table available to all code +
+ +

License

+

+ +[osi certified] + +Lua is free software distributed under the terms of the +MIT license +reproduced below; +it may be used for any purpose, including commercial purposes, +at absolutely no cost without having to ask us. + +The only requirement is that if you do use Lua, +then you should give us credit by including the appropriate copyright notice somewhere in your product or its documentation. + +For details, see +this. + +

+Copyright © 1994–2017 Lua.org, PUC-Rio. + +

+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/src/rcheevos/test/lua/src/Makefile b/src/rcheevos/test/lua/src/Makefile new file mode 100644 index 000000000..d71c75c87 --- /dev/null +++ b/src/rcheevos/test/lua/src/Makefile @@ -0,0 +1,197 @@ +# Makefile for building Lua +# See ../doc/readme.html for installation and customization instructions. + +# == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT ======================= + +# Your platform. See PLATS for possible values. +PLAT= none + +CC= gcc -std=gnu99 +CFLAGS= -O2 -Wall -Wextra -DLUA_COMPAT_5_2 $(SYSCFLAGS) $(MYCFLAGS) +LDFLAGS= $(SYSLDFLAGS) $(MYLDFLAGS) +LIBS= -lm $(SYSLIBS) $(MYLIBS) + +AR= ar rcu +RANLIB= ranlib +RM= rm -f + +SYSCFLAGS= +SYSLDFLAGS= +SYSLIBS= + +MYCFLAGS= +MYLDFLAGS= +MYLIBS= +MYOBJS= + +# == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE ======= + +PLATS= aix bsd c89 freebsd generic linux macosx mingw posix solaris + +LUA_A= liblua.a +CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \ + lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \ + ltm.o lundump.o lvm.o lzio.o +LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \ + lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o +BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS) + +LUA_T= lua +LUA_O= lua.o + +LUAC_T= luac +LUAC_O= luac.o + +ALL_O= $(BASE_O) $(LUA_O) $(LUAC_O) +ALL_T= $(LUA_A) $(LUA_T) $(LUAC_T) +ALL_A= $(LUA_A) + +# Targets start here. +default: $(PLAT) + +all: $(ALL_T) + +o: $(ALL_O) + +a: $(ALL_A) + +$(LUA_A): $(BASE_O) + $(AR) $@ $(BASE_O) + $(RANLIB) $@ + +$(LUA_T): $(LUA_O) $(LUA_A) + $(CC) -o $@ $(LDFLAGS) $(LUA_O) $(LUA_A) $(LIBS) + +$(LUAC_T): $(LUAC_O) $(LUA_A) + $(CC) -o $@ $(LDFLAGS) $(LUAC_O) $(LUA_A) $(LIBS) + +clean: + $(RM) $(ALL_T) $(ALL_O) + +depend: + @$(CC) $(CFLAGS) -MM l*.c + +echo: + @echo "PLAT= $(PLAT)" + @echo "CC= $(CC)" + @echo "CFLAGS= $(CFLAGS)" + @echo "LDFLAGS= $(SYSLDFLAGS)" + @echo "LIBS= $(LIBS)" + @echo "AR= $(AR)" + @echo "RANLIB= $(RANLIB)" + @echo "RM= $(RM)" + +# Convenience targets for popular platforms +ALL= all + +none: + @echo "Please do 'make PLATFORM' where PLATFORM is one of these:" + @echo " $(PLATS)" + +aix: + $(MAKE) $(ALL) CC="xlc" CFLAGS="-O2 -DLUA_USE_POSIX -DLUA_USE_DLOPEN" SYSLIBS="-ldl" SYSLDFLAGS="-brtl -bexpall" + +bsd: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_POSIX -DLUA_USE_DLOPEN" SYSLIBS="-Wl,-E" + +c89: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_C89" CC="gcc -std=c89" + @echo '' + @echo '*** C89 does not guarantee 64-bit integers for Lua.' + @echo '' + + +freebsd: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -lreadline" + +generic: $(ALL) + +linux: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-Wl,-E -ldl -lreadline" + +macosx: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_MACOSX" SYSLIBS="-lreadline" CC=cc + +mingw: + $(MAKE) "LUA_A=lua53.dll" "LUA_T=lua.exe" \ + "AR=$(CC) -shared -o" "RANLIB=strip --strip-unneeded" \ + "SYSCFLAGS=-DLUA_BUILD_AS_DLL" "SYSLIBS=" "SYSLDFLAGS=-s" lua.exe + $(MAKE) "LUAC_T=luac.exe" luac.exe + +posix: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_POSIX" + +solaris: + $(MAKE) $(ALL) SYSCFLAGS="-DLUA_USE_POSIX -DLUA_USE_DLOPEN -D_REENTRANT" SYSLIBS="-ldl" + +# list targets that do not create files (but not all makes understand .PHONY) +.PHONY: all $(PLATS) default o a clean depend echo none + +# DO NOT DELETE + +lapi.o: lapi.c lprefix.h lua.h luaconf.h lapi.h llimits.h lstate.h \ + lobject.h ltm.h lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h lstring.h \ + ltable.h lundump.h lvm.h +lauxlib.o: lauxlib.c lprefix.h lua.h luaconf.h lauxlib.h +lbaselib.o: lbaselib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lbitlib.o: lbitlib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lcode.o: lcode.c lprefix.h lua.h luaconf.h lcode.h llex.h lobject.h \ + llimits.h lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h \ + ldo.h lgc.h lstring.h ltable.h lvm.h +lcorolib.o: lcorolib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lctype.o: lctype.c lprefix.h lctype.h lua.h luaconf.h llimits.h +ldblib.o: ldblib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +ldebug.o: ldebug.c lprefix.h lua.h luaconf.h lapi.h llimits.h lstate.h \ + lobject.h ltm.h lzio.h lmem.h lcode.h llex.h lopcodes.h lparser.h \ + ldebug.h ldo.h lfunc.h lstring.h lgc.h ltable.h lvm.h +ldo.o: ldo.c lprefix.h lua.h luaconf.h lapi.h llimits.h lstate.h \ + lobject.h ltm.h lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h lopcodes.h \ + lparser.h lstring.h ltable.h lundump.h lvm.h +ldump.o: ldump.c lprefix.h lua.h luaconf.h lobject.h llimits.h lstate.h \ + ltm.h lzio.h lmem.h lundump.h +lfunc.o: lfunc.c lprefix.h lua.h luaconf.h lfunc.h lobject.h llimits.h \ + lgc.h lstate.h ltm.h lzio.h lmem.h +lgc.o: lgc.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h \ + llimits.h ltm.h lzio.h lmem.h ldo.h lfunc.h lgc.h lstring.h ltable.h +linit.o: linit.c lprefix.h lua.h luaconf.h lualib.h lauxlib.h +liolib.o: liolib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +llex.o: llex.c lprefix.h lua.h luaconf.h lctype.h llimits.h ldebug.h \ + lstate.h lobject.h ltm.h lzio.h lmem.h ldo.h lgc.h llex.h lparser.h \ + lstring.h ltable.h +lmathlib.o: lmathlib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lmem.o: lmem.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h \ + llimits.h ltm.h lzio.h lmem.h ldo.h lgc.h +loadlib.o: loadlib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lobject.o: lobject.c lprefix.h lua.h luaconf.h lctype.h llimits.h \ + ldebug.h lstate.h lobject.h ltm.h lzio.h lmem.h ldo.h lstring.h lgc.h \ + lvm.h +lopcodes.o: lopcodes.c lprefix.h lopcodes.h llimits.h lua.h luaconf.h +loslib.o: loslib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lparser.o: lparser.c lprefix.h lua.h luaconf.h lcode.h llex.h lobject.h \ + llimits.h lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h \ + ldo.h lfunc.h lstring.h lgc.h ltable.h +lstate.o: lstate.c lprefix.h lua.h luaconf.h lapi.h llimits.h lstate.h \ + lobject.h ltm.h lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h llex.h \ + lstring.h ltable.h +lstring.o: lstring.c lprefix.h lua.h luaconf.h ldebug.h lstate.h \ + lobject.h llimits.h ltm.h lzio.h lmem.h ldo.h lstring.h lgc.h +lstrlib.o: lstrlib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +ltable.o: ltable.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h \ + llimits.h ltm.h lzio.h lmem.h ldo.h lgc.h lstring.h ltable.h lvm.h +ltablib.o: ltablib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +ltm.o: ltm.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h \ + llimits.h ltm.h lzio.h lmem.h ldo.h lstring.h lgc.h ltable.h lvm.h +lua.o: lua.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +luac.o: luac.c lprefix.h lua.h luaconf.h lauxlib.h lobject.h llimits.h \ + lstate.h ltm.h lzio.h lmem.h lundump.h ldebug.h lopcodes.h +lundump.o: lundump.c lprefix.h lua.h luaconf.h ldebug.h lstate.h \ + lobject.h llimits.h ltm.h lzio.h lmem.h ldo.h lfunc.h lstring.h lgc.h \ + lundump.h +lutf8lib.o: lutf8lib.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h +lvm.o: lvm.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h \ + llimits.h ltm.h lzio.h lmem.h ldo.h lfunc.h lgc.h lopcodes.h lstring.h \ + ltable.h lvm.h +lzio.o: lzio.c lprefix.h lua.h luaconf.h llimits.h lmem.h lstate.h \ + lobject.h ltm.h lzio.h + +# (end of Makefile) diff --git a/src/rcheevos/test/lua/src/lapi.c b/src/rcheevos/test/lua/src/lapi.c new file mode 100644 index 000000000..0097d070b --- /dev/null +++ b/src/rcheevos/test/lua/src/lapi.c @@ -0,0 +1,1298 @@ +/* +** $Id: lapi.c,v 2.259 2016/02/29 14:27:14 roberto Exp $ +** Lua API +** See Copyright Notice in lua.h +*/ + +#define lapi_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lundump.h" +#include "lvm.h" + + + +const char lua_ident[] = + "$LuaVersion: " LUA_COPYRIGHT " $" + "$LuaAuthors: " LUA_AUTHORS " $"; + + +/* value at a non-valid index */ +#define NONVALIDVALUE cast(TValue *, luaO_nilobject) + +/* corresponding test */ +#define isvalid(o) ((o) != luaO_nilobject) + +/* test for pseudo index */ +#define ispseudo(i) ((i) <= LUA_REGISTRYINDEX) + +/* test for upvalue */ +#define isupvalue(i) ((i) < LUA_REGISTRYINDEX) + +/* test for valid but not pseudo index */ +#define isstackindex(i, o) (isvalid(o) && !ispseudo(i)) + +#define api_checkvalidindex(l,o) api_check(l, isvalid(o), "invalid index") + +#define api_checkstackindex(l, i, o) \ + api_check(l, isstackindex(i, o), "index not in the stack") + + +static TValue *index2addr (lua_State *L, int idx) { + CallInfo *ci = L->ci; + if (idx > 0) { + TValue *o = ci->func + idx; + api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index"); + if (o >= L->top) return NONVALIDVALUE; + else return o; + } + else if (!ispseudo(idx)) { /* negative index */ + api_check(L, idx != 0 && -idx <= L->top - (ci->func + 1), "invalid index"); + return L->top + idx; + } + else if (idx == LUA_REGISTRYINDEX) + return &G(L)->l_registry; + else { /* upvalues */ + idx = LUA_REGISTRYINDEX - idx; + api_check(L, idx <= MAXUPVAL + 1, "upvalue index too large"); + if (ttislcf(ci->func)) /* light C function? */ + return NONVALIDVALUE; /* it has no upvalues */ + else { + CClosure *func = clCvalue(ci->func); + return (idx <= func->nupvalues) ? &func->upvalue[idx-1] : NONVALIDVALUE; + } + } +} + + +/* +** to be called by 'lua_checkstack' in protected mode, to grow stack +** capturing memory errors +*/ +static void growstack (lua_State *L, void *ud) { + int size = *(int *)ud; + luaD_growstack(L, size); +} + + +LUA_API int lua_checkstack (lua_State *L, int n) { + int res; + CallInfo *ci = L->ci; + lua_lock(L); + api_check(L, n >= 0, "negative 'n'"); + if (L->stack_last - L->top > n) /* stack large enough? */ + res = 1; /* yes; check is OK */ + else { /* no; need to grow stack */ + int inuse = cast_int(L->top - L->stack) + EXTRC_STACK; + if (inuse > LUAI_MAXSTACK - n) /* can grow without overflow? */ + res = 0; /* no */ + else /* try to grow stack */ + res = (luaD_rawrunprotected(L, &growstack, &n) == LUA_OK); + } + if (res && ci->top < L->top + n) + ci->top = L->top + n; /* adjust frame top */ + lua_unlock(L); + return res; +} + + +LUA_API void lua_xmove (lua_State *from, lua_State *to, int n) { + int i; + if (from == to) return; + lua_lock(to); + api_checknelems(from, n); + api_check(from, G(from) == G(to), "moving among independent states"); + api_check(from, to->ci->top - to->top >= n, "stack overflow"); + from->top -= n; + for (i = 0; i < n; i++) { + setobj2s(to, to->top, from->top + i); + to->top++; /* stack already checked by previous 'api_check' */ + } + lua_unlock(to); +} + + +LUA_API lua_CFunction lua_atpanic (lua_State *L, lua_CFunction panicf) { + lua_CFunction old; + lua_lock(L); + old = G(L)->panic; + G(L)->panic = panicf; + lua_unlock(L); + return old; +} + + +LUA_API const lua_Number *lua_version (lua_State *L) { + static const lua_Number version = LUA_VERSION_NUM; + if (L == NULL) return &version; + else return G(L)->version; +} + + + +/* +** basic stack manipulation +*/ + + +/* +** convert an acceptable stack index into an absolute index +*/ +LUA_API int lua_absindex (lua_State *L, int idx) { + return (idx > 0 || ispseudo(idx)) + ? idx + : cast_int(L->top - L->ci->func) + idx; +} + + +LUA_API int lua_gettop (lua_State *L) { + return cast_int(L->top - (L->ci->func + 1)); +} + + +LUA_API void lua_settop (lua_State *L, int idx) { + StkId func = L->ci->func; + lua_lock(L); + if (idx >= 0) { + api_check(L, idx <= L->stack_last - (func + 1), "new top too large"); + while (L->top < (func + 1) + idx) + setnilvalue(L->top++); + L->top = (func + 1) + idx; + } + else { + api_check(L, -(idx+1) <= (L->top - (func + 1)), "invalid new top"); + L->top += idx+1; /* 'subtract' index (index is negative) */ + } + lua_unlock(L); +} + + +/* +** Reverse the stack segment from 'from' to 'to' +** (auxiliary to 'lua_rotate') +*/ +static void reverse (lua_State *L, StkId from, StkId to) { + for (; from < to; from++, to--) { + TValue temp; + setobj(L, &temp, from); + setobjs2s(L, from, to); + setobj2s(L, to, &temp); + } +} + + +/* +** Let x = AB, where A is a prefix of length 'n'. Then, +** rotate x n == BA. But BA == (A^r . B^r)^r. +*/ +LUA_API void lua_rotate (lua_State *L, int idx, int n) { + StkId p, t, m; + lua_lock(L); + t = L->top - 1; /* end of stack segment being rotated */ + p = index2addr(L, idx); /* start of segment */ + api_checkstackindex(L, idx, p); + api_check(L, (n >= 0 ? n : -n) <= (t - p + 1), "invalid 'n'"); + m = (n >= 0 ? t - n : p - n - 1); /* end of prefix */ + reverse(L, p, m); /* reverse the prefix with length 'n' */ + reverse(L, m + 1, t); /* reverse the suffix */ + reverse(L, p, t); /* reverse the entire segment */ + lua_unlock(L); +} + + +LUA_API void lua_copy (lua_State *L, int fromidx, int toidx) { + TValue *fr, *to; + lua_lock(L); + fr = index2addr(L, fromidx); + to = index2addr(L, toidx); + api_checkvalidindex(L, to); + setobj(L, to, fr); + if (isupvalue(toidx)) /* function upvalue? */ + luaC_barrier(L, clCvalue(L->ci->func), fr); + /* LUA_REGISTRYINDEX does not need gc barrier + (collector revisits it before finishing collection) */ + lua_unlock(L); +} + + +LUA_API void lua_pushvalue (lua_State *L, int idx) { + lua_lock(L); + setobj2s(L, L->top, index2addr(L, idx)); + api_incr_top(L); + lua_unlock(L); +} + + + +/* +** access functions (stack -> C) +*/ + + +LUA_API int lua_type (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + return (isvalid(o) ? ttnov(o) : LUA_TNONE); +} + + +LUA_API const char *lua_typename (lua_State *L, int t) { + UNUSED(L); + api_check(L, LUA_TNONE <= t && t < LUA_NUMTAGS, "invalid tag"); + return ttypename(t); +} + + +LUA_API int lua_iscfunction (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + return (ttislcf(o) || (ttisCclosure(o))); +} + + +LUA_API int lua_isinteger (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + return ttisinteger(o); +} + + +LUA_API int lua_isnumber (lua_State *L, int idx) { + lua_Number n; + const TValue *o = index2addr(L, idx); + return tonumber(o, &n); +} + + +LUA_API int lua_isstring (lua_State *L, int idx) { + const TValue *o = index2addr(L, idx); + return (ttisstring(o) || cvt2str(o)); +} + + +LUA_API int lua_isuserdata (lua_State *L, int idx) { + const TValue *o = index2addr(L, idx); + return (ttisfulluserdata(o) || ttislightuserdata(o)); +} + + +LUA_API int lua_rawequal (lua_State *L, int index1, int index2) { + StkId o1 = index2addr(L, index1); + StkId o2 = index2addr(L, index2); + return (isvalid(o1) && isvalid(o2)) ? luaV_rawequalobj(o1, o2) : 0; +} + + +LUA_API void lua_arith (lua_State *L, int op) { + lua_lock(L); + if (op != LUA_OPUNM && op != LUA_OPBNOT) + api_checknelems(L, 2); /* all other operations expect two operands */ + else { /* for unary operations, add fake 2nd operand */ + api_checknelems(L, 1); + setobjs2s(L, L->top, L->top - 1); + api_incr_top(L); + } + /* first operand at top - 2, second at top - 1; result go to top - 2 */ + luaO_arith(L, op, L->top - 2, L->top - 1, L->top - 2); + L->top--; /* remove second operand */ + lua_unlock(L); +} + + +LUA_API int lua_compare (lua_State *L, int index1, int index2, int op) { + StkId o1, o2; + int i = 0; + lua_lock(L); /* may call tag method */ + o1 = index2addr(L, index1); + o2 = index2addr(L, index2); + if (isvalid(o1) && isvalid(o2)) { + switch (op) { + case LUA_OPEQ: i = luaV_equalobj(L, o1, o2); break; + case LUA_OPLT: i = luaV_lessthan(L, o1, o2); break; + case LUA_OPLE: i = luaV_lessequal(L, o1, o2); break; + default: api_check(L, 0, "invalid option"); + } + } + lua_unlock(L); + return i; +} + + +LUA_API size_t lua_stringtonumber (lua_State *L, const char *s) { + size_t sz = luaO_str2num(s, L->top); + if (sz != 0) + api_incr_top(L); + return sz; +} + + +LUA_API lua_Number lua_tonumberx (lua_State *L, int idx, int *pisnum) { + lua_Number n; + const TValue *o = index2addr(L, idx); + int isnum = tonumber(o, &n); + if (!isnum) + n = 0; /* call to 'tonumber' may change 'n' even if it fails */ + if (pisnum) *pisnum = isnum; + return n; +} + + +LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum) { + lua_Integer res; + const TValue *o = index2addr(L, idx); + int isnum = tointeger(o, &res); + if (!isnum) + res = 0; /* call to 'tointeger' may change 'n' even if it fails */ + if (pisnum) *pisnum = isnum; + return res; +} + + +LUA_API int lua_toboolean (lua_State *L, int idx) { + const TValue *o = index2addr(L, idx); + return !l_isfalse(o); +} + + +LUA_API const char *lua_tolstring (lua_State *L, int idx, size_t *len) { + StkId o = index2addr(L, idx); + if (!ttisstring(o)) { + if (!cvt2str(o)) { /* not convertible? */ + if (len != NULL) *len = 0; + return NULL; + } + lua_lock(L); /* 'luaO_tostring' may create a new string */ + luaO_tostring(L, o); + luaC_checkGC(L); + o = index2addr(L, idx); /* previous call may reallocate the stack */ + lua_unlock(L); + } + if (len != NULL) + *len = vslen(o); + return svalue(o); +} + + +LUA_API size_t lua_rawlen (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + switch (ttype(o)) { + case LUA_TSHRSTR: return tsvalue(o)->shrlen; + case LUA_TLNGSTR: return tsvalue(o)->u.lnglen; + case LUA_TUSERDATA: return uvalue(o)->len; + case LUA_TTABLE: return luaH_getn(hvalue(o)); + default: return 0; + } +} + + +LUA_API lua_CFunction lua_tocfunction (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + if (ttislcf(o)) return fvalue(o); + else if (ttisCclosure(o)) + return clCvalue(o)->f; + else return NULL; /* not a C function */ +} + + +LUA_API void *lua_touserdata (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + switch (ttnov(o)) { + case LUA_TUSERDATA: return getudatamem(uvalue(o)); + case LUA_TLIGHTUSERDATA: return pvalue(o); + default: return NULL; + } +} + + +LUA_API lua_State *lua_tothread (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + return (!ttisthread(o)) ? NULL : thvalue(o); +} + + +LUA_API const void *lua_topointer (lua_State *L, int idx) { + StkId o = index2addr(L, idx); + switch (ttype(o)) { + case LUA_TTABLE: return hvalue(o); + case LUA_TLCL: return clLvalue(o); + case LUA_TCCL: return clCvalue(o); + case LUA_TLCF: return cast(void *, cast(size_t, fvalue(o))); + case LUA_TTHREAD: return thvalue(o); + case LUA_TUSERDATA: return getudatamem(uvalue(o)); + case LUA_TLIGHTUSERDATA: return pvalue(o); + default: return NULL; + } +} + + + +/* +** push functions (C -> stack) +*/ + + +LUA_API void lua_pushnil (lua_State *L) { + lua_lock(L); + setnilvalue(L->top); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushnumber (lua_State *L, lua_Number n) { + lua_lock(L); + setfltvalue(L->top, n); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushinteger (lua_State *L, lua_Integer n) { + lua_lock(L); + setivalue(L->top, n); + api_incr_top(L); + lua_unlock(L); +} + + +/* +** Pushes on the stack a string with given length. Avoid using 's' when +** 'len' == 0 (as 's' can be NULL in that case), due to later use of +** 'memcmp' and 'memcpy'. +*/ +LUA_API const char *lua_pushlstring (lua_State *L, const char *s, size_t len) { + TString *ts; + lua_lock(L); + ts = (len == 0) ? luaS_new(L, "") : luaS_newlstr(L, s, len); + setsvalue2s(L, L->top, ts); + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return getstr(ts); +} + + +LUA_API const char *lua_pushstring (lua_State *L, const char *s) { + lua_lock(L); + if (s == NULL) + setnilvalue(L->top); + else { + TString *ts; + ts = luaS_new(L, s); + setsvalue2s(L, L->top, ts); + s = getstr(ts); /* internal copy's address */ + } + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return s; +} + + +LUA_API const char *lua_pushvfstring (lua_State *L, const char *fmt, + va_list argp) { + const char *ret; + lua_lock(L); + ret = luaO_pushvfstring(L, fmt, argp); + luaC_checkGC(L); + lua_unlock(L); + return ret; +} + + +LUA_API const char *lua_pushfstring (lua_State *L, const char *fmt, ...) { + const char *ret; + va_list argp; + lua_lock(L); + va_start(argp, fmt); + ret = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + luaC_checkGC(L); + lua_unlock(L); + return ret; +} + + +LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) { + lua_lock(L); + if (n == 0) { + setfvalue(L->top, fn); + } + else { + CClosure *cl; + api_checknelems(L, n); + api_check(L, n <= MAXUPVAL, "upvalue index too large"); + cl = luaF_newCclosure(L, n); + cl->f = fn; + L->top -= n; + while (n--) { + setobj2n(L, &cl->upvalue[n], L->top + n); + /* does not need barrier because closure is white */ + } + setclCvalue(L, L->top, cl); + } + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); +} + + +LUA_API void lua_pushboolean (lua_State *L, int b) { + lua_lock(L); + setbvalue(L->top, (b != 0)); /* ensure that true is 1 */ + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushlightuserdata (lua_State *L, void *p) { + lua_lock(L); + setpvalue(L->top, p); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API int lua_pushthread (lua_State *L) { + lua_lock(L); + setthvalue(L, L->top, L); + api_incr_top(L); + lua_unlock(L); + return (G(L)->mainthread == L); +} + + + +/* +** get functions (Lua -> stack) +*/ + + +static int auxgetstr (lua_State *L, const TValue *t, const char *k) { + const TValue *slot; + TString *str = luaS_new(L, k); + if (luaV_fastget(L, t, str, slot, luaH_getstr)) { + setobj2s(L, L->top, slot); + api_incr_top(L); + } + else { + setsvalue2s(L, L->top, str); + api_incr_top(L); + luaV_finishget(L, t, L->top - 1, L->top - 1, slot); + } + lua_unlock(L); + return ttnov(L->top - 1); +} + + +LUA_API int lua_getglobal (lua_State *L, const char *name) { + Table *reg = hvalue(&G(L)->l_registry); + lua_lock(L); + return auxgetstr(L, luaH_getint(reg, LUA_RIDX_GLOBALS), name); +} + + +LUA_API int lua_gettable (lua_State *L, int idx) { + StkId t; + lua_lock(L); + t = index2addr(L, idx); + luaV_gettable(L, t, L->top - 1, L->top - 1); + lua_unlock(L); + return ttnov(L->top - 1); +} + + +LUA_API int lua_getfield (lua_State *L, int idx, const char *k) { + lua_lock(L); + return auxgetstr(L, index2addr(L, idx), k); +} + + +LUA_API int lua_geti (lua_State *L, int idx, lua_Integer n) { + StkId t; + const TValue *slot; + lua_lock(L); + t = index2addr(L, idx); + if (luaV_fastget(L, t, n, slot, luaH_getint)) { + setobj2s(L, L->top, slot); + api_incr_top(L); + } + else { + setivalue(L->top, n); + api_incr_top(L); + luaV_finishget(L, t, L->top - 1, L->top - 1, slot); + } + lua_unlock(L); + return ttnov(L->top - 1); +} + + +LUA_API int lua_rawget (lua_State *L, int idx) { + StkId t; + lua_lock(L); + t = index2addr(L, idx); + api_check(L, ttistable(t), "table expected"); + setobj2s(L, L->top - 1, luaH_get(hvalue(t), L->top - 1)); + lua_unlock(L); + return ttnov(L->top - 1); +} + + +LUA_API int lua_rawgeti (lua_State *L, int idx, lua_Integer n) { + StkId t; + lua_lock(L); + t = index2addr(L, idx); + api_check(L, ttistable(t), "table expected"); + setobj2s(L, L->top, luaH_getint(hvalue(t), n)); + api_incr_top(L); + lua_unlock(L); + return ttnov(L->top - 1); +} + + +LUA_API int lua_rawgetp (lua_State *L, int idx, const void *p) { + StkId t; + TValue k; + lua_lock(L); + t = index2addr(L, idx); + api_check(L, ttistable(t), "table expected"); + setpvalue(&k, cast(void *, p)); + setobj2s(L, L->top, luaH_get(hvalue(t), &k)); + api_incr_top(L); + lua_unlock(L); + return ttnov(L->top - 1); +} + + +LUA_API void lua_createtable (lua_State *L, int narray, int nrec) { + Table *t; + lua_lock(L); + t = luaH_new(L); + sethvalue(L, L->top, t); + api_incr_top(L); + if (narray > 0 || nrec > 0) + luaH_resize(L, t, narray, nrec); + luaC_checkGC(L); + lua_unlock(L); +} + + +LUA_API int lua_getmetatable (lua_State *L, int objindex) { + const TValue *obj; + Table *mt; + int res = 0; + lua_lock(L); + obj = index2addr(L, objindex); + switch (ttnov(obj)) { + case LUA_TTABLE: + mt = hvalue(obj)->metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(obj)->metatable; + break; + default: + mt = G(L)->mt[ttnov(obj)]; + break; + } + if (mt != NULL) { + sethvalue(L, L->top, mt); + api_incr_top(L); + res = 1; + } + lua_unlock(L); + return res; +} + + +LUA_API int lua_getuservalue (lua_State *L, int idx) { + StkId o; + lua_lock(L); + o = index2addr(L, idx); + api_check(L, ttisfulluserdata(o), "full userdata expected"); + getuservalue(L, uvalue(o), L->top); + api_incr_top(L); + lua_unlock(L); + return ttnov(L->top - 1); +} + + +/* +** set functions (stack -> Lua) +*/ + +/* +** t[k] = value at the top of the stack (where 'k' is a string) +*/ +static void auxsetstr (lua_State *L, const TValue *t, const char *k) { + const TValue *slot; + TString *str = luaS_new(L, k); + api_checknelems(L, 1); + if (luaV_fastset(L, t, str, slot, luaH_getstr, L->top - 1)) + L->top--; /* pop value */ + else { + setsvalue2s(L, L->top, str); /* push 'str' (to make it a TValue) */ + api_incr_top(L); + luaV_finishset(L, t, L->top - 1, L->top - 2, slot); + L->top -= 2; /* pop value and key */ + } + lua_unlock(L); /* lock done by caller */ +} + + +LUA_API void lua_setglobal (lua_State *L, const char *name) { + Table *reg = hvalue(&G(L)->l_registry); + lua_lock(L); /* unlock done in 'auxsetstr' */ + auxsetstr(L, luaH_getint(reg, LUA_RIDX_GLOBALS), name); +} + + +LUA_API void lua_settable (lua_State *L, int idx) { + StkId t; + lua_lock(L); + api_checknelems(L, 2); + t = index2addr(L, idx); + luaV_settable(L, t, L->top - 2, L->top - 1); + L->top -= 2; /* pop index and value */ + lua_unlock(L); +} + + +LUA_API void lua_setfield (lua_State *L, int idx, const char *k) { + lua_lock(L); /* unlock done in 'auxsetstr' */ + auxsetstr(L, index2addr(L, idx), k); +} + + +LUA_API void lua_seti (lua_State *L, int idx, lua_Integer n) { + StkId t; + const TValue *slot; + lua_lock(L); + api_checknelems(L, 1); + t = index2addr(L, idx); + if (luaV_fastset(L, t, n, slot, luaH_getint, L->top - 1)) + L->top--; /* pop value */ + else { + setivalue(L->top, n); + api_incr_top(L); + luaV_finishset(L, t, L->top - 1, L->top - 2, slot); + L->top -= 2; /* pop value and key */ + } + lua_unlock(L); +} + + +LUA_API void lua_rawset (lua_State *L, int idx) { + StkId o; + TValue *slot; + lua_lock(L); + api_checknelems(L, 2); + o = index2addr(L, idx); + api_check(L, ttistable(o), "table expected"); + slot = luaH_set(L, hvalue(o), L->top - 2); + setobj2t(L, slot, L->top - 1); + invalidateTMcache(hvalue(o)); + luaC_barrierback(L, hvalue(o), L->top-1); + L->top -= 2; + lua_unlock(L); +} + + +LUA_API void lua_rawseti (lua_State *L, int idx, lua_Integer n) { + StkId o; + lua_lock(L); + api_checknelems(L, 1); + o = index2addr(L, idx); + api_check(L, ttistable(o), "table expected"); + luaH_setint(L, hvalue(o), n, L->top - 1); + luaC_barrierback(L, hvalue(o), L->top-1); + L->top--; + lua_unlock(L); +} + + +LUA_API void lua_rawsetp (lua_State *L, int idx, const void *p) { + StkId o; + TValue k, *slot; + lua_lock(L); + api_checknelems(L, 1); + o = index2addr(L, idx); + api_check(L, ttistable(o), "table expected"); + setpvalue(&k, cast(void *, p)); + slot = luaH_set(L, hvalue(o), &k); + setobj2t(L, slot, L->top - 1); + luaC_barrierback(L, hvalue(o), L->top - 1); + L->top--; + lua_unlock(L); +} + + +LUA_API int lua_setmetatable (lua_State *L, int objindex) { + TValue *obj; + Table *mt; + lua_lock(L); + api_checknelems(L, 1); + obj = index2addr(L, objindex); + if (ttisnil(L->top - 1)) + mt = NULL; + else { + api_check(L, ttistable(L->top - 1), "table expected"); + mt = hvalue(L->top - 1); + } + switch (ttnov(obj)) { + case LUA_TTABLE: { + hvalue(obj)->metatable = mt; + if (mt) { + luaC_objbarrier(L, gcvalue(obj), mt); + luaC_checkfinalizer(L, gcvalue(obj), mt); + } + break; + } + case LUA_TUSERDATA: { + uvalue(obj)->metatable = mt; + if (mt) { + luaC_objbarrier(L, uvalue(obj), mt); + luaC_checkfinalizer(L, gcvalue(obj), mt); + } + break; + } + default: { + G(L)->mt[ttnov(obj)] = mt; + break; + } + } + L->top--; + lua_unlock(L); + return 1; +} + + +LUA_API void lua_setuservalue (lua_State *L, int idx) { + StkId o; + lua_lock(L); + api_checknelems(L, 1); + o = index2addr(L, idx); + api_check(L, ttisfulluserdata(o), "full userdata expected"); + setuservalue(L, uvalue(o), L->top - 1); + luaC_barrier(L, gcvalue(o), L->top - 1); + L->top--; + lua_unlock(L); +} + + +/* +** 'load' and 'call' functions (run Lua code) +*/ + + +#define checkresults(L,na,nr) \ + api_check(L, (nr) == LUA_MULTRET || (L->ci->top - L->top >= (nr) - (na)), \ + "results from function overflow current stack size") + + +LUA_API void lua_callk (lua_State *L, int nargs, int nresults, + lua_KContext ctx, lua_KFunction k) { + StkId func; + lua_lock(L); + api_check(L, k == NULL || !isLua(L->ci), + "cannot use continuations inside hooks"); + api_checknelems(L, nargs+1); + api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread"); + checkresults(L, nargs, nresults); + func = L->top - (nargs+1); + if (k != NULL && L->nny == 0) { /* need to prepare continuation? */ + L->ci->u.c.k = k; /* save continuation */ + L->ci->u.c.ctx = ctx; /* save context */ + luaD_call(L, func, nresults); /* do the call */ + } + else /* no continuation or no yieldable */ + luaD_callnoyield(L, func, nresults); /* just do the call */ + adjustresults(L, nresults); + lua_unlock(L); +} + + + +/* +** Execute a protected call. +*/ +struct CallS { /* data to 'f_call' */ + StkId func; + int nresults; +}; + + +static void f_call (lua_State *L, void *ud) { + struct CallS *c = cast(struct CallS *, ud); + luaD_callnoyield(L, c->func, c->nresults); +} + + + +LUA_API int lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc, + lua_KContext ctx, lua_KFunction k) { + struct CallS c; + int status; + ptrdiff_t func; + lua_lock(L); + api_check(L, k == NULL || !isLua(L->ci), + "cannot use continuations inside hooks"); + api_checknelems(L, nargs+1); + api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread"); + checkresults(L, nargs, nresults); + if (errfunc == 0) + func = 0; + else { + StkId o = index2addr(L, errfunc); + api_checkstackindex(L, errfunc, o); + func = savestack(L, o); + } + c.func = L->top - (nargs+1); /* function to be called */ + if (k == NULL || L->nny > 0) { /* no continuation or no yieldable? */ + c.nresults = nresults; /* do a 'conventional' protected call */ + status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); + } + else { /* prepare continuation (call is already protected by 'resume') */ + CallInfo *ci = L->ci; + ci->u.c.k = k; /* save continuation */ + ci->u.c.ctx = ctx; /* save context */ + /* save information for error recovery */ + ci->extra = savestack(L, c.func); + ci->u.c.old_errfunc = L->errfunc; + L->errfunc = func; + setoah(ci->callstatus, L->allowhook); /* save value of 'allowhook' */ + ci->callstatus |= CIST_YPCALL; /* function can do error recovery */ + luaD_call(L, c.func, nresults); /* do the call */ + ci->callstatus &= ~CIST_YPCALL; + L->errfunc = ci->u.c.old_errfunc; + status = LUA_OK; /* if it is here, there were no errors */ + } + adjustresults(L, nresults); + lua_unlock(L); + return status; +} + + +LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data, + const char *chunkname, const char *mode) { + ZIO z; + int status; + lua_lock(L); + if (!chunkname) chunkname = "?"; + luaZ_init(L, &z, reader, data); + status = luaD_protectedparser(L, &z, chunkname, mode); + if (status == LUA_OK) { /* no errors? */ + LClosure *f = clLvalue(L->top - 1); /* get newly created function */ + if (f->nupvalues >= 1) { /* does it have an upvalue? */ + /* get global table from registry */ + Table *reg = hvalue(&G(L)->l_registry); + const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS); + /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */ + setobj(L, f->upvals[0]->v, gt); + luaC_upvalbarrier(L, f->upvals[0]); + } + } + lua_unlock(L); + return status; +} + + +LUA_API int lua_dump (lua_State *L, lua_Writer writer, void *data, int strip) { + int status; + TValue *o; + lua_lock(L); + api_checknelems(L, 1); + o = L->top - 1; + if (isLfunction(o)) + status = luaU_dump(L, getproto(o), writer, data, strip); + else + status = 1; + lua_unlock(L); + return status; +} + + +LUA_API int lua_status (lua_State *L) { + return L->status; +} + + +/* +** Garbage-collection function +*/ + +LUA_API int lua_gc (lua_State *L, int what, int data) { + int res = 0; + global_State *g; + lua_lock(L); + g = G(L); + switch (what) { + case LUA_GCSTOP: { + g->gcrunning = 0; + break; + } + case LUA_GCRESTART: { + luaE_setdebt(g, 0); + g->gcrunning = 1; + break; + } + case LUA_GCCOLLECT: { + luaC_fullgc(L, 0); + break; + } + case LUA_GCCOUNT: { + /* GC values are expressed in Kbytes: #bytes/2^10 */ + res = cast_int(gettotalbytes(g) >> 10); + break; + } + case LUA_GCCOUNTB: { + res = cast_int(gettotalbytes(g) & 0x3ff); + break; + } + case LUA_GCSTEP: { + l_mem debt = 1; /* =1 to signal that it did an actual step */ + lu_byte oldrunning = g->gcrunning; + g->gcrunning = 1; /* allow GC to run */ + if (data == 0) { + luaE_setdebt(g, -GCSTEPSIZE); /* to do a "small" step */ + luaC_step(L); + } + else { /* add 'data' to total debt */ + debt = cast(l_mem, data) * 1024 + g->GCdebt; + luaE_setdebt(g, debt); + luaC_checkGC(L); + } + g->gcrunning = oldrunning; /* restore previous state */ + if (debt > 0 && g->gcstate == GCSpause) /* end of cycle? */ + res = 1; /* signal it */ + break; + } + case LUA_GCSETPAUSE: { + res = g->gcpause; + g->gcpause = data; + break; + } + case LUA_GCSETSTEPMUL: { + res = g->gcstepmul; + if (data < 40) data = 40; /* avoid ridiculous low values (and 0) */ + g->gcstepmul = data; + break; + } + case LUA_GCISRUNNING: { + res = g->gcrunning; + break; + } + default: res = -1; /* invalid option */ + } + lua_unlock(L); + return res; +} + + + +/* +** miscellaneous functions +*/ + + +LUA_API int lua_error (lua_State *L) { + lua_lock(L); + api_checknelems(L, 1); + luaG_errormsg(L); + /* code unreachable; will unlock when control actually leaves the kernel */ + return 0; /* to avoid warnings */ +} + + +LUA_API int lua_next (lua_State *L, int idx) { + StkId t; + int more; + lua_lock(L); + t = index2addr(L, idx); + api_check(L, ttistable(t), "table expected"); + more = luaH_next(L, hvalue(t), L->top - 1); + if (more) { + api_incr_top(L); + } + else /* no more elements */ + L->top -= 1; /* remove key */ + lua_unlock(L); + return more; +} + + +LUA_API void lua_concat (lua_State *L, int n) { + lua_lock(L); + api_checknelems(L, n); + if (n >= 2) { + luaV_concat(L, n); + } + else if (n == 0) { /* push empty string */ + setsvalue2s(L, L->top, luaS_newlstr(L, "", 0)); + api_incr_top(L); + } + /* else n == 1; nothing to do */ + luaC_checkGC(L); + lua_unlock(L); +} + + +LUA_API void lua_len (lua_State *L, int idx) { + StkId t; + lua_lock(L); + t = index2addr(L, idx); + luaV_objlen(L, L->top, t); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API lua_Alloc lua_getallocf (lua_State *L, void **ud) { + lua_Alloc f; + lua_lock(L); + if (ud) *ud = G(L)->ud; + f = G(L)->frealloc; + lua_unlock(L); + return f; +} + + +LUA_API void lua_setallocf (lua_State *L, lua_Alloc f, void *ud) { + lua_lock(L); + G(L)->ud = ud; + G(L)->frealloc = f; + lua_unlock(L); +} + + +LUA_API void *lua_newuserdata (lua_State *L, size_t size) { + Udata *u; + lua_lock(L); + u = luaS_newudata(L, size); + setuvalue(L, L->top, u); + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return getudatamem(u); +} + + + +static const char *aux_upvalue (StkId fi, int n, TValue **val, + CClosure **owner, UpVal **uv) { + switch (ttype(fi)) { + case LUA_TCCL: { /* C closure */ + CClosure *f = clCvalue(fi); + if (!(1 <= n && n <= f->nupvalues)) return NULL; + *val = &f->upvalue[n-1]; + if (owner) *owner = f; + return ""; + } + case LUA_TLCL: { /* Lua closure */ + LClosure *f = clLvalue(fi); + TString *name; + Proto *p = f->p; + if (!(1 <= n && n <= p->sizeupvalues)) return NULL; + *val = f->upvals[n-1]->v; + if (uv) *uv = f->upvals[n - 1]; + name = p->upvalues[n-1].name; + return (name == NULL) ? "(*no name)" : getstr(name); + } + default: return NULL; /* not a closure */ + } +} + + +LUA_API const char *lua_getupvalue (lua_State *L, int funcindex, int n) { + const char *name; + TValue *val = NULL; /* to avoid warnings */ + lua_lock(L); + name = aux_upvalue(index2addr(L, funcindex), n, &val, NULL, NULL); + if (name) { + setobj2s(L, L->top, val); + api_incr_top(L); + } + lua_unlock(L); + return name; +} + + +LUA_API const char *lua_setupvalue (lua_State *L, int funcindex, int n) { + const char *name; + TValue *val = NULL; /* to avoid warnings */ + CClosure *owner = NULL; + UpVal *uv = NULL; + StkId fi; + lua_lock(L); + fi = index2addr(L, funcindex); + api_checknelems(L, 1); + name = aux_upvalue(fi, n, &val, &owner, &uv); + if (name) { + L->top--; + setobj(L, val, L->top); + if (owner) { luaC_barrier(L, owner, L->top); } + else if (uv) { luaC_upvalbarrier(L, uv); } + } + lua_unlock(L); + return name; +} + + +static UpVal **getupvalref (lua_State *L, int fidx, int n, LClosure **pf) { + LClosure *f; + StkId fi = index2addr(L, fidx); + api_check(L, ttisLclosure(fi), "Lua function expected"); + f = clLvalue(fi); + api_check(L, (1 <= n && n <= f->p->sizeupvalues), "invalid upvalue index"); + if (pf) *pf = f; + return &f->upvals[n - 1]; /* get its upvalue pointer */ +} + + +LUA_API void *lua_upvalueid (lua_State *L, int fidx, int n) { + StkId fi = index2addr(L, fidx); + switch (ttype(fi)) { + case LUA_TLCL: { /* lua closure */ + return *getupvalref(L, fidx, n, NULL); + } + case LUA_TCCL: { /* C closure */ + CClosure *f = clCvalue(fi); + api_check(L, 1 <= n && n <= f->nupvalues, "invalid upvalue index"); + return &f->upvalue[n - 1]; + } + default: { + api_check(L, 0, "closure expected"); + return NULL; + } + } +} + + +LUA_API void lua_upvaluejoin (lua_State *L, int fidx1, int n1, + int fidx2, int n2) { + LClosure *f1; + UpVal **up1 = getupvalref(L, fidx1, n1, &f1); + UpVal **up2 = getupvalref(L, fidx2, n2, NULL); + luaC_upvdeccount(L, *up1); + *up1 = *up2; + (*up1)->refcount++; + if (upisopen(*up1)) (*up1)->u.open.touched = 1; + luaC_upvalbarrier(L, *up1); +} + + diff --git a/src/rcheevos/test/lua/src/lapi.h b/src/rcheevos/test/lua/src/lapi.h new file mode 100644 index 000000000..6d36dee3f --- /dev/null +++ b/src/rcheevos/test/lua/src/lapi.h @@ -0,0 +1,24 @@ +/* +** $Id: lapi.h,v 2.9 2015/03/06 19:49:50 roberto Exp $ +** Auxiliary functions from Lua API +** See Copyright Notice in lua.h +*/ + +#ifndef lapi_h +#define lapi_h + + +#include "llimits.h" +#include "lstate.h" + +#define api_incr_top(L) {L->top++; api_check(L, L->top <= L->ci->top, \ + "stack overflow");} + +#define adjustresults(L,nres) \ + { if ((nres) == LUA_MULTRET && L->ci->top < L->top) L->ci->top = L->top; } + +#define api_checknelems(L,n) api_check(L, (n) < (L->top - L->ci->func), \ + "not enough elements in the stack") + + +#endif diff --git a/src/rcheevos/test/lua/src/lauxlib.c b/src/rcheevos/test/lua/src/lauxlib.c new file mode 100644 index 000000000..f7a383663 --- /dev/null +++ b/src/rcheevos/test/lua/src/lauxlib.c @@ -0,0 +1,1043 @@ +/* +** $Id: lauxlib.c,v 1.289 2016/12/20 18:37:00 roberto Exp $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + +#define lauxlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include + + +/* +** This file uses only the official API of Lua. +** Any function declared here could be written as an application function. +*/ + +#include "lua.h" + +#include "lauxlib.h" + + +/* +** {====================================================== +** Traceback +** ======================================================= +*/ + + +#define LEVELS1 10 /* size of the first part of the stack */ +#define LEVELS2 11 /* size of the second part of the stack */ + + + +/* +** search for 'objidx' in table at index -1. +** return 1 + string at top if find a good name. +*/ +static int findfield (lua_State *L, int objidx, int level) { + if (level == 0 || !lua_istable(L, -1)) + return 0; /* not found */ + lua_pushnil(L); /* start 'next' loop */ + while (lua_next(L, -2)) { /* for each pair in table */ + if (lua_type(L, -2) == LUA_TSTRING) { /* ignore non-string keys */ + if (lua_rawequal(L, objidx, -1)) { /* found object? */ + lua_pop(L, 1); /* remove value (but keep name) */ + return 1; + } + else if (findfield(L, objidx, level - 1)) { /* try recursively */ + lua_remove(L, -2); /* remove table (but keep name) */ + lua_pushliteral(L, "."); + lua_insert(L, -2); /* place '.' between the two names */ + lua_concat(L, 3); + return 1; + } + } + lua_pop(L, 1); /* remove value */ + } + return 0; /* not found */ +} + + +/* +** Search for a name for a function in all loaded modules +*/ +static int pushglobalfuncname (lua_State *L, lua_Debug *ar) { + int top = lua_gettop(L); + lua_getinfo(L, "f", ar); /* push function */ + lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + if (findfield(L, top + 1, 2)) { + const char *name = lua_tostring(L, -1); + if (strncmp(name, "_G.", 3) == 0) { /* name start with '_G.'? */ + lua_pushstring(L, name + 3); /* push name without prefix */ + lua_remove(L, -2); /* remove original name */ + } + lua_copy(L, -1, top + 1); /* move name to proper place */ + lua_pop(L, 2); /* remove pushed values */ + return 1; + } + else { + lua_settop(L, top); /* remove function and global table */ + return 0; + } +} + + +static void pushfuncname (lua_State *L, lua_Debug *ar) { + if (pushglobalfuncname(L, ar)) { /* try first a global name */ + lua_pushfstring(L, "function '%s'", lua_tostring(L, -1)); + lua_remove(L, -2); /* remove name */ + } + else if (*ar->namewhat != '\0') /* is there a name from code? */ + lua_pushfstring(L, "%s '%s'", ar->namewhat, ar->name); /* use it */ + else if (*ar->what == 'm') /* main? */ + lua_pushliteral(L, "main chunk"); + else if (*ar->what != 'C') /* for Lua functions, use */ + lua_pushfstring(L, "function <%s:%d>", ar->short_src, ar->linedefined); + else /* nothing left... */ + lua_pushliteral(L, "?"); +} + + +static int lastlevel (lua_State *L) { + lua_Debug ar; + int li = 1, le = 1; + /* find an upper bound */ + while (lua_getstack(L, le, &ar)) { li = le; le *= 2; } + /* do a binary search */ + while (li < le) { + int m = (li + le)/2; + if (lua_getstack(L, m, &ar)) li = m + 1; + else le = m; + } + return le - 1; +} + + +LUALIB_API void luaL_traceback (lua_State *L, lua_State *L1, + const char *msg, int level) { + lua_Debug ar; + int top = lua_gettop(L); + int last = lastlevel(L1); + int n1 = (last - level > LEVELS1 + LEVELS2) ? LEVELS1 : -1; + if (msg) + lua_pushfstring(L, "%s\n", msg); + luaL_checkstack(L, 10, NULL); + lua_pushliteral(L, "stack traceback:"); + while (lua_getstack(L1, level++, &ar)) { + if (n1-- == 0) { /* too many levels? */ + lua_pushliteral(L, "\n\t..."); /* add a '...' */ + level = last - LEVELS2 + 1; /* and skip to last ones */ + } + else { + lua_getinfo(L1, "Slnt", &ar); + lua_pushfstring(L, "\n\t%s:", ar.short_src); + if (ar.currentline > 0) + lua_pushfstring(L, "%d:", ar.currentline); + lua_pushliteral(L, " in "); + pushfuncname(L, &ar); + if (ar.istailcall) + lua_pushliteral(L, "\n\t(...tail calls...)"); + lua_concat(L, lua_gettop(L) - top); + } + } + lua_concat(L, lua_gettop(L) - top); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Error-report functions +** ======================================================= +*/ + +LUALIB_API int luaL_argerror (lua_State *L, int arg, const char *extramsg) { + lua_Debug ar; + if (!lua_getstack(L, 0, &ar)) /* no stack frame? */ + return luaL_error(L, "bad argument #%d (%s)", arg, extramsg); + lua_getinfo(L, "n", &ar); + if (strcmp(ar.namewhat, "method") == 0) { + arg--; /* do not count 'self' */ + if (arg == 0) /* error is in the self argument itself? */ + return luaL_error(L, "calling '%s' on bad self (%s)", + ar.name, extramsg); + } + if (ar.name == NULL) + ar.name = (pushglobalfuncname(L, &ar)) ? lua_tostring(L, -1) : "?"; + return luaL_error(L, "bad argument #%d to '%s' (%s)", + arg, ar.name, extramsg); +} + + +static int typeerror (lua_State *L, int arg, const char *tname) { + const char *msg; + const char *typearg; /* name for the type of the actual argument */ + if (luaL_getmetafield(L, arg, "__name") == LUA_TSTRING) + typearg = lua_tostring(L, -1); /* use the given type name */ + else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA) + typearg = "light userdata"; /* special name for messages */ + else + typearg = luaL_typename(L, arg); /* standard name */ + msg = lua_pushfstring(L, "%s expected, got %s", tname, typearg); + return luaL_argerror(L, arg, msg); +} + + +static void tag_error (lua_State *L, int arg, int tag) { + typeerror(L, arg, lua_typename(L, tag)); +} + + +/* +** The use of 'lua_pushfstring' ensures this function does not +** need reserved stack space when called. +*/ +LUALIB_API void luaL_where (lua_State *L, int level) { + lua_Debug ar; + if (lua_getstack(L, level, &ar)) { /* check function at level */ + lua_getinfo(L, "Sl", &ar); /* get info about it */ + if (ar.currentline > 0) { /* is there info? */ + lua_pushfstring(L, "%s:%d: ", ar.short_src, ar.currentline); + return; + } + } + lua_pushfstring(L, ""); /* else, no information available... */ +} + + +/* +** Again, the use of 'lua_pushvfstring' ensures this function does +** not need reserved stack space when called. (At worst, it generates +** an error with "stack overflow" instead of the given message.) +*/ +LUALIB_API int luaL_error (lua_State *L, const char *fmt, ...) { + va_list argp; + va_start(argp, fmt); + luaL_where(L, 1); + lua_pushvfstring(L, fmt, argp); + va_end(argp); + lua_concat(L, 2); + return lua_error(L); +} + + +LUALIB_API int luaL_fileresult (lua_State *L, int stat, const char *fname) { + int en = errno; /* calls to Lua API may change this value */ + if (stat) { + lua_pushboolean(L, 1); + return 1; + } + else { + lua_pushnil(L); + if (fname) + lua_pushfstring(L, "%s: %s", fname, strerror(en)); + else + lua_pushstring(L, strerror(en)); + lua_pushinteger(L, en); + return 3; + } +} + + +#if !defined(l_inspectstat) /* { */ + +#if defined(LUA_USE_POSIX) + +#include + +/* +** use appropriate macros to interpret 'pclose' return status +*/ +#define l_inspectstat(stat,what) \ + if (WIFEXITED(stat)) { stat = WEXITSTATUS(stat); } \ + else if (WIFSIGNALED(stat)) { stat = WTERMSIG(stat); what = "signal"; } + +#else + +#define l_inspectstat(stat,what) /* no op */ + +#endif + +#endif /* } */ + + +LUALIB_API int luaL_execresult (lua_State *L, int stat) { + const char *what = "exit"; /* type of termination */ + if (stat == -1) /* error? */ + return luaL_fileresult(L, 0, NULL); + else { + l_inspectstat(stat, what); /* interpret result */ + if (*what == 'e' && stat == 0) /* successful termination? */ + lua_pushboolean(L, 1); + else + lua_pushnil(L); + lua_pushstring(L, what); + lua_pushinteger(L, stat); + return 3; /* return true/nil,what,code */ + } +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Userdata's metatable manipulation +** ======================================================= +*/ + +LUALIB_API int luaL_newmetatable (lua_State *L, const char *tname) { + if (luaL_getmetatable(L, tname) != LUA_TNIL) /* name already in use? */ + return 0; /* leave previous value on top, but return 0 */ + lua_pop(L, 1); + lua_createtable(L, 0, 2); /* create metatable */ + lua_pushstring(L, tname); + lua_setfield(L, -2, "__name"); /* metatable.__name = tname */ + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, tname); /* registry.name = metatable */ + return 1; +} + + +LUALIB_API void luaL_setmetatable (lua_State *L, const char *tname) { + luaL_getmetatable(L, tname); + lua_setmetatable(L, -2); +} + + +LUALIB_API void *luaL_testudata (lua_State *L, int ud, const char *tname) { + void *p = lua_touserdata(L, ud); + if (p != NULL) { /* value is a userdata? */ + if (lua_getmetatable(L, ud)) { /* does it have a metatable? */ + luaL_getmetatable(L, tname); /* get correct metatable */ + if (!lua_rawequal(L, -1, -2)) /* not the same? */ + p = NULL; /* value is a userdata with wrong metatable */ + lua_pop(L, 2); /* remove both metatables */ + return p; + } + } + return NULL; /* value is not a userdata with a metatable */ +} + + +LUALIB_API void *luaL_checkudata (lua_State *L, int ud, const char *tname) { + void *p = luaL_testudata(L, ud, tname); + if (p == NULL) typeerror(L, ud, tname); + return p; +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Argument check functions +** ======================================================= +*/ + +LUALIB_API int luaL_checkoption (lua_State *L, int arg, const char *def, + const char *const lst[]) { + const char *name = (def) ? luaL_optstring(L, arg, def) : + luaL_checkstring(L, arg); + int i; + for (i=0; lst[i]; i++) + if (strcmp(lst[i], name) == 0) + return i; + return luaL_argerror(L, arg, + lua_pushfstring(L, "invalid option '%s'", name)); +} + + +/* +** Ensures the stack has at least 'space' extra slots, raising an error +** if it cannot fulfill the request. (The error handling needs a few +** extra slots to format the error message. In case of an error without +** this extra space, Lua will generate the same 'stack overflow' error, +** but without 'msg'.) +*/ +LUALIB_API void luaL_checkstack (lua_State *L, int space, const char *msg) { + if (!lua_checkstack(L, space)) { + if (msg) + luaL_error(L, "stack overflow (%s)", msg); + else + luaL_error(L, "stack overflow"); + } +} + + +LUALIB_API void luaL_checktype (lua_State *L, int arg, int t) { + if (lua_type(L, arg) != t) + tag_error(L, arg, t); +} + + +LUALIB_API void luaL_checkany (lua_State *L, int arg) { + if (lua_type(L, arg) == LUA_TNONE) + luaL_argerror(L, arg, "value expected"); +} + + +LUALIB_API const char *luaL_checklstring (lua_State *L, int arg, size_t *len) { + const char *s = lua_tolstring(L, arg, len); + if (!s) tag_error(L, arg, LUA_TSTRING); + return s; +} + + +LUALIB_API const char *luaL_optlstring (lua_State *L, int arg, + const char *def, size_t *len) { + if (lua_isnoneornil(L, arg)) { + if (len) + *len = (def ? strlen(def) : 0); + return def; + } + else return luaL_checklstring(L, arg, len); +} + + +LUALIB_API lua_Number luaL_checknumber (lua_State *L, int arg) { + int isnum; + lua_Number d = lua_tonumberx(L, arg, &isnum); + if (!isnum) + tag_error(L, arg, LUA_TNUMBER); + return d; +} + + +LUALIB_API lua_Number luaL_optnumber (lua_State *L, int arg, lua_Number def) { + return luaL_opt(L, luaL_checknumber, arg, def); +} + + +static void interror (lua_State *L, int arg) { + if (lua_isnumber(L, arg)) + luaL_argerror(L, arg, "number has no integer representation"); + else + tag_error(L, arg, LUA_TNUMBER); +} + + +LUALIB_API lua_Integer luaL_checkinteger (lua_State *L, int arg) { + int isnum; + lua_Integer d = lua_tointegerx(L, arg, &isnum); + if (!isnum) { + interror(L, arg); + } + return d; +} + + +LUALIB_API lua_Integer luaL_optinteger (lua_State *L, int arg, + lua_Integer def) { + return luaL_opt(L, luaL_checkinteger, arg, def); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + +/* userdata to box arbitrary data */ +typedef struct UBox { + void *box; + size_t bsize; +} UBox; + + +static void *resizebox (lua_State *L, int idx, size_t newsize) { + void *ud; + lua_Alloc allocf = lua_getallocf(L, &ud); + UBox *box = (UBox *)lua_touserdata(L, idx); + void *temp = allocf(ud, box->box, box->bsize, newsize); + if (temp == NULL && newsize > 0) { /* allocation error? */ + resizebox(L, idx, 0); /* free buffer */ + luaL_error(L, "not enough memory for buffer allocation"); + } + box->box = temp; + box->bsize = newsize; + return temp; +} + + +static int boxgc (lua_State *L) { + resizebox(L, 1, 0); + return 0; +} + + +static void *newbox (lua_State *L, size_t newsize) { + UBox *box = (UBox *)lua_newuserdata(L, sizeof(UBox)); + box->box = NULL; + box->bsize = 0; + if (luaL_newmetatable(L, "LUABOX")) { /* creating metatable? */ + lua_pushcfunction(L, boxgc); + lua_setfield(L, -2, "__gc"); /* metatable.__gc = boxgc */ + } + lua_setmetatable(L, -2); + return resizebox(L, -1, newsize); +} + + +/* +** check whether buffer is using a userdata on the stack as a temporary +** buffer +*/ +#define buffonstack(B) ((B)->b != (B)->initb) + + +/* +** returns a pointer to a free area with at least 'sz' bytes +*/ +LUALIB_API char *luaL_prepbuffsize (luaL_Buffer *B, size_t sz) { + lua_State *L = B->L; + if (B->size - B->n < sz) { /* not enough space? */ + char *newbuff; + size_t newsize = B->size * 2; /* double buffer size */ + if (newsize - B->n < sz) /* not big enough? */ + newsize = B->n + sz; + if (newsize < B->n || newsize - B->n < sz) + luaL_error(L, "buffer too large"); + /* create larger buffer */ + if (buffonstack(B)) + newbuff = (char *)resizebox(L, -1, newsize); + else { /* no buffer yet */ + newbuff = (char *)newbox(L, newsize); + memcpy(newbuff, B->b, B->n * sizeof(char)); /* copy original content */ + } + B->b = newbuff; + B->size = newsize; + } + return &B->b[B->n]; +} + + +LUALIB_API void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l) { + if (l > 0) { /* avoid 'memcpy' when 's' can be NULL */ + char *b = luaL_prepbuffsize(B, l); + memcpy(b, s, l * sizeof(char)); + luaL_addsize(B, l); + } +} + + +LUALIB_API void luaL_addstring (luaL_Buffer *B, const char *s) { + luaL_addlstring(B, s, strlen(s)); +} + + +LUALIB_API void luaL_pushresult (luaL_Buffer *B) { + lua_State *L = B->L; + lua_pushlstring(L, B->b, B->n); + if (buffonstack(B)) { + resizebox(L, -2, 0); /* delete old buffer */ + lua_remove(L, -2); /* remove its header from the stack */ + } +} + + +LUALIB_API void luaL_pushresultsize (luaL_Buffer *B, size_t sz) { + luaL_addsize(B, sz); + luaL_pushresult(B); +} + + +LUALIB_API void luaL_addvalue (luaL_Buffer *B) { + lua_State *L = B->L; + size_t l; + const char *s = lua_tolstring(L, -1, &l); + if (buffonstack(B)) + lua_insert(L, -2); /* put value below buffer */ + luaL_addlstring(B, s, l); + lua_remove(L, (buffonstack(B)) ? -2 : -1); /* remove value */ +} + + +LUALIB_API void luaL_buffinit (lua_State *L, luaL_Buffer *B) { + B->L = L; + B->b = B->initb; + B->n = 0; + B->size = LUAL_BUFFERSIZE; +} + + +LUALIB_API char *luaL_buffinitsize (lua_State *L, luaL_Buffer *B, size_t sz) { + luaL_buffinit(L, B); + return luaL_prepbuffsize(B, sz); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Reference system +** ======================================================= +*/ + +/* index of free-list header */ +#define freelist 0 + + +LUALIB_API int luaL_ref (lua_State *L, int t) { + int ref; + if (lua_isnil(L, -1)) { + lua_pop(L, 1); /* remove from stack */ + return LUA_REFNIL; /* 'nil' has a unique fixed reference */ + } + t = lua_absindex(L, t); + lua_rawgeti(L, t, freelist); /* get first free element */ + ref = (int)lua_tointeger(L, -1); /* ref = t[freelist] */ + lua_pop(L, 1); /* remove it from stack */ + if (ref != 0) { /* any free element? */ + lua_rawgeti(L, t, ref); /* remove it from list */ + lua_rawseti(L, t, freelist); /* (t[freelist] = t[ref]) */ + } + else /* no free elements */ + ref = (int)lua_rawlen(L, t) + 1; /* get a new reference */ + lua_rawseti(L, t, ref); + return ref; +} + + +LUALIB_API void luaL_unref (lua_State *L, int t, int ref) { + if (ref >= 0) { + t = lua_absindex(L, t); + lua_rawgeti(L, t, freelist); + lua_rawseti(L, t, ref); /* t[ref] = t[freelist] */ + lua_pushinteger(L, ref); + lua_rawseti(L, t, freelist); /* t[freelist] = ref */ + } +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Load functions +** ======================================================= +*/ + +typedef struct LoadF { + int n; /* number of pre-read characters */ + FILE *f; /* file being read */ + char buff[BUFSIZ]; /* area for reading file */ +} LoadF; + + +static const char *getF (lua_State *L, void *ud, size_t *size) { + LoadF *lf = (LoadF *)ud; + (void)L; /* not used */ + if (lf->n > 0) { /* are there pre-read characters to be read? */ + *size = lf->n; /* return them (chars already in buffer) */ + lf->n = 0; /* no more pre-read characters */ + } + else { /* read a block from file */ + /* 'fread' can return > 0 *and* set the EOF flag. If next call to + 'getF' called 'fread', it might still wait for user input. + The next check avoids this problem. */ + if (feof(lf->f)) return NULL; + *size = fread(lf->buff, 1, sizeof(lf->buff), lf->f); /* read block */ + } + return lf->buff; +} + + +static int errfile (lua_State *L, const char *what, int fnameindex) { + const char *serr = strerror(errno); + const char *filename = lua_tostring(L, fnameindex) + 1; + lua_pushfstring(L, "cannot %s %s: %s", what, filename, serr); + lua_remove(L, fnameindex); + return LUA_ERRFILE; +} + + +static int skipBOM (LoadF *lf) { + const char *p = "\xEF\xBB\xBF"; /* UTF-8 BOM mark */ + int c; + lf->n = 0; + do { + c = getc(lf->f); + if (c == EOF || c != *(const unsigned char *)p++) return c; + lf->buff[lf->n++] = c; /* to be read by the parser */ + } while (*p != '\0'); + lf->n = 0; /* prefix matched; discard it */ + return getc(lf->f); /* return next character */ +} + + +/* +** reads the first character of file 'f' and skips an optional BOM mark +** in its beginning plus its first line if it starts with '#'. Returns +** true if it skipped the first line. In any case, '*cp' has the +** first "valid" character of the file (after the optional BOM and +** a first-line comment). +*/ +static int skipcomment (LoadF *lf, int *cp) { + int c = *cp = skipBOM(lf); + if (c == '#') { /* first line is a comment (Unix exec. file)? */ + do { /* skip first line */ + c = getc(lf->f); + } while (c != EOF && c != '\n'); + *cp = getc(lf->f); /* skip end-of-line, if present */ + return 1; /* there was a comment */ + } + else return 0; /* no comment */ +} + + +LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename, + const char *mode) { + LoadF lf; + int status, readstatus; + int c; + int fnameindex = lua_gettop(L) + 1; /* index of filename on the stack */ + if (filename == NULL) { + lua_pushliteral(L, "=stdin"); + lf.f = stdin; + } + else { + lua_pushfstring(L, "@%s", filename); + lf.f = fopen(filename, "r"); + if (lf.f == NULL) return errfile(L, "open", fnameindex); + } + if (skipcomment(&lf, &c)) /* read initial portion */ + lf.buff[lf.n++] = '\n'; /* add line to correct line numbers */ + if (c == LUA_SIGNATURE[0] && filename) { /* binary file? */ + lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */ + if (lf.f == NULL) return errfile(L, "reopen", fnameindex); + skipcomment(&lf, &c); /* re-read initial portion */ + } + if (c != EOF) + lf.buff[lf.n++] = c; /* 'c' is the first character of the stream */ + status = lua_load(L, getF, &lf, lua_tostring(L, -1), mode); + readstatus = ferror(lf.f); + if (filename) fclose(lf.f); /* close file (even in case of errors) */ + if (readstatus) { + lua_settop(L, fnameindex); /* ignore results from 'lua_load' */ + return errfile(L, "read", fnameindex); + } + lua_remove(L, fnameindex); + return status; +} + + +typedef struct LoadS { + const char *s; + size_t size; +} LoadS; + + +static const char *getS (lua_State *L, void *ud, size_t *size) { + LoadS *ls = (LoadS *)ud; + (void)L; /* not used */ + if (ls->size == 0) return NULL; + *size = ls->size; + ls->size = 0; + return ls->s; +} + + +LUALIB_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t size, + const char *name, const char *mode) { + LoadS ls; + ls.s = buff; + ls.size = size; + return lua_load(L, getS, &ls, name, mode); +} + + +LUALIB_API int luaL_loadstring (lua_State *L, const char *s) { + return luaL_loadbuffer(L, s, strlen(s), s); +} + +/* }====================================================== */ + + + +LUALIB_API int luaL_getmetafield (lua_State *L, int obj, const char *event) { + if (!lua_getmetatable(L, obj)) /* no metatable? */ + return LUA_TNIL; + else { + int tt; + lua_pushstring(L, event); + tt = lua_rawget(L, -2); + if (tt == LUA_TNIL) /* is metafield nil? */ + lua_pop(L, 2); /* remove metatable and metafield */ + else + lua_remove(L, -2); /* remove only metatable */ + return tt; /* return metafield type */ + } +} + + +LUALIB_API int luaL_callmeta (lua_State *L, int obj, const char *event) { + obj = lua_absindex(L, obj); + if (luaL_getmetafield(L, obj, event) == LUA_TNIL) /* no metafield? */ + return 0; + lua_pushvalue(L, obj); + lua_call(L, 1, 1); + return 1; +} + + +LUALIB_API lua_Integer luaL_len (lua_State *L, int idx) { + lua_Integer l; + int isnum; + lua_len(L, idx); + l = lua_tointegerx(L, -1, &isnum); + if (!isnum) + luaL_error(L, "object length is not an integer"); + lua_pop(L, 1); /* remove object */ + return l; +} + + +LUALIB_API const char *luaL_tolstring (lua_State *L, int idx, size_t *len) { + if (luaL_callmeta(L, idx, "__tostring")) { /* metafield? */ + if (!lua_isstring(L, -1)) + luaL_error(L, "'__tostring' must return a string"); + } + else { + switch (lua_type(L, idx)) { + case LUA_TNUMBER: { + if (lua_isinteger(L, idx)) + lua_pushfstring(L, "%I", (LUAI_UACINT)lua_tointeger(L, idx)); + else + lua_pushfstring(L, "%f", (LUAI_UACNUMBER)lua_tonumber(L, idx)); + break; + } + case LUA_TSTRING: + lua_pushvalue(L, idx); + break; + case LUA_TBOOLEAN: + lua_pushstring(L, (lua_toboolean(L, idx) ? "true" : "false")); + break; + case LUA_TNIL: + lua_pushliteral(L, "nil"); + break; + default: { + int tt = luaL_getmetafield(L, idx, "__name"); /* try name */ + const char *kind = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : + luaL_typename(L, idx); + lua_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx)); + if (tt != LUA_TNIL) + lua_remove(L, -2); /* remove '__name' */ + break; + } + } + } + return lua_tolstring(L, -1, len); +} + + +/* +** {====================================================== +** Compatibility with 5.1 module functions +** ======================================================= +*/ +#if defined(LUA_COMPAT_MODULE) + +static const char *luaL_findtable (lua_State *L, int idx, + const char *fname, int szhint) { + const char *e; + if (idx) lua_pushvalue(L, idx); + do { + e = strchr(fname, '.'); + if (e == NULL) e = fname + strlen(fname); + lua_pushlstring(L, fname, e - fname); + if (lua_rawget(L, -2) == LUA_TNIL) { /* no such field? */ + lua_pop(L, 1); /* remove this nil */ + lua_createtable(L, 0, (*e == '.' ? 1 : szhint)); /* new table for field */ + lua_pushlstring(L, fname, e - fname); + lua_pushvalue(L, -2); + lua_settable(L, -4); /* set new table into field */ + } + else if (!lua_istable(L, -1)) { /* field has a non-table value? */ + lua_pop(L, 2); /* remove table and value */ + return fname; /* return problematic part of the name */ + } + lua_remove(L, -2); /* remove previous table */ + fname = e + 1; + } while (*e == '.'); + return NULL; +} + + +/* +** Count number of elements in a luaL_Reg list. +*/ +static int libsize (const luaL_Reg *l) { + int size = 0; + for (; l && l->name; l++) size++; + return size; +} + + +/* +** Find or create a module table with a given name. The function +** first looks at the LOADED table and, if that fails, try a +** global variable with that name. In any case, leaves on the stack +** the module table. +*/ +LUALIB_API void luaL_pushmodule (lua_State *L, const char *modname, + int sizehint) { + luaL_findtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE, 1); + if (lua_getfield(L, -1, modname) != LUA_TTABLE) { /* no LOADED[modname]? */ + lua_pop(L, 1); /* remove previous result */ + /* try global variable (and create one if it does not exist) */ + lua_pushglobaltable(L); + if (luaL_findtable(L, 0, modname, sizehint) != NULL) + luaL_error(L, "name conflict for module '%s'", modname); + lua_pushvalue(L, -1); + lua_setfield(L, -3, modname); /* LOADED[modname] = new table */ + } + lua_remove(L, -2); /* remove LOADED table */ +} + + +LUALIB_API void luaL_openlib (lua_State *L, const char *libname, + const luaL_Reg *l, int nup) { + luaL_checkversion(L); + if (libname) { + luaL_pushmodule(L, libname, libsize(l)); /* get/create library table */ + lua_insert(L, -(nup + 1)); /* move library table to below upvalues */ + } + if (l) + luaL_setfuncs(L, l, nup); + else + lua_pop(L, nup); /* remove upvalues */ +} + +#endif +/* }====================================================== */ + +/* +** set functions from list 'l' into table at top - 'nup'; each +** function gets the 'nup' elements at the top as upvalues. +** Returns with only the table at the stack. +*/ +LUALIB_API void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { + luaL_checkstack(L, nup, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -nup); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_setfield(L, -(nup + 2), l->name); + } + lua_pop(L, nup); /* remove upvalues */ +} + + +/* +** ensure that stack[idx][fname] has a table and push that table +** into the stack +*/ +LUALIB_API int luaL_getsubtable (lua_State *L, int idx, const char *fname) { + if (lua_getfield(L, idx, fname) == LUA_TTABLE) + return 1; /* table already there */ + else { + lua_pop(L, 1); /* remove previous result */ + idx = lua_absindex(L, idx); + lua_newtable(L); + lua_pushvalue(L, -1); /* copy to be left at top */ + lua_setfield(L, idx, fname); /* assign new table to field */ + return 0; /* false, because did not find table there */ + } +} + + +/* +** Stripped-down 'require': After checking "loaded" table, calls 'openf' +** to open a module, registers the result in 'package.loaded' table and, +** if 'glb' is true, also registers the result in the global table. +** Leaves resulting module on the top. +*/ +LUALIB_API void luaL_requiref (lua_State *L, const char *modname, + lua_CFunction openf, int glb) { + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + lua_getfield(L, -1, modname); /* LOADED[modname] */ + if (!lua_toboolean(L, -1)) { /* package not already loaded? */ + lua_pop(L, 1); /* remove field */ + lua_pushcfunction(L, openf); + lua_pushstring(L, modname); /* argument to open function */ + lua_call(L, 1, 1); /* call 'openf' to open module */ + lua_pushvalue(L, -1); /* make copy of module (call result) */ + lua_setfield(L, -3, modname); /* LOADED[modname] = module */ + } + lua_remove(L, -2); /* remove LOADED table */ + if (glb) { + lua_pushvalue(L, -1); /* copy of module */ + lua_setglobal(L, modname); /* _G[modname] = module */ + } +} + + +LUALIB_API const char *luaL_gsub (lua_State *L, const char *s, const char *p, + const char *r) { + const char *wild; + size_t l = strlen(p); + luaL_Buffer b; + luaL_buffinit(L, &b); + while ((wild = strstr(s, p)) != NULL) { + luaL_addlstring(&b, s, wild - s); /* push prefix */ + luaL_addstring(&b, r); /* push replacement in place of pattern */ + s = wild + l; /* continue after 'p' */ + } + luaL_addstring(&b, s); /* push last suffix */ + luaL_pushresult(&b); + return lua_tostring(L, -1); +} + + +static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { + (void)ud; (void)osize; /* not used */ + if (nsize == 0) { + free(ptr); + return NULL; + } + else + return realloc(ptr, nsize); +} + + +static int panic (lua_State *L) { + lua_writestringerror("PANIC: unprotected error in call to Lua API (%s)\n", + lua_tostring(L, -1)); + return 0; /* return to Lua to abort */ +} + + +LUALIB_API lua_State *luaL_newstate (void) { + lua_State *L = lua_newstate(l_alloc, NULL); + if (L) lua_atpanic(L, &panic); + return L; +} + + +LUALIB_API void luaL_checkversion_ (lua_State *L, lua_Number ver, size_t sz) { + const lua_Number *v = lua_version(L); + if (sz != LUAL_NUMSIZES) /* check numeric types */ + luaL_error(L, "core and library have incompatible numeric types"); + if (v != lua_version(NULL)) + luaL_error(L, "multiple Lua VMs detected"); + else if (*v != ver) + luaL_error(L, "version mismatch: app. needs %f, Lua core provides %f", + (LUAI_UACNUMBER)ver, (LUAI_UACNUMBER)*v); +} + diff --git a/src/rcheevos/test/lua/src/lauxlib.h b/src/rcheevos/test/lua/src/lauxlib.h new file mode 100644 index 000000000..9a2e66aa0 --- /dev/null +++ b/src/rcheevos/test/lua/src/lauxlib.h @@ -0,0 +1,264 @@ +/* +** $Id: lauxlib.h,v 1.131 2016/12/06 14:54:31 roberto Exp $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lauxlib_h +#define lauxlib_h + + +#include +#include + +#include "lua.h" + + + +/* extra error code for 'luaL_loadfilex' */ +#define LUA_ERRFILE (LUA_ERRERR+1) + + +/* key, in the registry, for table of loaded modules */ +#define LUA_LOADED_TABLE "_LOADED" + + +/* key, in the registry, for table of preloaded loaders */ +#define LUA_PRELOAD_TABLE "_PRELOAD" + + +typedef struct luaL_Reg { + const char *name; + lua_CFunction func; +} luaL_Reg; + + +#define LUAL_NUMSIZES (sizeof(lua_Integer)*16 + sizeof(lua_Number)) + +LUALIB_API void (luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz); +#define luaL_checkversion(L) \ + luaL_checkversion_(L, LUA_VERSION_NUM, LUAL_NUMSIZES) + +LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e); +LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e); +LUALIB_API const char *(luaL_tolstring) (lua_State *L, int idx, size_t *len); +LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg); +LUALIB_API const char *(luaL_checklstring) (lua_State *L, int arg, + size_t *l); +LUALIB_API const char *(luaL_optlstring) (lua_State *L, int arg, + const char *def, size_t *l); +LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int arg); +LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int arg, lua_Number def); + +LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int arg); +LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int arg, + lua_Integer def); + +LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg); +LUALIB_API void (luaL_checktype) (lua_State *L, int arg, int t); +LUALIB_API void (luaL_checkany) (lua_State *L, int arg); + +LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname); +LUALIB_API void (luaL_setmetatable) (lua_State *L, const char *tname); +LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname); +LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname); + +LUALIB_API void (luaL_where) (lua_State *L, int lvl); +LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...); + +LUALIB_API int (luaL_checkoption) (lua_State *L, int arg, const char *def, + const char *const lst[]); + +LUALIB_API int (luaL_fileresult) (lua_State *L, int stat, const char *fname); +LUALIB_API int (luaL_execresult) (lua_State *L, int stat); + +/* predefined references */ +#define LUA_NOREF (-2) +#define LUA_REFNIL (-1) + +LUALIB_API int (luaL_ref) (lua_State *L, int t); +LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref); + +LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename, + const char *mode); + +#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL) + +LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz, + const char *name, const char *mode); +LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s); + +LUALIB_API lua_State *(luaL_newstate) (void); + +LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx); + +LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s, const char *p, + const char *r); + +LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup); + +LUALIB_API int (luaL_getsubtable) (lua_State *L, int idx, const char *fname); + +LUALIB_API void (luaL_traceback) (lua_State *L, lua_State *L1, + const char *msg, int level); + +LUALIB_API void (luaL_requiref) (lua_State *L, const char *modname, + lua_CFunction openf, int glb); + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ + + +#define luaL_newlibtable(L,l) \ + lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1) + +#define luaL_newlib(L,l) \ + (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)) + +#define luaL_argcheck(L, cond,arg,extramsg) \ + ((void)((cond) || luaL_argerror(L, (arg), (extramsg)))) +#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL)) +#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL)) + +#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i))) + +#define luaL_dofile(L, fn) \ + (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_dostring(L, s) \ + (luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n))) + +#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n))) + +#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL) + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + +typedef struct luaL_Buffer { + char *b; /* buffer address */ + size_t size; /* buffer size */ + size_t n; /* number of characters in buffer */ + lua_State *L; + char initb[LUAL_BUFFERSIZE]; /* initial buffer */ +} luaL_Buffer; + + +#define luaL_addchar(B,c) \ + ((void)((B)->n < (B)->size || luaL_prepbuffsize((B), 1)), \ + ((B)->b[(B)->n++] = (c))) + +#define luaL_addsize(B,s) ((B)->n += (s)) + +LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B); +LUALIB_API char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); +LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); +LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s); +LUALIB_API void (luaL_addvalue) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresult) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresultsize) (luaL_Buffer *B, size_t sz); +LUALIB_API char *(luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); + +#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE) + +/* }====================================================== */ + + + +/* +** {====================================================== +** File handles for IO library +** ======================================================= +*/ + +/* +** A file handle is a userdata with metatable 'LUA_FILEHANDLE' and +** initial structure 'luaL_Stream' (it may contain other fields +** after that initial structure). +*/ + +#define LUA_FILEHANDLE "FILE*" + + +typedef struct luaL_Stream { + FILE *f; /* stream (NULL for incompletely created streams) */ + lua_CFunction closef; /* to close stream (NULL for closed streams) */ +} luaL_Stream; + +/* }====================================================== */ + + + +/* compatibility with old module system */ +#if defined(LUA_COMPAT_MODULE) + +LUALIB_API void (luaL_pushmodule) (lua_State *L, const char *modname, + int sizehint); +LUALIB_API void (luaL_openlib) (lua_State *L, const char *libname, + const luaL_Reg *l, int nup); + +#define luaL_register(L,n,l) (luaL_openlib(L,(n),(l),0)) + +#endif + + +/* +** {================================================================== +** "Abstraction Layer" for basic report of messages and errors +** =================================================================== +*/ + +/* print a string */ +#if !defined(lua_writestring) +#define lua_writestring(s,l) fwrite((s), sizeof(char), (l), stdout) +#endif + +/* print a newline and flush the output */ +#if !defined(lua_writeline) +#define lua_writeline() (lua_writestring("\n", 1), fflush(stdout)) +#endif + +/* print an error message */ +#if !defined(lua_writestringerror) +#define lua_writestringerror(s,p) \ + (fprintf(stderr, (s), (p)), fflush(stderr)) +#endif + +/* }================================================================== */ + + +/* +** {============================================================ +** Compatibility with deprecated conversions +** ============================================================= +*/ +#if defined(LUA_COMPAT_APIINTCASTS) + +#define luaL_checkunsigned(L,a) ((lua_Unsigned)luaL_checkinteger(L,a)) +#define luaL_optunsigned(L,a,d) \ + ((lua_Unsigned)luaL_optinteger(L,a,(lua_Integer)(d))) + +#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n))) +#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d))) + +#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n))) +#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d))) + +#endif +/* }============================================================ */ + + + +#endif + + diff --git a/src/rcheevos/test/lua/src/lbaselib.c b/src/rcheevos/test/lua/src/lbaselib.c new file mode 100644 index 000000000..08523e6e7 --- /dev/null +++ b/src/rcheevos/test/lua/src/lbaselib.c @@ -0,0 +1,498 @@ +/* +** $Id: lbaselib.c,v 1.314 2016/09/05 19:06:34 roberto Exp $ +** Basic library +** See Copyright Notice in lua.h +*/ + +#define lbaselib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +static int luaB_print (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int i; + lua_getglobal(L, "tostring"); + for (i=1; i<=n; i++) { + const char *s; + size_t l; + lua_pushvalue(L, -1); /* function to be called */ + lua_pushvalue(L, i); /* value to print */ + lua_call(L, 1, 1); + s = lua_tolstring(L, -1, &l); /* get result */ + if (s == NULL) + return luaL_error(L, "'tostring' must return a string to 'print'"); + if (i>1) lua_writestring("\t", 1); + lua_writestring(s, l); + lua_pop(L, 1); /* pop result */ + } + lua_writeline(); + return 0; +} + + +#define SPACECHARS " \f\n\r\t\v" + +static const char *b_str2int (const char *s, int base, lua_Integer *pn) { + lua_Unsigned n = 0; + int neg = 0; + s += strspn(s, SPACECHARS); /* skip initial spaces */ + if (*s == '-') { s++; neg = 1; } /* handle signal */ + else if (*s == '+') s++; + if (!isalnum((unsigned char)*s)) /* no digit? */ + return NULL; + do { + int digit = (isdigit((unsigned char)*s)) ? *s - '0' + : (toupper((unsigned char)*s) - 'A') + 10; + if (digit >= base) return NULL; /* invalid numeral */ + n = n * base + digit; + s++; + } while (isalnum((unsigned char)*s)); + s += strspn(s, SPACECHARS); /* skip trailing spaces */ + *pn = (lua_Integer)((neg) ? (0u - n) : n); + return s; +} + + +static int luaB_tonumber (lua_State *L) { + if (lua_isnoneornil(L, 2)) { /* standard conversion? */ + luaL_checkany(L, 1); + if (lua_type(L, 1) == LUA_TNUMBER) { /* already a number? */ + lua_settop(L, 1); /* yes; return it */ + return 1; + } + else { + size_t l; + const char *s = lua_tolstring(L, 1, &l); + if (s != NULL && lua_stringtonumber(L, s) == l + 1) + return 1; /* successful conversion to number */ + /* else not a number */ + } + } + else { + size_t l; + const char *s; + lua_Integer n = 0; /* to avoid warnings */ + lua_Integer base = luaL_checkinteger(L, 2); + luaL_checktype(L, 1, LUA_TSTRING); /* no numbers as strings */ + s = lua_tolstring(L, 1, &l); + luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range"); + if (b_str2int(s, (int)base, &n) == s + l) { + lua_pushinteger(L, n); + return 1; + } /* else not a number */ + } /* else not a number */ + lua_pushnil(L); /* not a number */ + return 1; +} + + +static int luaB_error (lua_State *L) { + int level = (int)luaL_optinteger(L, 2, 1); + lua_settop(L, 1); + if (lua_type(L, 1) == LUA_TSTRING && level > 0) { + luaL_where(L, level); /* add extra information */ + lua_pushvalue(L, 1); + lua_concat(L, 2); + } + return lua_error(L); +} + + +static int luaB_getmetatable (lua_State *L) { + luaL_checkany(L, 1); + if (!lua_getmetatable(L, 1)) { + lua_pushnil(L); + return 1; /* no metatable */ + } + luaL_getmetafield(L, 1, "__metatable"); + return 1; /* returns either __metatable field (if present) or metatable */ +} + + +static int luaB_setmetatable (lua_State *L) { + int t = lua_type(L, 2); + luaL_checktype(L, 1, LUA_TTABLE); + luaL_argcheck(L, t == LUA_TNIL || t == LUA_TTABLE, 2, + "nil or table expected"); + if (luaL_getmetafield(L, 1, "__metatable") != LUA_TNIL) + return luaL_error(L, "cannot change a protected metatable"); + lua_settop(L, 2); + lua_setmetatable(L, 1); + return 1; +} + + +static int luaB_rawequal (lua_State *L) { + luaL_checkany(L, 1); + luaL_checkany(L, 2); + lua_pushboolean(L, lua_rawequal(L, 1, 2)); + return 1; +} + + +static int luaB_rawlen (lua_State *L) { + int t = lua_type(L, 1); + luaL_argcheck(L, t == LUA_TTABLE || t == LUA_TSTRING, 1, + "table or string expected"); + lua_pushinteger(L, lua_rawlen(L, 1)); + return 1; +} + + +static int luaB_rawget (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + lua_settop(L, 2); + lua_rawget(L, 1); + return 1; +} + +static int luaB_rawset (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + luaL_checkany(L, 3); + lua_settop(L, 3); + lua_rawset(L, 1); + return 1; +} + + +static int luaB_collectgarbage (lua_State *L) { + static const char *const opts[] = {"stop", "restart", "collect", + "count", "step", "setpause", "setstepmul", + "isrunning", NULL}; + static const int optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, LUA_GCCOLLECT, + LUA_GCCOUNT, LUA_GCSTEP, LUA_GCSETPAUSE, LUA_GCSETSTEPMUL, + LUA_GCISRUNNING}; + int o = optsnum[luaL_checkoption(L, 1, "collect", opts)]; + int ex = (int)luaL_optinteger(L, 2, 0); + int res = lua_gc(L, o, ex); + switch (o) { + case LUA_GCCOUNT: { + int b = lua_gc(L, LUA_GCCOUNTB, 0); + lua_pushnumber(L, (lua_Number)res + ((lua_Number)b/1024)); + return 1; + } + case LUA_GCSTEP: case LUA_GCISRUNNING: { + lua_pushboolean(L, res); + return 1; + } + default: { + lua_pushinteger(L, res); + return 1; + } + } +} + + +static int luaB_type (lua_State *L) { + int t = lua_type(L, 1); + luaL_argcheck(L, t != LUA_TNONE, 1, "value expected"); + lua_pushstring(L, lua_typename(L, t)); + return 1; +} + + +static int pairsmeta (lua_State *L, const char *method, int iszero, + lua_CFunction iter) { + luaL_checkany(L, 1); + if (luaL_getmetafield(L, 1, method) == LUA_TNIL) { /* no metamethod? */ + lua_pushcfunction(L, iter); /* will return generator, */ + lua_pushvalue(L, 1); /* state, */ + if (iszero) lua_pushinteger(L, 0); /* and initial value */ + else lua_pushnil(L); + } + else { + lua_pushvalue(L, 1); /* argument 'self' to metamethod */ + lua_call(L, 1, 3); /* get 3 values from metamethod */ + } + return 3; +} + + +static int luaB_next (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 2); /* create a 2nd argument if there isn't one */ + if (lua_next(L, 1)) + return 2; + else { + lua_pushnil(L); + return 1; + } +} + + +static int luaB_pairs (lua_State *L) { + return pairsmeta(L, "__pairs", 0, luaB_next); +} + + +/* +** Traversal function for 'ipairs' +*/ +static int ipairsaux (lua_State *L) { + lua_Integer i = luaL_checkinteger(L, 2) + 1; + lua_pushinteger(L, i); + return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2; +} + + +/* +** 'ipairs' function. Returns 'ipairsaux', given "table", 0. +** (The given "table" may not be a table.) +*/ +static int luaB_ipairs (lua_State *L) { +#if defined(LUA_COMPAT_IPAIRS) + return pairsmeta(L, "__ipairs", 1, ipairsaux); +#else + luaL_checkany(L, 1); + lua_pushcfunction(L, ipairsaux); /* iteration function */ + lua_pushvalue(L, 1); /* state */ + lua_pushinteger(L, 0); /* initial value */ + return 3; +#endif +} + + +static int load_aux (lua_State *L, int status, int envidx) { + if (status == LUA_OK) { + if (envidx != 0) { /* 'env' parameter? */ + lua_pushvalue(L, envidx); /* environment for loaded function */ + if (!lua_setupvalue(L, -2, 1)) /* set it as 1st upvalue */ + lua_pop(L, 1); /* remove 'env' if not used by previous call */ + } + return 1; + } + else { /* error (message is on top of the stack) */ + lua_pushnil(L); + lua_insert(L, -2); /* put before error message */ + return 2; /* return nil plus error message */ + } +} + + +static int luaB_loadfile (lua_State *L) { + const char *fname = luaL_optstring(L, 1, NULL); + const char *mode = luaL_optstring(L, 2, NULL); + int env = (!lua_isnone(L, 3) ? 3 : 0); /* 'env' index or 0 if no 'env' */ + int status = luaL_loadfilex(L, fname, mode); + return load_aux(L, status, env); +} + + +/* +** {====================================================== +** Generic Read function +** ======================================================= +*/ + + +/* +** reserved slot, above all arguments, to hold a copy of the returned +** string to avoid it being collected while parsed. 'load' has four +** optional arguments (chunk, source name, mode, and environment). +*/ +#define RESERVEDSLOT 5 + + +/* +** Reader for generic 'load' function: 'lua_load' uses the +** stack for internal stuff, so the reader cannot change the +** stack top. Instead, it keeps its resulting string in a +** reserved slot inside the stack. +*/ +static const char *generic_reader (lua_State *L, void *ud, size_t *size) { + (void)(ud); /* not used */ + luaL_checkstack(L, 2, "too many nested functions"); + lua_pushvalue(L, 1); /* get function */ + lua_call(L, 0, 1); /* call it */ + if (lua_isnil(L, -1)) { + lua_pop(L, 1); /* pop result */ + *size = 0; + return NULL; + } + else if (!lua_isstring(L, -1)) + luaL_error(L, "reader function must return a string"); + lua_replace(L, RESERVEDSLOT); /* save string in reserved slot */ + return lua_tolstring(L, RESERVEDSLOT, size); +} + + +static int luaB_load (lua_State *L) { + int status; + size_t l; + const char *s = lua_tolstring(L, 1, &l); + const char *mode = luaL_optstring(L, 3, "bt"); + int env = (!lua_isnone(L, 4) ? 4 : 0); /* 'env' index or 0 if no 'env' */ + if (s != NULL) { /* loading a string? */ + const char *chunkname = luaL_optstring(L, 2, s); + status = luaL_loadbufferx(L, s, l, chunkname, mode); + } + else { /* loading from a reader function */ + const char *chunkname = luaL_optstring(L, 2, "=(load)"); + luaL_checktype(L, 1, LUA_TFUNCTION); + lua_settop(L, RESERVEDSLOT); /* create reserved slot */ + status = lua_load(L, generic_reader, NULL, chunkname, mode); + } + return load_aux(L, status, env); +} + +/* }====================================================== */ + + +static int dofilecont (lua_State *L, int d1, lua_KContext d2) { + (void)d1; (void)d2; /* only to match 'lua_Kfunction' prototype */ + return lua_gettop(L) - 1; +} + + +static int luaB_dofile (lua_State *L) { + const char *fname = luaL_optstring(L, 1, NULL); + lua_settop(L, 1); + if (luaL_loadfile(L, fname) != LUA_OK) + return lua_error(L); + lua_callk(L, 0, LUA_MULTRET, 0, dofilecont); + return dofilecont(L, 0, 0); +} + + +static int luaB_assert (lua_State *L) { + if (lua_toboolean(L, 1)) /* condition is true? */ + return lua_gettop(L); /* return all arguments */ + else { /* error */ + luaL_checkany(L, 1); /* there must be a condition */ + lua_remove(L, 1); /* remove it */ + lua_pushliteral(L, "assertion failed!"); /* default message */ + lua_settop(L, 1); /* leave only message (default if no other one) */ + return luaB_error(L); /* call 'error' */ + } +} + + +static int luaB_select (lua_State *L) { + int n = lua_gettop(L); + if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#') { + lua_pushinteger(L, n-1); + return 1; + } + else { + lua_Integer i = luaL_checkinteger(L, 1); + if (i < 0) i = n + i; + else if (i > n) i = n; + luaL_argcheck(L, 1 <= i, 1, "index out of range"); + return n - (int)i; + } +} + + +/* +** Continuation function for 'pcall' and 'xpcall'. Both functions +** already pushed a 'true' before doing the call, so in case of success +** 'finishpcall' only has to return everything in the stack minus +** 'extra' values (where 'extra' is exactly the number of items to be +** ignored). +*/ +static int finishpcall (lua_State *L, int status, lua_KContext extra) { + if (status != LUA_OK && status != LUA_YIELD) { /* error? */ + lua_pushboolean(L, 0); /* first result (false) */ + lua_pushvalue(L, -2); /* error message */ + return 2; /* return false, msg */ + } + else + return lua_gettop(L) - (int)extra; /* return all results */ +} + + +static int luaB_pcall (lua_State *L) { + int status; + luaL_checkany(L, 1); + lua_pushboolean(L, 1); /* first result if no errors */ + lua_insert(L, 1); /* put it in place */ + status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall); + return finishpcall(L, status, 0); +} + + +/* +** Do a protected call with error handling. After 'lua_rotate', the +** stack will have ; so, the function passes +** 2 to 'finishpcall' to skip the 2 first values when returning results. +*/ +static int luaB_xpcall (lua_State *L) { + int status; + int n = lua_gettop(L); + luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */ + lua_pushboolean(L, 1); /* first result */ + lua_pushvalue(L, 1); /* function */ + lua_rotate(L, 3, 2); /* move them below function's arguments */ + status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall); + return finishpcall(L, status, 2); +} + + +static int luaB_tostring (lua_State *L) { + luaL_checkany(L, 1); + luaL_tolstring(L, 1, NULL); + return 1; +} + + +static const luaL_Reg base_funcs[] = { + {"assert", luaB_assert}, + {"collectgarbage", luaB_collectgarbage}, + {"dofile", luaB_dofile}, + {"error", luaB_error}, + {"getmetatable", luaB_getmetatable}, + {"ipairs", luaB_ipairs}, + {"loadfile", luaB_loadfile}, + {"load", luaB_load}, +#if defined(LUA_COMPAT_LOADSTRING) + {"loadstring", luaB_load}, +#endif + {"next", luaB_next}, + {"pairs", luaB_pairs}, + {"pcall", luaB_pcall}, + {"print", luaB_print}, + {"rawequal", luaB_rawequal}, + {"rawlen", luaB_rawlen}, + {"rawget", luaB_rawget}, + {"rawset", luaB_rawset}, + {"select", luaB_select}, + {"setmetatable", luaB_setmetatable}, + {"tonumber", luaB_tonumber}, + {"tostring", luaB_tostring}, + {"type", luaB_type}, + {"xpcall", luaB_xpcall}, + /* placeholders */ + {"_G", NULL}, + {"_VERSION", NULL}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_base (lua_State *L) { + /* open lib into global table */ + lua_pushglobaltable(L); + luaL_setfuncs(L, base_funcs, 0); + /* set global _G */ + lua_pushvalue(L, -1); + lua_setfield(L, -2, "_G"); + /* set global _VERSION */ + lua_pushliteral(L, LUA_VERSION); + lua_setfield(L, -2, "_VERSION"); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/lbitlib.c b/src/rcheevos/test/lua/src/lbitlib.c new file mode 100644 index 000000000..1cb1d5b93 --- /dev/null +++ b/src/rcheevos/test/lua/src/lbitlib.c @@ -0,0 +1,233 @@ +/* +** $Id: lbitlib.c,v 1.30 2015/11/11 19:08:09 roberto Exp $ +** Standard library for bitwise operations +** See Copyright Notice in lua.h +*/ + +#define lbitlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +#if defined(LUA_COMPAT_BITLIB) /* { */ + + +#define pushunsigned(L,n) lua_pushinteger(L, (lua_Integer)(n)) +#define checkunsigned(L,i) ((lua_Unsigned)luaL_checkinteger(L,i)) + + +/* number of bits to consider in a number */ +#if !defined(LUA_NBITS) +#define LUA_NBITS 32 +#endif + + +/* +** a lua_Unsigned with its first LUA_NBITS bits equal to 1. (Shift must +** be made in two parts to avoid problems when LUA_NBITS is equal to the +** number of bits in a lua_Unsigned.) +*/ +#define ALLONES (~(((~(lua_Unsigned)0) << (LUA_NBITS - 1)) << 1)) + + +/* macro to trim extra bits */ +#define trim(x) ((x) & ALLONES) + + +/* builds a number with 'n' ones (1 <= n <= LUA_NBITS) */ +#define mask(n) (~((ALLONES << 1) << ((n) - 1))) + + + +static lua_Unsigned andaux (lua_State *L) { + int i, n = lua_gettop(L); + lua_Unsigned r = ~(lua_Unsigned)0; + for (i = 1; i <= n; i++) + r &= checkunsigned(L, i); + return trim(r); +} + + +static int b_and (lua_State *L) { + lua_Unsigned r = andaux(L); + pushunsigned(L, r); + return 1; +} + + +static int b_test (lua_State *L) { + lua_Unsigned r = andaux(L); + lua_pushboolean(L, r != 0); + return 1; +} + + +static int b_or (lua_State *L) { + int i, n = lua_gettop(L); + lua_Unsigned r = 0; + for (i = 1; i <= n; i++) + r |= checkunsigned(L, i); + pushunsigned(L, trim(r)); + return 1; +} + + +static int b_xor (lua_State *L) { + int i, n = lua_gettop(L); + lua_Unsigned r = 0; + for (i = 1; i <= n; i++) + r ^= checkunsigned(L, i); + pushunsigned(L, trim(r)); + return 1; +} + + +static int b_not (lua_State *L) { + lua_Unsigned r = ~checkunsigned(L, 1); + pushunsigned(L, trim(r)); + return 1; +} + + +static int b_shift (lua_State *L, lua_Unsigned r, lua_Integer i) { + if (i < 0) { /* shift right? */ + i = -i; + r = trim(r); + if (i >= LUA_NBITS) r = 0; + else r >>= i; + } + else { /* shift left */ + if (i >= LUA_NBITS) r = 0; + else r <<= i; + r = trim(r); + } + pushunsigned(L, r); + return 1; +} + + +static int b_lshift (lua_State *L) { + return b_shift(L, checkunsigned(L, 1), luaL_checkinteger(L, 2)); +} + + +static int b_rshift (lua_State *L) { + return b_shift(L, checkunsigned(L, 1), -luaL_checkinteger(L, 2)); +} + + +static int b_arshift (lua_State *L) { + lua_Unsigned r = checkunsigned(L, 1); + lua_Integer i = luaL_checkinteger(L, 2); + if (i < 0 || !(r & ((lua_Unsigned)1 << (LUA_NBITS - 1)))) + return b_shift(L, r, -i); + else { /* arithmetic shift for 'negative' number */ + if (i >= LUA_NBITS) r = ALLONES; + else + r = trim((r >> i) | ~(trim(~(lua_Unsigned)0) >> i)); /* add signal bit */ + pushunsigned(L, r); + return 1; + } +} + + +static int b_rot (lua_State *L, lua_Integer d) { + lua_Unsigned r = checkunsigned(L, 1); + int i = d & (LUA_NBITS - 1); /* i = d % NBITS */ + r = trim(r); + if (i != 0) /* avoid undefined shift of LUA_NBITS when i == 0 */ + r = (r << i) | (r >> (LUA_NBITS - i)); + pushunsigned(L, trim(r)); + return 1; +} + + +static int b_lrot (lua_State *L) { + return b_rot(L, luaL_checkinteger(L, 2)); +} + + +static int b_rrot (lua_State *L) { + return b_rot(L, -luaL_checkinteger(L, 2)); +} + + +/* +** get field and width arguments for field-manipulation functions, +** checking whether they are valid. +** ('luaL_error' called without 'return' to avoid later warnings about +** 'width' being used uninitialized.) +*/ +static int fieldargs (lua_State *L, int farg, int *width) { + lua_Integer f = luaL_checkinteger(L, farg); + lua_Integer w = luaL_optinteger(L, farg + 1, 1); + luaL_argcheck(L, 0 <= f, farg, "field cannot be negative"); + luaL_argcheck(L, 0 < w, farg + 1, "width must be positive"); + if (f + w > LUA_NBITS) + luaL_error(L, "trying to access non-existent bits"); + *width = (int)w; + return (int)f; +} + + +static int b_extract (lua_State *L) { + int w; + lua_Unsigned r = trim(checkunsigned(L, 1)); + int f = fieldargs(L, 2, &w); + r = (r >> f) & mask(w); + pushunsigned(L, r); + return 1; +} + + +static int b_replace (lua_State *L) { + int w; + lua_Unsigned r = trim(checkunsigned(L, 1)); + lua_Unsigned v = trim(checkunsigned(L, 2)); + int f = fieldargs(L, 3, &w); + lua_Unsigned m = mask(w); + r = (r & ~(m << f)) | ((v & m) << f); + pushunsigned(L, r); + return 1; +} + + +static const luaL_Reg bitlib[] = { + {"arshift", b_arshift}, + {"band", b_and}, + {"bnot", b_not}, + {"bor", b_or}, + {"bxor", b_xor}, + {"btest", b_test}, + {"extract", b_extract}, + {"lrotate", b_lrot}, + {"lshift", b_lshift}, + {"replace", b_replace}, + {"rrotate", b_rrot}, + {"rshift", b_rshift}, + {NULL, NULL} +}; + + + +LUAMOD_API int luaopen_bit32 (lua_State *L) { + luaL_newlib(L, bitlib); + return 1; +} + + +#else /* }{ */ + + +LUAMOD_API int luaopen_bit32 (lua_State *L) { + return luaL_error(L, "library 'bit32' has been deprecated"); +} + +#endif /* } */ diff --git a/src/rcheevos/test/lua/src/lcode.c b/src/rcheevos/test/lua/src/lcode.c new file mode 100644 index 000000000..0bb414262 --- /dev/null +++ b/src/rcheevos/test/lua/src/lcode.c @@ -0,0 +1,1203 @@ +/* +** $Id: lcode.c,v 2.112 2016/12/22 13:08:50 roberto Exp $ +** Code generator for Lua +** See Copyright Notice in lua.h +*/ + +#define lcode_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "llex.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstring.h" +#include "ltable.h" +#include "lvm.h" + + +/* Maximum number of registers in a Lua function (must fit in 8 bits) */ +#define MAXREGS 255 + + +#define hasjumps(e) ((e)->t != (e)->f) + + +/* +** If expression is a numeric constant, fills 'v' with its value +** and returns 1. Otherwise, returns 0. +*/ +static int tonumeral(const expdesc *e, TValue *v) { + if (hasjumps(e)) + return 0; /* not a numeral */ + switch (e->k) { + case VKINT: + if (v) setivalue(v, e->u.ival); + return 1; + case VKFLT: + if (v) setfltvalue(v, e->u.nval); + return 1; + default: return 0; + } +} + + +/* +** Create a OP_LOADNIL instruction, but try to optimize: if the previous +** instruction is also OP_LOADNIL and ranges are compatible, adjust +** range of previous instruction instead of emitting a new one. (For +** instance, 'local a; local b' will generate a single opcode.) +*/ +void luaK_nil (FuncState *fs, int from, int n) { + Instruction *previous; + int l = from + n - 1; /* last register to set nil */ + if (fs->pc > fs->lasttarget) { /* no jumps to current position? */ + previous = &fs->f->code[fs->pc-1]; + if (GET_OPCODE(*previous) == OP_LOADNIL) { /* previous is LOADNIL? */ + int pfrom = GETARG_A(*previous); /* get previous range */ + int pl = pfrom + GETARG_B(*previous); + if ((pfrom <= from && from <= pl + 1) || + (from <= pfrom && pfrom <= l + 1)) { /* can connect both? */ + if (pfrom < from) from = pfrom; /* from = min(from, pfrom) */ + if (pl > l) l = pl; /* l = max(l, pl) */ + SETARG_A(*previous, from); + SETARG_B(*previous, l - from); + return; + } + } /* else go through */ + } + luaK_codeABC(fs, OP_LOADNIL, from, n - 1, 0); /* else no optimization */ +} + + +/* +** Gets the destination address of a jump instruction. Used to traverse +** a list of jumps. +*/ +static int getjump (FuncState *fs, int pc) { + int offset = GETARG_sBx(fs->f->code[pc]); + if (offset == NO_JUMP) /* point to itself represents end of list */ + return NO_JUMP; /* end of list */ + else + return (pc+1)+offset; /* turn offset into absolute position */ +} + + +/* +** Fix jump instruction at position 'pc' to jump to 'dest'. +** (Jump addresses are relative in Lua) +*/ +static void fixjump (FuncState *fs, int pc, int dest) { + Instruction *jmp = &fs->f->code[pc]; + int offset = dest - (pc + 1); + lua_assert(dest != NO_JUMP); + if (abs(offset) > MAXARG_sBx) + luaX_syntaxerror(fs->ls, "control structure too long"); + SETARG_sBx(*jmp, offset); +} + + +/* +** Concatenate jump-list 'l2' into jump-list 'l1' +*/ +void luaK_concat (FuncState *fs, int *l1, int l2) { + if (l2 == NO_JUMP) return; /* nothing to concatenate? */ + else if (*l1 == NO_JUMP) /* no original list? */ + *l1 = l2; /* 'l1' points to 'l2' */ + else { + int list = *l1; + int next; + while ((next = getjump(fs, list)) != NO_JUMP) /* find last element */ + list = next; + fixjump(fs, list, l2); /* last element links to 'l2' */ + } +} + + +/* +** Create a jump instruction and return its position, so its destination +** can be fixed later (with 'fixjump'). If there are jumps to +** this position (kept in 'jpc'), link them all together so that +** 'patchlistaux' will fix all them directly to the final destination. +*/ +int luaK_jump (FuncState *fs) { + int jpc = fs->jpc; /* save list of jumps to here */ + int j; + fs->jpc = NO_JUMP; /* no more jumps to here */ + j = luaK_codeAsBx(fs, OP_JMP, 0, NO_JUMP); + luaK_concat(fs, &j, jpc); /* keep them on hold */ + return j; +} + + +/* +** Code a 'return' instruction +*/ +void luaK_ret (FuncState *fs, int first, int nret) { + luaK_codeABC(fs, OP_RETURN, first, nret+1, 0); +} + + +/* +** Code a "conditional jump", that is, a test or comparison opcode +** followed by a jump. Return jump position. +*/ +static int condjump (FuncState *fs, OpCode op, int A, int B, int C) { + luaK_codeABC(fs, op, A, B, C); + return luaK_jump(fs); +} + + +/* +** returns current 'pc' and marks it as a jump target (to avoid wrong +** optimizations with consecutive instructions not in the same basic block). +*/ +int luaK_getlabel (FuncState *fs) { + fs->lasttarget = fs->pc; + return fs->pc; +} + + +/* +** Returns the position of the instruction "controlling" a given +** jump (that is, its condition), or the jump itself if it is +** unconditional. +*/ +static Instruction *getjumpcontrol (FuncState *fs, int pc) { + Instruction *pi = &fs->f->code[pc]; + if (pc >= 1 && testTMode(GET_OPCODE(*(pi-1)))) + return pi-1; + else + return pi; +} + + +/* +** Patch destination register for a TESTSET instruction. +** If instruction in position 'node' is not a TESTSET, return 0 ("fails"). +** Otherwise, if 'reg' is not 'NO_REG', set it as the destination +** register. Otherwise, change instruction to a simple 'TEST' (produces +** no register value) +*/ +static int patchtestreg (FuncState *fs, int node, int reg) { + Instruction *i = getjumpcontrol(fs, node); + if (GET_OPCODE(*i) != OP_TESTSET) + return 0; /* cannot patch other instructions */ + if (reg != NO_REG && reg != GETARG_B(*i)) + SETARG_A(*i, reg); + else { + /* no register to put value or register already has the value; + change instruction to simple test */ + *i = CREATE_ABC(OP_TEST, GETARG_B(*i), 0, GETARG_C(*i)); + } + return 1; +} + + +/* +** Traverse a list of tests ensuring no one produces a value +*/ +static void removevalues (FuncState *fs, int list) { + for (; list != NO_JUMP; list = getjump(fs, list)) + patchtestreg(fs, list, NO_REG); +} + + +/* +** Traverse a list of tests, patching their destination address and +** registers: tests producing values jump to 'vtarget' (and put their +** values in 'reg'), other tests jump to 'dtarget'. +*/ +static void patchlistaux (FuncState *fs, int list, int vtarget, int reg, + int dtarget) { + while (list != NO_JUMP) { + int next = getjump(fs, list); + if (patchtestreg(fs, list, reg)) + fixjump(fs, list, vtarget); + else + fixjump(fs, list, dtarget); /* jump to default target */ + list = next; + } +} + + +/* +** Ensure all pending jumps to current position are fixed (jumping +** to current position with no values) and reset list of pending +** jumps +*/ +static void dischargejpc (FuncState *fs) { + patchlistaux(fs, fs->jpc, fs->pc, NO_REG, fs->pc); + fs->jpc = NO_JUMP; +} + + +/* +** Add elements in 'list' to list of pending jumps to "here" +** (current position) +*/ +void luaK_patchtohere (FuncState *fs, int list) { + luaK_getlabel(fs); /* mark "here" as a jump target */ + luaK_concat(fs, &fs->jpc, list); +} + + +/* +** Path all jumps in 'list' to jump to 'target'. +** (The assert means that we cannot fix a jump to a forward address +** because we only know addresses once code is generated.) +*/ +void luaK_patchlist (FuncState *fs, int list, int target) { + if (target == fs->pc) /* 'target' is current position? */ + luaK_patchtohere(fs, list); /* add list to pending jumps */ + else { + lua_assert(target < fs->pc); + patchlistaux(fs, list, target, NO_REG, target); + } +} + + +/* +** Path all jumps in 'list' to close upvalues up to given 'level' +** (The assertion checks that jumps either were closing nothing +** or were closing higher levels, from inner blocks.) +*/ +void luaK_patchclose (FuncState *fs, int list, int level) { + level++; /* argument is +1 to reserve 0 as non-op */ + for (; list != NO_JUMP; list = getjump(fs, list)) { + lua_assert(GET_OPCODE(fs->f->code[list]) == OP_JMP && + (GETARG_A(fs->f->code[list]) == 0 || + GETARG_A(fs->f->code[list]) >= level)); + SETARG_A(fs->f->code[list], level); + } +} + + +/* +** Emit instruction 'i', checking for array sizes and saving also its +** line information. Return 'i' position. +*/ +static int luaK_code (FuncState *fs, Instruction i) { + Proto *f = fs->f; + dischargejpc(fs); /* 'pc' will change */ + /* put new instruction in code array */ + luaM_growvector(fs->ls->L, f->code, fs->pc, f->sizecode, Instruction, + MAX_INT, "opcodes"); + f->code[fs->pc] = i; + /* save corresponding line information */ + luaM_growvector(fs->ls->L, f->lineinfo, fs->pc, f->sizelineinfo, int, + MAX_INT, "opcodes"); + f->lineinfo[fs->pc] = fs->ls->lastline; + return fs->pc++; +} + + +/* +** Format and emit an 'iABC' instruction. (Assertions check consistency +** of parameters versus opcode.) +*/ +int luaK_codeABC (FuncState *fs, OpCode o, int a, int b, int c) { + lua_assert(getOpMode(o) == iABC); + lua_assert(getBMode(o) != OpArgN || b == 0); + lua_assert(getCMode(o) != OpArgN || c == 0); + lua_assert(a <= MAXARG_A && b <= MAXARG_B && c <= MAXARG_C); + return luaK_code(fs, CREATE_ABC(o, a, b, c)); +} + + +/* +** Format and emit an 'iABx' instruction. +*/ +int luaK_codeABx (FuncState *fs, OpCode o, int a, unsigned int bc) { + lua_assert(getOpMode(o) == iABx || getOpMode(o) == iAsBx); + lua_assert(getCMode(o) == OpArgN); + lua_assert(a <= MAXARG_A && bc <= MAXARG_Bx); + return luaK_code(fs, CREATE_ABx(o, a, bc)); +} + + +/* +** Emit an "extra argument" instruction (format 'iAx') +*/ +static int codeextraarg (FuncState *fs, int a) { + lua_assert(a <= MAXARG_Ax); + return luaK_code(fs, CREATE_Ax(OP_EXTRAARG, a)); +} + + +/* +** Emit a "load constant" instruction, using either 'OP_LOADK' +** (if constant index 'k' fits in 18 bits) or an 'OP_LOADKX' +** instruction with "extra argument". +*/ +int luaK_codek (FuncState *fs, int reg, int k) { + if (k <= MAXARG_Bx) + return luaK_codeABx(fs, OP_LOADK, reg, k); + else { + int p = luaK_codeABx(fs, OP_LOADKX, reg, 0); + codeextraarg(fs, k); + return p; + } +} + + +/* +** Check register-stack level, keeping track of its maximum size +** in field 'maxstacksize' +*/ +void luaK_checkstack (FuncState *fs, int n) { + int newstack = fs->freereg + n; + if (newstack > fs->f->maxstacksize) { + if (newstack >= MAXREGS) + luaX_syntaxerror(fs->ls, + "function or expression needs too many registers"); + fs->f->maxstacksize = cast_byte(newstack); + } +} + + +/* +** Reserve 'n' registers in register stack +*/ +void luaK_reserveregs (FuncState *fs, int n) { + luaK_checkstack(fs, n); + fs->freereg += n; +} + + +/* +** Free register 'reg', if it is neither a constant index nor +** a local variable. +) +*/ +static void freereg (FuncState *fs, int reg) { + if (!ISK(reg) && reg >= fs->nactvar) { + fs->freereg--; + lua_assert(reg == fs->freereg); + } +} + + +/* +** Free register used by expression 'e' (if any) +*/ +static void freeexp (FuncState *fs, expdesc *e) { + if (e->k == VNONRELOC) + freereg(fs, e->u.info); +} + + +/* +** Free registers used by expressions 'e1' and 'e2' (if any) in proper +** order. +*/ +static void freeexps (FuncState *fs, expdesc *e1, expdesc *e2) { + int r1 = (e1->k == VNONRELOC) ? e1->u.info : -1; + int r2 = (e2->k == VNONRELOC) ? e2->u.info : -1; + if (r1 > r2) { + freereg(fs, r1); + freereg(fs, r2); + } + else { + freereg(fs, r2); + freereg(fs, r1); + } +} + + +/* +** Add constant 'v' to prototype's list of constants (field 'k'). +** Use scanner's table to cache position of constants in constant list +** and try to reuse constants. Because some values should not be used +** as keys (nil cannot be a key, integer keys can collapse with float +** keys), the caller must provide a useful 'key' for indexing the cache. +*/ +static int addk (FuncState *fs, TValue *key, TValue *v) { + lua_State *L = fs->ls->L; + Proto *f = fs->f; + TValue *idx = luaH_set(L, fs->ls->h, key); /* index scanner table */ + int k, oldsize; + if (ttisinteger(idx)) { /* is there an index there? */ + k = cast_int(ivalue(idx)); + /* correct value? (warning: must distinguish floats from integers!) */ + if (k < fs->nk && ttype(&f->k[k]) == ttype(v) && + luaV_rawequalobj(&f->k[k], v)) + return k; /* reuse index */ + } + /* constant not found; create a new entry */ + oldsize = f->sizek; + k = fs->nk; + /* numerical value does not need GC barrier; + table has no metatable, so it does not need to invalidate cache */ + setivalue(idx, k); + luaM_growvector(L, f->k, k, f->sizek, TValue, MAXARG_Ax, "constants"); + while (oldsize < f->sizek) setnilvalue(&f->k[oldsize++]); + setobj(L, &f->k[k], v); + fs->nk++; + luaC_barrier(L, f, v); + return k; +} + + +/* +** Add a string to list of constants and return its index. +*/ +int luaK_stringK (FuncState *fs, TString *s) { + TValue o; + setsvalue(fs->ls->L, &o, s); + return addk(fs, &o, &o); /* use string itself as key */ +} + + +/* +** Add an integer to list of constants and return its index. +** Integers use userdata as keys to avoid collision with floats with +** same value; conversion to 'void*' is used only for hashing, so there +** are no "precision" problems. +*/ +int luaK_intK (FuncState *fs, lua_Integer n) { + TValue k, o; + setpvalue(&k, cast(void*, cast(size_t, n))); + setivalue(&o, n); + return addk(fs, &k, &o); +} + +/* +** Add a float to list of constants and return its index. +*/ +static int luaK_numberK (FuncState *fs, lua_Number r) { + TValue o; + setfltvalue(&o, r); + return addk(fs, &o, &o); /* use number itself as key */ +} + + +/* +** Add a boolean to list of constants and return its index. +*/ +static int boolK (FuncState *fs, int b) { + TValue o; + setbvalue(&o, b); + return addk(fs, &o, &o); /* use boolean itself as key */ +} + + +/* +** Add nil to list of constants and return its index. +*/ +static int nilK (FuncState *fs) { + TValue k, v; + setnilvalue(&v); + /* cannot use nil as key; instead use table itself to represent nil */ + sethvalue(fs->ls->L, &k, fs->ls->h); + return addk(fs, &k, &v); +} + + +/* +** Fix an expression to return the number of results 'nresults'. +** Either 'e' is a multi-ret expression (function call or vararg) +** or 'nresults' is LUA_MULTRET (as any expression can satisfy that). +*/ +void luaK_setreturns (FuncState *fs, expdesc *e, int nresults) { + if (e->k == VCALL) { /* expression is an open function call? */ + SETARG_C(getinstruction(fs, e), nresults + 1); + } + else if (e->k == VVARARG) { + Instruction *pc = &getinstruction(fs, e); + SETARG_B(*pc, nresults + 1); + SETARG_A(*pc, fs->freereg); + luaK_reserveregs(fs, 1); + } + else lua_assert(nresults == LUA_MULTRET); +} + + +/* +** Fix an expression to return one result. +** If expression is not a multi-ret expression (function call or +** vararg), it already returns one result, so nothing needs to be done. +** Function calls become VNONRELOC expressions (as its result comes +** fixed in the base register of the call), while vararg expressions +** become VRELOCABLE (as OP_VARARG puts its results where it wants). +** (Calls are created returning one result, so that does not need +** to be fixed.) +*/ +void luaK_setoneret (FuncState *fs, expdesc *e) { + if (e->k == VCALL) { /* expression is an open function call? */ + /* already returns 1 value */ + lua_assert(GETARG_C(getinstruction(fs, e)) == 2); + e->k = VNONRELOC; /* result has fixed position */ + e->u.info = GETARG_A(getinstruction(fs, e)); + } + else if (e->k == VVARARG) { + SETARG_B(getinstruction(fs, e), 2); + e->k = VRELOCABLE; /* can relocate its simple result */ + } +} + + +/* +** Ensure that expression 'e' is not a variable. +*/ +void luaK_dischargevars (FuncState *fs, expdesc *e) { + switch (e->k) { + case VLOCAL: { /* already in a register */ + e->k = VNONRELOC; /* becomes a non-relocatable value */ + break; + } + case VUPVAL: { /* move value to some (pending) register */ + e->u.info = luaK_codeABC(fs, OP_GETUPVAL, 0, e->u.info, 0); + e->k = VRELOCABLE; + break; + } + case VINDEXED: { + OpCode op; + freereg(fs, e->u.ind.idx); + if (e->u.ind.vt == VLOCAL) { /* is 't' in a register? */ + freereg(fs, e->u.ind.t); + op = OP_GETTABLE; + } + else { + lua_assert(e->u.ind.vt == VUPVAL); + op = OP_GETTABUP; /* 't' is in an upvalue */ + } + e->u.info = luaK_codeABC(fs, op, 0, e->u.ind.t, e->u.ind.idx); + e->k = VRELOCABLE; + break; + } + case VVARARG: case VCALL: { + luaK_setoneret(fs, e); + break; + } + default: break; /* there is one value available (somewhere) */ + } +} + + +/* +** Ensures expression value is in register 'reg' (and therefore +** 'e' will become a non-relocatable expression). +*/ +static void discharge2reg (FuncState *fs, expdesc *e, int reg) { + luaK_dischargevars(fs, e); + switch (e->k) { + case VNIL: { + luaK_nil(fs, reg, 1); + break; + } + case VFALSE: case VTRUE: { + luaK_codeABC(fs, OP_LOADBOOL, reg, e->k == VTRUE, 0); + break; + } + case VK: { + luaK_codek(fs, reg, e->u.info); + break; + } + case VKFLT: { + luaK_codek(fs, reg, luaK_numberK(fs, e->u.nval)); + break; + } + case VKINT: { + luaK_codek(fs, reg, luaK_intK(fs, e->u.ival)); + break; + } + case VRELOCABLE: { + Instruction *pc = &getinstruction(fs, e); + SETARG_A(*pc, reg); /* instruction will put result in 'reg' */ + break; + } + case VNONRELOC: { + if (reg != e->u.info) + luaK_codeABC(fs, OP_MOVE, reg, e->u.info, 0); + break; + } + default: { + lua_assert(e->k == VJMP); + return; /* nothing to do... */ + } + } + e->u.info = reg; + e->k = VNONRELOC; +} + + +/* +** Ensures expression value is in any register. +*/ +static void discharge2anyreg (FuncState *fs, expdesc *e) { + if (e->k != VNONRELOC) { /* no fixed register yet? */ + luaK_reserveregs(fs, 1); /* get a register */ + discharge2reg(fs, e, fs->freereg-1); /* put value there */ + } +} + + +static int code_loadbool (FuncState *fs, int A, int b, int jump) { + luaK_getlabel(fs); /* those instructions may be jump targets */ + return luaK_codeABC(fs, OP_LOADBOOL, A, b, jump); +} + + +/* +** check whether list has any jump that do not produce a value +** or produce an inverted value +*/ +static int need_value (FuncState *fs, int list) { + for (; list != NO_JUMP; list = getjump(fs, list)) { + Instruction i = *getjumpcontrol(fs, list); + if (GET_OPCODE(i) != OP_TESTSET) return 1; + } + return 0; /* not found */ +} + + +/* +** Ensures final expression result (including results from its jump +** lists) is in register 'reg'. +** If expression has jumps, need to patch these jumps either to +** its final position or to "load" instructions (for those tests +** that do not produce values). +*/ +static void exp2reg (FuncState *fs, expdesc *e, int reg) { + discharge2reg(fs, e, reg); + if (e->k == VJMP) /* expression itself is a test? */ + luaK_concat(fs, &e->t, e->u.info); /* put this jump in 't' list */ + if (hasjumps(e)) { + int final; /* position after whole expression */ + int p_f = NO_JUMP; /* position of an eventual LOAD false */ + int p_t = NO_JUMP; /* position of an eventual LOAD true */ + if (need_value(fs, e->t) || need_value(fs, e->f)) { + int fj = (e->k == VJMP) ? NO_JUMP : luaK_jump(fs); + p_f = code_loadbool(fs, reg, 0, 1); + p_t = code_loadbool(fs, reg, 1, 0); + luaK_patchtohere(fs, fj); + } + final = luaK_getlabel(fs); + patchlistaux(fs, e->f, final, reg, p_f); + patchlistaux(fs, e->t, final, reg, p_t); + } + e->f = e->t = NO_JUMP; + e->u.info = reg; + e->k = VNONRELOC; +} + + +/* +** Ensures final expression result (including results from its jump +** lists) is in next available register. +*/ +void luaK_exp2nextreg (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + freeexp(fs, e); + luaK_reserveregs(fs, 1); + exp2reg(fs, e, fs->freereg - 1); +} + + +/* +** Ensures final expression result (including results from its jump +** lists) is in some (any) register and return that register. +*/ +int luaK_exp2anyreg (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + if (e->k == VNONRELOC) { /* expression already has a register? */ + if (!hasjumps(e)) /* no jumps? */ + return e->u.info; /* result is already in a register */ + if (e->u.info >= fs->nactvar) { /* reg. is not a local? */ + exp2reg(fs, e, e->u.info); /* put final result in it */ + return e->u.info; + } + } + luaK_exp2nextreg(fs, e); /* otherwise, use next available register */ + return e->u.info; +} + + +/* +** Ensures final expression result is either in a register or in an +** upvalue. +*/ +void luaK_exp2anyregup (FuncState *fs, expdesc *e) { + if (e->k != VUPVAL || hasjumps(e)) + luaK_exp2anyreg(fs, e); +} + + +/* +** Ensures final expression result is either in a register or it is +** a constant. +*/ +void luaK_exp2val (FuncState *fs, expdesc *e) { + if (hasjumps(e)) + luaK_exp2anyreg(fs, e); + else + luaK_dischargevars(fs, e); +} + + +/* +** Ensures final expression result is in a valid R/K index +** (that is, it is either in a register or in 'k' with an index +** in the range of R/K indices). +** Returns R/K index. +*/ +int luaK_exp2RK (FuncState *fs, expdesc *e) { + luaK_exp2val(fs, e); + switch (e->k) { /* move constants to 'k' */ + case VTRUE: e->u.info = boolK(fs, 1); goto vk; + case VFALSE: e->u.info = boolK(fs, 0); goto vk; + case VNIL: e->u.info = nilK(fs); goto vk; + case VKINT: e->u.info = luaK_intK(fs, e->u.ival); goto vk; + case VKFLT: e->u.info = luaK_numberK(fs, e->u.nval); goto vk; + case VK: + vk: + e->k = VK; + if (e->u.info <= MAXINDEXRK) /* constant fits in 'argC'? */ + return RKASK(e->u.info); + else break; + default: break; + } + /* not a constant in the right range: put it in a register */ + return luaK_exp2anyreg(fs, e); +} + + +/* +** Generate code to store result of expression 'ex' into variable 'var'. +*/ +void luaK_storevar (FuncState *fs, expdesc *var, expdesc *ex) { + switch (var->k) { + case VLOCAL: { + freeexp(fs, ex); + exp2reg(fs, ex, var->u.info); /* compute 'ex' into proper place */ + return; + } + case VUPVAL: { + int e = luaK_exp2anyreg(fs, ex); + luaK_codeABC(fs, OP_SETUPVAL, e, var->u.info, 0); + break; + } + case VINDEXED: { + OpCode op = (var->u.ind.vt == VLOCAL) ? OP_SETTABLE : OP_SETTABUP; + int e = luaK_exp2RK(fs, ex); + luaK_codeABC(fs, op, var->u.ind.t, var->u.ind.idx, e); + break; + } + default: lua_assert(0); /* invalid var kind to store */ + } + freeexp(fs, ex); +} + + +/* +** Emit SELF instruction (convert expression 'e' into 'e:key(e,'). +*/ +void luaK_self (FuncState *fs, expdesc *e, expdesc *key) { + int ereg; + luaK_exp2anyreg(fs, e); + ereg = e->u.info; /* register where 'e' was placed */ + freeexp(fs, e); + e->u.info = fs->freereg; /* base register for op_self */ + e->k = VNONRELOC; /* self expression has a fixed register */ + luaK_reserveregs(fs, 2); /* function and 'self' produced by op_self */ + luaK_codeABC(fs, OP_SELF, e->u.info, ereg, luaK_exp2RK(fs, key)); + freeexp(fs, key); +} + + +/* +** Negate condition 'e' (where 'e' is a comparison). +*/ +static void negatecondition (FuncState *fs, expdesc *e) { + Instruction *pc = getjumpcontrol(fs, e->u.info); + lua_assert(testTMode(GET_OPCODE(*pc)) && GET_OPCODE(*pc) != OP_TESTSET && + GET_OPCODE(*pc) != OP_TEST); + SETARG_A(*pc, !(GETARG_A(*pc))); +} + + +/* +** Emit instruction to jump if 'e' is 'cond' (that is, if 'cond' +** is true, code will jump if 'e' is true.) Return jump position. +** Optimize when 'e' is 'not' something, inverting the condition +** and removing the 'not'. +*/ +static int jumponcond (FuncState *fs, expdesc *e, int cond) { + if (e->k == VRELOCABLE) { + Instruction ie = getinstruction(fs, e); + if (GET_OPCODE(ie) == OP_NOT) { + fs->pc--; /* remove previous OP_NOT */ + return condjump(fs, OP_TEST, GETARG_B(ie), 0, !cond); + } + /* else go through */ + } + discharge2anyreg(fs, e); + freeexp(fs, e); + return condjump(fs, OP_TESTSET, NO_REG, e->u.info, cond); +} + + +/* +** Emit code to go through if 'e' is true, jump otherwise. +*/ +void luaK_goiftrue (FuncState *fs, expdesc *e) { + int pc; /* pc of new jump */ + luaK_dischargevars(fs, e); + switch (e->k) { + case VJMP: { /* condition? */ + negatecondition(fs, e); /* jump when it is false */ + pc = e->u.info; /* save jump position */ + break; + } + case VK: case VKFLT: case VKINT: case VTRUE: { + pc = NO_JUMP; /* always true; do nothing */ + break; + } + default: { + pc = jumponcond(fs, e, 0); /* jump when false */ + break; + } + } + luaK_concat(fs, &e->f, pc); /* insert new jump in false list */ + luaK_patchtohere(fs, e->t); /* true list jumps to here (to go through) */ + e->t = NO_JUMP; +} + + +/* +** Emit code to go through if 'e' is false, jump otherwise. +*/ +void luaK_goiffalse (FuncState *fs, expdesc *e) { + int pc; /* pc of new jump */ + luaK_dischargevars(fs, e); + switch (e->k) { + case VJMP: { + pc = e->u.info; /* already jump if true */ + break; + } + case VNIL: case VFALSE: { + pc = NO_JUMP; /* always false; do nothing */ + break; + } + default: { + pc = jumponcond(fs, e, 1); /* jump if true */ + break; + } + } + luaK_concat(fs, &e->t, pc); /* insert new jump in 't' list */ + luaK_patchtohere(fs, e->f); /* false list jumps to here (to go through) */ + e->f = NO_JUMP; +} + + +/* +** Code 'not e', doing constant folding. +*/ +static void codenot (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + switch (e->k) { + case VNIL: case VFALSE: { + e->k = VTRUE; /* true == not nil == not false */ + break; + } + case VK: case VKFLT: case VKINT: case VTRUE: { + e->k = VFALSE; /* false == not "x" == not 0.5 == not 1 == not true */ + break; + } + case VJMP: { + negatecondition(fs, e); + break; + } + case VRELOCABLE: + case VNONRELOC: { + discharge2anyreg(fs, e); + freeexp(fs, e); + e->u.info = luaK_codeABC(fs, OP_NOT, 0, e->u.info, 0); + e->k = VRELOCABLE; + break; + } + default: lua_assert(0); /* cannot happen */ + } + /* interchange true and false lists */ + { int temp = e->f; e->f = e->t; e->t = temp; } + removevalues(fs, e->f); /* values are useless when negated */ + removevalues(fs, e->t); +} + + +/* +** Create expression 't[k]'. 't' must have its final result already in a +** register or upvalue. +*/ +void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k) { + lua_assert(!hasjumps(t) && (vkisinreg(t->k) || t->k == VUPVAL)); + t->u.ind.t = t->u.info; /* register or upvalue index */ + t->u.ind.idx = luaK_exp2RK(fs, k); /* R/K index for key */ + t->u.ind.vt = (t->k == VUPVAL) ? VUPVAL : VLOCAL; + t->k = VINDEXED; +} + + +/* +** Return false if folding can raise an error. +** Bitwise operations need operands convertible to integers; division +** operations cannot have 0 as divisor. +*/ +static int validop (int op, TValue *v1, TValue *v2) { + switch (op) { + case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR: + case LUA_OPSHL: case LUA_OPSHR: case LUA_OPBNOT: { /* conversion errors */ + lua_Integer i; + return (tointeger(v1, &i) && tointeger(v2, &i)); + } + case LUA_OPDIV: case LUA_OPIDIV: case LUA_OPMOD: /* division by 0 */ + return (nvalue(v2) != 0); + default: return 1; /* everything else is valid */ + } +} + + +/* +** Try to "constant-fold" an operation; return 1 iff successful. +** (In this case, 'e1' has the final result.) +*/ +static int constfolding (FuncState *fs, int op, expdesc *e1, + const expdesc *e2) { + TValue v1, v2, res; + if (!tonumeral(e1, &v1) || !tonumeral(e2, &v2) || !validop(op, &v1, &v2)) + return 0; /* non-numeric operands or not safe to fold */ + luaO_arith(fs->ls->L, op, &v1, &v2, &res); /* does operation */ + if (ttisinteger(&res)) { + e1->k = VKINT; + e1->u.ival = ivalue(&res); + } + else { /* folds neither NaN nor 0.0 (to avoid problems with -0.0) */ + lua_Number n = fltvalue(&res); + if (luai_numisnan(n) || n == 0) + return 0; + e1->k = VKFLT; + e1->u.nval = n; + } + return 1; +} + + +/* +** Emit code for unary expressions that "produce values" +** (everything but 'not'). +** Expression to produce final result will be encoded in 'e'. +*/ +static void codeunexpval (FuncState *fs, OpCode op, expdesc *e, int line) { + int r = luaK_exp2anyreg(fs, e); /* opcodes operate only on registers */ + freeexp(fs, e); + e->u.info = luaK_codeABC(fs, op, 0, r, 0); /* generate opcode */ + e->k = VRELOCABLE; /* all those operations are relocatable */ + luaK_fixline(fs, line); +} + + +/* +** Emit code for binary expressions that "produce values" +** (everything but logical operators 'and'/'or' and comparison +** operators). +** Expression to produce final result will be encoded in 'e1'. +** Because 'luaK_exp2RK' can free registers, its calls must be +** in "stack order" (that is, first on 'e2', which may have more +** recent registers to be released). +*/ +static void codebinexpval (FuncState *fs, OpCode op, + expdesc *e1, expdesc *e2, int line) { + int rk2 = luaK_exp2RK(fs, e2); /* both operands are "RK" */ + int rk1 = luaK_exp2RK(fs, e1); + freeexps(fs, e1, e2); + e1->u.info = luaK_codeABC(fs, op, 0, rk1, rk2); /* generate opcode */ + e1->k = VRELOCABLE; /* all those operations are relocatable */ + luaK_fixline(fs, line); +} + + +/* +** Emit code for comparisons. +** 'e1' was already put in R/K form by 'luaK_infix'. +*/ +static void codecomp (FuncState *fs, BinOpr opr, expdesc *e1, expdesc *e2) { + int rk1 = (e1->k == VK) ? RKASK(e1->u.info) + : check_exp(e1->k == VNONRELOC, e1->u.info); + int rk2 = luaK_exp2RK(fs, e2); + freeexps(fs, e1, e2); + switch (opr) { + case OPR_NE: { /* '(a ~= b)' ==> 'not (a == b)' */ + e1->u.info = condjump(fs, OP_EQ, 0, rk1, rk2); + break; + } + case OPR_GT: case OPR_GE: { + /* '(a > b)' ==> '(b < a)'; '(a >= b)' ==> '(b <= a)' */ + OpCode op = cast(OpCode, (opr - OPR_NE) + OP_EQ); + e1->u.info = condjump(fs, op, 1, rk2, rk1); /* invert operands */ + break; + } + default: { /* '==', '<', '<=' use their own opcodes */ + OpCode op = cast(OpCode, (opr - OPR_EQ) + OP_EQ); + e1->u.info = condjump(fs, op, 1, rk1, rk2); + break; + } + } + e1->k = VJMP; +} + + +/* +** Aplly prefix operation 'op' to expression 'e'. +*/ +void luaK_prefix (FuncState *fs, UnOpr op, expdesc *e, int line) { + static const expdesc ef = {VKINT, {0}, NO_JUMP, NO_JUMP}; + switch (op) { + case OPR_MINUS: case OPR_BNOT: /* use 'ef' as fake 2nd operand */ + if (constfolding(fs, op + LUA_OPUNM, e, &ef)) + break; + /* FALLTHROUGH */ + case OPR_LEN: + codeunexpval(fs, cast(OpCode, op + OP_UNM), e, line); + break; + case OPR_NOT: codenot(fs, e); break; + default: lua_assert(0); + } +} + + +/* +** Process 1st operand 'v' of binary operation 'op' before reading +** 2nd operand. +*/ +void luaK_infix (FuncState *fs, BinOpr op, expdesc *v) { + switch (op) { + case OPR_AND: { + luaK_goiftrue(fs, v); /* go ahead only if 'v' is true */ + break; + } + case OPR_OR: { + luaK_goiffalse(fs, v); /* go ahead only if 'v' is false */ + break; + } + case OPR_CONCAT: { + luaK_exp2nextreg(fs, v); /* operand must be on the 'stack' */ + break; + } + case OPR_ADD: case OPR_SUB: + case OPR_MUL: case OPR_DIV: case OPR_IDIV: + case OPR_MOD: case OPR_POW: + case OPR_BAND: case OPR_BOR: case OPR_BXOR: + case OPR_SHL: case OPR_SHR: { + if (!tonumeral(v, NULL)) + luaK_exp2RK(fs, v); + /* else keep numeral, which may be folded with 2nd operand */ + break; + } + default: { + luaK_exp2RK(fs, v); + break; + } + } +} + + +/* +** Finalize code for binary operation, after reading 2nd operand. +** For '(a .. b .. c)' (which is '(a .. (b .. c))', because +** concatenation is right associative), merge second CONCAT into first +** one. +*/ +void luaK_posfix (FuncState *fs, BinOpr op, + expdesc *e1, expdesc *e2, int line) { + switch (op) { + case OPR_AND: { + lua_assert(e1->t == NO_JUMP); /* list closed by 'luK_infix' */ + luaK_dischargevars(fs, e2); + luaK_concat(fs, &e2->f, e1->f); + *e1 = *e2; + break; + } + case OPR_OR: { + lua_assert(e1->f == NO_JUMP); /* list closed by 'luK_infix' */ + luaK_dischargevars(fs, e2); + luaK_concat(fs, &e2->t, e1->t); + *e1 = *e2; + break; + } + case OPR_CONCAT: { + luaK_exp2val(fs, e2); + if (e2->k == VRELOCABLE && + GET_OPCODE(getinstruction(fs, e2)) == OP_CONCAT) { + lua_assert(e1->u.info == GETARG_B(getinstruction(fs, e2))-1); + freeexp(fs, e1); + SETARG_B(getinstruction(fs, e2), e1->u.info); + e1->k = VRELOCABLE; e1->u.info = e2->u.info; + } + else { + luaK_exp2nextreg(fs, e2); /* operand must be on the 'stack' */ + codebinexpval(fs, OP_CONCAT, e1, e2, line); + } + break; + } + case OPR_ADD: case OPR_SUB: case OPR_MUL: case OPR_DIV: + case OPR_IDIV: case OPR_MOD: case OPR_POW: + case OPR_BAND: case OPR_BOR: case OPR_BXOR: + case OPR_SHL: case OPR_SHR: { + if (!constfolding(fs, op + LUA_OPADD, e1, e2)) + codebinexpval(fs, cast(OpCode, op + OP_ADD), e1, e2, line); + break; + } + case OPR_EQ: case OPR_LT: case OPR_LE: + case OPR_NE: case OPR_GT: case OPR_GE: { + codecomp(fs, op, e1, e2); + break; + } + default: lua_assert(0); + } +} + + +/* +** Change line information associated with current position. +*/ +void luaK_fixline (FuncState *fs, int line) { + fs->f->lineinfo[fs->pc - 1] = line; +} + + +/* +** Emit a SETLIST instruction. +** 'base' is register that keeps table; +** 'nelems' is #table plus those to be stored now; +** 'tostore' is number of values (in registers 'base + 1',...) to add to +** table (or LUA_MULTRET to add up to stack top). +*/ +void luaK_setlist (FuncState *fs, int base, int nelems, int tostore) { + int c = (nelems - 1)/LFIELDS_PER_FLUSH + 1; + int b = (tostore == LUA_MULTRET) ? 0 : tostore; + lua_assert(tostore != 0 && tostore <= LFIELDS_PER_FLUSH); + if (c <= MAXARG_C) + luaK_codeABC(fs, OP_SETLIST, base, b, c); + else if (c <= MAXARG_Ax) { + luaK_codeABC(fs, OP_SETLIST, base, b, 0); + codeextraarg(fs, c); + } + else + luaX_syntaxerror(fs->ls, "constructor too long"); + fs->freereg = base + 1; /* free registers with list values */ +} + diff --git a/src/rcheevos/test/lua/src/lcode.h b/src/rcheevos/test/lua/src/lcode.h new file mode 100644 index 000000000..cd306d573 --- /dev/null +++ b/src/rcheevos/test/lua/src/lcode.h @@ -0,0 +1,88 @@ +/* +** $Id: lcode.h,v 1.64 2016/01/05 16:22:37 roberto Exp $ +** Code generator for Lua +** See Copyright Notice in lua.h +*/ + +#ifndef lcode_h +#define lcode_h + +#include "llex.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" + + +/* +** Marks the end of a patch list. It is an invalid value both as an absolute +** address, and as a list link (would link an element to itself). +*/ +#define NO_JUMP (-1) + + +/* +** grep "ORDER OPR" if you change these enums (ORDER OP) +*/ +typedef enum BinOpr { + OPR_ADD, OPR_SUB, OPR_MUL, OPR_MOD, OPR_POW, + OPR_DIV, + OPR_IDIV, + OPR_BAND, OPR_BOR, OPR_BXOR, + OPR_SHL, OPR_SHR, + OPR_CONCAT, + OPR_EQ, OPR_LT, OPR_LE, + OPR_NE, OPR_GT, OPR_GE, + OPR_AND, OPR_OR, + OPR_NOBINOPR +} BinOpr; + + +typedef enum UnOpr { OPR_MINUS, OPR_BNOT, OPR_NOT, OPR_LEN, OPR_NOUNOPR } UnOpr; + + +/* get (pointer to) instruction of given 'expdesc' */ +#define getinstruction(fs,e) ((fs)->f->code[(e)->u.info]) + +#define luaK_codeAsBx(fs,o,A,sBx) luaK_codeABx(fs,o,A,(sBx)+MAXARG_sBx) + +#define luaK_setmultret(fs,e) luaK_setreturns(fs, e, LUA_MULTRET) + +#define luaK_jumpto(fs,t) luaK_patchlist(fs, luaK_jump(fs), t) + +LUAI_FUNC int luaK_codeABx (FuncState *fs, OpCode o, int A, unsigned int Bx); +LUAI_FUNC int luaK_codeABC (FuncState *fs, OpCode o, int A, int B, int C); +LUAI_FUNC int luaK_codek (FuncState *fs, int reg, int k); +LUAI_FUNC void luaK_fixline (FuncState *fs, int line); +LUAI_FUNC void luaK_nil (FuncState *fs, int from, int n); +LUAI_FUNC void luaK_reserveregs (FuncState *fs, int n); +LUAI_FUNC void luaK_checkstack (FuncState *fs, int n); +LUAI_FUNC int luaK_stringK (FuncState *fs, TString *s); +LUAI_FUNC int luaK_intK (FuncState *fs, lua_Integer n); +LUAI_FUNC void luaK_dischargevars (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_exp2anyreg (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2anyregup (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2nextreg (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2val (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_exp2RK (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_self (FuncState *fs, expdesc *e, expdesc *key); +LUAI_FUNC void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k); +LUAI_FUNC void luaK_goiftrue (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_goiffalse (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_storevar (FuncState *fs, expdesc *var, expdesc *e); +LUAI_FUNC void luaK_setreturns (FuncState *fs, expdesc *e, int nresults); +LUAI_FUNC void luaK_setoneret (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_jump (FuncState *fs); +LUAI_FUNC void luaK_ret (FuncState *fs, int first, int nret); +LUAI_FUNC void luaK_patchlist (FuncState *fs, int list, int target); +LUAI_FUNC void luaK_patchtohere (FuncState *fs, int list); +LUAI_FUNC void luaK_patchclose (FuncState *fs, int list, int level); +LUAI_FUNC void luaK_concat (FuncState *fs, int *l1, int l2); +LUAI_FUNC int luaK_getlabel (FuncState *fs); +LUAI_FUNC void luaK_prefix (FuncState *fs, UnOpr op, expdesc *v, int line); +LUAI_FUNC void luaK_infix (FuncState *fs, BinOpr op, expdesc *v); +LUAI_FUNC void luaK_posfix (FuncState *fs, BinOpr op, expdesc *v1, + expdesc *v2, int line); +LUAI_FUNC void luaK_setlist (FuncState *fs, int base, int nelems, int tostore); + + +#endif diff --git a/src/rcheevos/test/lua/src/lcorolib.c b/src/rcheevos/test/lua/src/lcorolib.c new file mode 100644 index 000000000..2303429e7 --- /dev/null +++ b/src/rcheevos/test/lua/src/lcorolib.c @@ -0,0 +1,168 @@ +/* +** $Id: lcorolib.c,v 1.10 2016/04/11 19:19:55 roberto Exp $ +** Coroutine Library +** See Copyright Notice in lua.h +*/ + +#define lcorolib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +static lua_State *getco (lua_State *L) { + lua_State *co = lua_tothread(L, 1); + luaL_argcheck(L, co, 1, "thread expected"); + return co; +} + + +static int auxresume (lua_State *L, lua_State *co, int narg) { + int status; + if (!lua_checkstack(co, narg)) { + lua_pushliteral(L, "too many arguments to resume"); + return -1; /* error flag */ + } + if (lua_status(co) == LUA_OK && lua_gettop(co) == 0) { + lua_pushliteral(L, "cannot resume dead coroutine"); + return -1; /* error flag */ + } + lua_xmove(L, co, narg); + status = lua_resume(co, L, narg); + if (status == LUA_OK || status == LUA_YIELD) { + int nres = lua_gettop(co); + if (!lua_checkstack(L, nres + 1)) { + lua_pop(co, nres); /* remove results anyway */ + lua_pushliteral(L, "too many results to resume"); + return -1; /* error flag */ + } + lua_xmove(co, L, nres); /* move yielded values */ + return nres; + } + else { + lua_xmove(co, L, 1); /* move error message */ + return -1; /* error flag */ + } +} + + +static int luaB_coresume (lua_State *L) { + lua_State *co = getco(L); + int r; + r = auxresume(L, co, lua_gettop(L) - 1); + if (r < 0) { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; /* return false + error message */ + } + else { + lua_pushboolean(L, 1); + lua_insert(L, -(r + 1)); + return r + 1; /* return true + 'resume' returns */ + } +} + + +static int luaB_auxwrap (lua_State *L) { + lua_State *co = lua_tothread(L, lua_upvalueindex(1)); + int r = auxresume(L, co, lua_gettop(L)); + if (r < 0) { + if (lua_type(L, -1) == LUA_TSTRING) { /* error object is a string? */ + luaL_where(L, 1); /* add extra info */ + lua_insert(L, -2); + lua_concat(L, 2); + } + return lua_error(L); /* propagate error */ + } + return r; +} + + +static int luaB_cocreate (lua_State *L) { + lua_State *NL; + luaL_checktype(L, 1, LUA_TFUNCTION); + NL = lua_newthread(L); + lua_pushvalue(L, 1); /* move function to top */ + lua_xmove(L, NL, 1); /* move function from L to NL */ + return 1; +} + + +static int luaB_cowrap (lua_State *L) { + luaB_cocreate(L); + lua_pushcclosure(L, luaB_auxwrap, 1); + return 1; +} + + +static int luaB_yield (lua_State *L) { + return lua_yield(L, lua_gettop(L)); +} + + +static int luaB_costatus (lua_State *L) { + lua_State *co = getco(L); + if (L == co) lua_pushliteral(L, "running"); + else { + switch (lua_status(co)) { + case LUA_YIELD: + lua_pushliteral(L, "suspended"); + break; + case LUA_OK: { + lua_Debug ar; + if (lua_getstack(co, 0, &ar) > 0) /* does it have frames? */ + lua_pushliteral(L, "normal"); /* it is running */ + else if (lua_gettop(co) == 0) + lua_pushliteral(L, "dead"); + else + lua_pushliteral(L, "suspended"); /* initial state */ + break; + } + default: /* some error occurred */ + lua_pushliteral(L, "dead"); + break; + } + } + return 1; +} + + +static int luaB_yieldable (lua_State *L) { + lua_pushboolean(L, lua_isyieldable(L)); + return 1; +} + + +static int luaB_corunning (lua_State *L) { + int ismain = lua_pushthread(L); + lua_pushboolean(L, ismain); + return 2; +} + + +static const luaL_Reg co_funcs[] = { + {"create", luaB_cocreate}, + {"resume", luaB_coresume}, + {"running", luaB_corunning}, + {"status", luaB_costatus}, + {"wrap", luaB_cowrap}, + {"yield", luaB_yield}, + {"isyieldable", luaB_yieldable}, + {NULL, NULL} +}; + + + +LUAMOD_API int luaopen_coroutine (lua_State *L) { + luaL_newlib(L, co_funcs); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/lctype.c b/src/rcheevos/test/lua/src/lctype.c new file mode 100644 index 000000000..ae9367e69 --- /dev/null +++ b/src/rcheevos/test/lua/src/lctype.c @@ -0,0 +1,55 @@ +/* +** $Id: lctype.c,v 1.12 2014/11/02 19:19:04 roberto Exp $ +** 'ctype' functions for Lua +** See Copyright Notice in lua.h +*/ + +#define lctype_c +#define LUA_CORE + +#include "lprefix.h" + + +#include "lctype.h" + +#if !LUA_USE_CTYPE /* { */ + +#include + +LUAI_DDEF const lu_byte luai_ctype_[UCHAR_MAX + 2] = { + 0x00, /* EOZ */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0. */ + 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, /* 2. */ + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, /* 3. */ + 0x16, 0x16, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 4. */ + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 5. */ + 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x05, + 0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 6. */ + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 7. */ + 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 8. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 9. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* a. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* b. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* c. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* d. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* e. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* f. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#endif /* } */ diff --git a/src/rcheevos/test/lua/src/lctype.h b/src/rcheevos/test/lua/src/lctype.h new file mode 100644 index 000000000..99c7d1223 --- /dev/null +++ b/src/rcheevos/test/lua/src/lctype.h @@ -0,0 +1,95 @@ +/* +** $Id: lctype.h,v 1.12 2011/07/15 12:50:29 roberto Exp $ +** 'ctype' functions for Lua +** See Copyright Notice in lua.h +*/ + +#ifndef lctype_h +#define lctype_h + +#include "lua.h" + + +/* +** WARNING: the functions defined here do not necessarily correspond +** to the similar functions in the standard C ctype.h. They are +** optimized for the specific needs of Lua +*/ + +#if !defined(LUA_USE_CTYPE) + +#if 'A' == 65 && '0' == 48 +/* ASCII case: can use its own tables; faster and fixed */ +#define LUA_USE_CTYPE 0 +#else +/* must use standard C ctype */ +#define LUA_USE_CTYPE 1 +#endif + +#endif + + +#if !LUA_USE_CTYPE /* { */ + +#include + +#include "llimits.h" + + +#define ALPHABIT 0 +#define DIGITBIT 1 +#define PRINTBIT 2 +#define SPACEBIT 3 +#define XDIGITBIT 4 + + +#define MASK(B) (1 << (B)) + + +/* +** add 1 to char to allow index -1 (EOZ) +*/ +#define testprop(c,p) (luai_ctype_[(c)+1] & (p)) + +/* +** 'lalpha' (Lua alphabetic) and 'lalnum' (Lua alphanumeric) both include '_' +*/ +#define lislalpha(c) testprop(c, MASK(ALPHABIT)) +#define lislalnum(c) testprop(c, (MASK(ALPHABIT) | MASK(DIGITBIT))) +#define lisdigit(c) testprop(c, MASK(DIGITBIT)) +#define lisspace(c) testprop(c, MASK(SPACEBIT)) +#define lisprint(c) testprop(c, MASK(PRINTBIT)) +#define lisxdigit(c) testprop(c, MASK(XDIGITBIT)) + +/* +** this 'ltolower' only works for alphabetic characters +*/ +#define ltolower(c) ((c) | ('A' ^ 'a')) + + +/* two more entries for 0 and -1 (EOZ) */ +LUAI_DDEC const lu_byte luai_ctype_[UCHAR_MAX + 2]; + + +#else /* }{ */ + +/* +** use standard C ctypes +*/ + +#include + + +#define lislalpha(c) (isalpha(c) || (c) == '_') +#define lislalnum(c) (isalnum(c) || (c) == '_') +#define lisdigit(c) (isdigit(c)) +#define lisspace(c) (isspace(c)) +#define lisprint(c) (isprint(c)) +#define lisxdigit(c) (isxdigit(c)) + +#define ltolower(c) (tolower(c)) + +#endif /* } */ + +#endif + diff --git a/src/rcheevos/test/lua/src/ldblib.c b/src/rcheevos/test/lua/src/ldblib.c new file mode 100644 index 000000000..786f6cd95 --- /dev/null +++ b/src/rcheevos/test/lua/src/ldblib.c @@ -0,0 +1,456 @@ +/* +** $Id: ldblib.c,v 1.151 2015/11/23 11:29:43 roberto Exp $ +** Interface from Lua to its debug API +** See Copyright Notice in lua.h +*/ + +#define ldblib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* +** The hook table at registry[&HOOKKEY] maps threads to their current +** hook function. (We only need the unique address of 'HOOKKEY'.) +*/ +static const int HOOKKEY = 0; + + +/* +** If L1 != L, L1 can be in any state, and therefore there are no +** guarantees about its stack space; any push in L1 must be +** checked. +*/ +static void checkstack (lua_State *L, lua_State *L1, int n) { + if (L != L1 && !lua_checkstack(L1, n)) + luaL_error(L, "stack overflow"); +} + + +static int db_getregistry (lua_State *L) { + lua_pushvalue(L, LUA_REGISTRYINDEX); + return 1; +} + + +static int db_getmetatable (lua_State *L) { + luaL_checkany(L, 1); + if (!lua_getmetatable(L, 1)) { + lua_pushnil(L); /* no metatable */ + } + return 1; +} + + +static int db_setmetatable (lua_State *L) { + int t = lua_type(L, 2); + luaL_argcheck(L, t == LUA_TNIL || t == LUA_TTABLE, 2, + "nil or table expected"); + lua_settop(L, 2); + lua_setmetatable(L, 1); + return 1; /* return 1st argument */ +} + + +static int db_getuservalue (lua_State *L) { + if (lua_type(L, 1) != LUA_TUSERDATA) + lua_pushnil(L); + else + lua_getuservalue(L, 1); + return 1; +} + + +static int db_setuservalue (lua_State *L) { + luaL_checktype(L, 1, LUA_TUSERDATA); + luaL_checkany(L, 2); + lua_settop(L, 2); + lua_setuservalue(L, 1); + return 1; +} + + +/* +** Auxiliary function used by several library functions: check for +** an optional thread as function's first argument and set 'arg' with +** 1 if this argument is present (so that functions can skip it to +** access their other arguments) +*/ +static lua_State *getthread (lua_State *L, int *arg) { + if (lua_isthread(L, 1)) { + *arg = 1; + return lua_tothread(L, 1); + } + else { + *arg = 0; + return L; /* function will operate over current thread */ + } +} + + +/* +** Variations of 'lua_settable', used by 'db_getinfo' to put results +** from 'lua_getinfo' into result table. Key is always a string; +** value can be a string, an int, or a boolean. +*/ +static void settabss (lua_State *L, const char *k, const char *v) { + lua_pushstring(L, v); + lua_setfield(L, -2, k); +} + +static void settabsi (lua_State *L, const char *k, int v) { + lua_pushinteger(L, v); + lua_setfield(L, -2, k); +} + +static void settabsb (lua_State *L, const char *k, int v) { + lua_pushboolean(L, v); + lua_setfield(L, -2, k); +} + + +/* +** In function 'db_getinfo', the call to 'lua_getinfo' may push +** results on the stack; later it creates the result table to put +** these objects. Function 'treatstackoption' puts the result from +** 'lua_getinfo' on top of the result table so that it can call +** 'lua_setfield'. +*/ +static void treatstackoption (lua_State *L, lua_State *L1, const char *fname) { + if (L == L1) + lua_rotate(L, -2, 1); /* exchange object and table */ + else + lua_xmove(L1, L, 1); /* move object to the "main" stack */ + lua_setfield(L, -2, fname); /* put object into table */ +} + + +/* +** Calls 'lua_getinfo' and collects all results in a new table. +** L1 needs stack space for an optional input (function) plus +** two optional outputs (function and line table) from function +** 'lua_getinfo'. +*/ +static int db_getinfo (lua_State *L) { + lua_Debug ar; + int arg; + lua_State *L1 = getthread(L, &arg); + const char *options = luaL_optstring(L, arg+2, "flnStu"); + checkstack(L, L1, 3); + if (lua_isfunction(L, arg + 1)) { /* info about a function? */ + options = lua_pushfstring(L, ">%s", options); /* add '>' to 'options' */ + lua_pushvalue(L, arg + 1); /* move function to 'L1' stack */ + lua_xmove(L, L1, 1); + } + else { /* stack level */ + if (!lua_getstack(L1, (int)luaL_checkinteger(L, arg + 1), &ar)) { + lua_pushnil(L); /* level out of range */ + return 1; + } + } + if (!lua_getinfo(L1, options, &ar)) + return luaL_argerror(L, arg+2, "invalid option"); + lua_newtable(L); /* table to collect results */ + if (strchr(options, 'S')) { + settabss(L, "source", ar.source); + settabss(L, "short_src", ar.short_src); + settabsi(L, "linedefined", ar.linedefined); + settabsi(L, "lastlinedefined", ar.lastlinedefined); + settabss(L, "what", ar.what); + } + if (strchr(options, 'l')) + settabsi(L, "currentline", ar.currentline); + if (strchr(options, 'u')) { + settabsi(L, "nups", ar.nups); + settabsi(L, "nparams", ar.nparams); + settabsb(L, "isvararg", ar.isvararg); + } + if (strchr(options, 'n')) { + settabss(L, "name", ar.name); + settabss(L, "namewhat", ar.namewhat); + } + if (strchr(options, 't')) + settabsb(L, "istailcall", ar.istailcall); + if (strchr(options, 'L')) + treatstackoption(L, L1, "activelines"); + if (strchr(options, 'f')) + treatstackoption(L, L1, "func"); + return 1; /* return table */ +} + + +static int db_getlocal (lua_State *L) { + int arg; + lua_State *L1 = getthread(L, &arg); + lua_Debug ar; + const char *name; + int nvar = (int)luaL_checkinteger(L, arg + 2); /* local-variable index */ + if (lua_isfunction(L, arg + 1)) { /* function argument? */ + lua_pushvalue(L, arg + 1); /* push function */ + lua_pushstring(L, lua_getlocal(L, NULL, nvar)); /* push local name */ + return 1; /* return only name (there is no value) */ + } + else { /* stack-level argument */ + int level = (int)luaL_checkinteger(L, arg + 1); + if (!lua_getstack(L1, level, &ar)) /* out of range? */ + return luaL_argerror(L, arg+1, "level out of range"); + checkstack(L, L1, 1); + name = lua_getlocal(L1, &ar, nvar); + if (name) { + lua_xmove(L1, L, 1); /* move local value */ + lua_pushstring(L, name); /* push name */ + lua_rotate(L, -2, 1); /* re-order */ + return 2; + } + else { + lua_pushnil(L); /* no name (nor value) */ + return 1; + } + } +} + + +static int db_setlocal (lua_State *L) { + int arg; + const char *name; + lua_State *L1 = getthread(L, &arg); + lua_Debug ar; + int level = (int)luaL_checkinteger(L, arg + 1); + int nvar = (int)luaL_checkinteger(L, arg + 2); + if (!lua_getstack(L1, level, &ar)) /* out of range? */ + return luaL_argerror(L, arg+1, "level out of range"); + luaL_checkany(L, arg+3); + lua_settop(L, arg+3); + checkstack(L, L1, 1); + lua_xmove(L, L1, 1); + name = lua_setlocal(L1, &ar, nvar); + if (name == NULL) + lua_pop(L1, 1); /* pop value (if not popped by 'lua_setlocal') */ + lua_pushstring(L, name); + return 1; +} + + +/* +** get (if 'get' is true) or set an upvalue from a closure +*/ +static int auxupvalue (lua_State *L, int get) { + const char *name; + int n = (int)luaL_checkinteger(L, 2); /* upvalue index */ + luaL_checktype(L, 1, LUA_TFUNCTION); /* closure */ + name = get ? lua_getupvalue(L, 1, n) : lua_setupvalue(L, 1, n); + if (name == NULL) return 0; + lua_pushstring(L, name); + lua_insert(L, -(get+1)); /* no-op if get is false */ + return get + 1; +} + + +static int db_getupvalue (lua_State *L) { + return auxupvalue(L, 1); +} + + +static int db_setupvalue (lua_State *L) { + luaL_checkany(L, 3); + return auxupvalue(L, 0); +} + + +/* +** Check whether a given upvalue from a given closure exists and +** returns its index +*/ +static int checkupval (lua_State *L, int argf, int argnup) { + int nup = (int)luaL_checkinteger(L, argnup); /* upvalue index */ + luaL_checktype(L, argf, LUA_TFUNCTION); /* closure */ + luaL_argcheck(L, (lua_getupvalue(L, argf, nup) != NULL), argnup, + "invalid upvalue index"); + return nup; +} + + +static int db_upvalueid (lua_State *L) { + int n = checkupval(L, 1, 2); + lua_pushlightuserdata(L, lua_upvalueid(L, 1, n)); + return 1; +} + + +static int db_upvaluejoin (lua_State *L) { + int n1 = checkupval(L, 1, 2); + int n2 = checkupval(L, 3, 4); + luaL_argcheck(L, !lua_iscfunction(L, 1), 1, "Lua function expected"); + luaL_argcheck(L, !lua_iscfunction(L, 3), 3, "Lua function expected"); + lua_upvaluejoin(L, 1, n1, 3, n2); + return 0; +} + + +/* +** Call hook function registered at hook table for the current +** thread (if there is one) +*/ +static void hookf (lua_State *L, lua_Debug *ar) { + static const char *const hooknames[] = + {"call", "return", "line", "count", "tail call"}; + lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY); + lua_pushthread(L); + if (lua_rawget(L, -2) == LUA_TFUNCTION) { /* is there a hook function? */ + lua_pushstring(L, hooknames[(int)ar->event]); /* push event name */ + if (ar->currentline >= 0) + lua_pushinteger(L, ar->currentline); /* push current line */ + else lua_pushnil(L); + lua_assert(lua_getinfo(L, "lS", ar)); + lua_call(L, 2, 0); /* call hook function */ + } +} + + +/* +** Convert a string mask (for 'sethook') into a bit mask +*/ +static int makemask (const char *smask, int count) { + int mask = 0; + if (strchr(smask, 'c')) mask |= LUA_MASKCALL; + if (strchr(smask, 'r')) mask |= LUA_MASKRET; + if (strchr(smask, 'l')) mask |= LUA_MASKLINE; + if (count > 0) mask |= LUA_MASKCOUNT; + return mask; +} + + +/* +** Convert a bit mask (for 'gethook') into a string mask +*/ +static char *unmakemask (int mask, char *smask) { + int i = 0; + if (mask & LUA_MASKCALL) smask[i++] = 'c'; + if (mask & LUA_MASKRET) smask[i++] = 'r'; + if (mask & LUA_MASKLINE) smask[i++] = 'l'; + smask[i] = '\0'; + return smask; +} + + +static int db_sethook (lua_State *L) { + int arg, mask, count; + lua_Hook func; + lua_State *L1 = getthread(L, &arg); + if (lua_isnoneornil(L, arg+1)) { /* no hook? */ + lua_settop(L, arg+1); + func = NULL; mask = 0; count = 0; /* turn off hooks */ + } + else { + const char *smask = luaL_checkstring(L, arg+2); + luaL_checktype(L, arg+1, LUA_TFUNCTION); + count = (int)luaL_optinteger(L, arg + 3, 0); + func = hookf; mask = makemask(smask, count); + } + if (lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY) == LUA_TNIL) { + lua_createtable(L, 0, 2); /* create a hook table */ + lua_pushvalue(L, -1); + lua_rawsetp(L, LUA_REGISTRYINDEX, &HOOKKEY); /* set it in position */ + lua_pushstring(L, "k"); + lua_setfield(L, -2, "__mode"); /** hooktable.__mode = "k" */ + lua_pushvalue(L, -1); + lua_setmetatable(L, -2); /* setmetatable(hooktable) = hooktable */ + } + checkstack(L, L1, 1); + lua_pushthread(L1); lua_xmove(L1, L, 1); /* key (thread) */ + lua_pushvalue(L, arg + 1); /* value (hook function) */ + lua_rawset(L, -3); /* hooktable[L1] = new Lua hook */ + lua_sethook(L1, func, mask, count); + return 0; +} + + +static int db_gethook (lua_State *L) { + int arg; + lua_State *L1 = getthread(L, &arg); + char buff[5]; + int mask = lua_gethookmask(L1); + lua_Hook hook = lua_gethook(L1); + if (hook == NULL) /* no hook? */ + lua_pushnil(L); + else if (hook != hookf) /* external hook? */ + lua_pushliteral(L, "external hook"); + else { /* hook table must exist */ + lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY); + checkstack(L, L1, 1); + lua_pushthread(L1); lua_xmove(L1, L, 1); + lua_rawget(L, -2); /* 1st result = hooktable[L1] */ + lua_remove(L, -2); /* remove hook table */ + } + lua_pushstring(L, unmakemask(mask, buff)); /* 2nd result = mask */ + lua_pushinteger(L, lua_gethookcount(L1)); /* 3rd result = count */ + return 3; +} + + +static int db_debug (lua_State *L) { + for (;;) { + char buffer[250]; + lua_writestringerror("%s", "lua_debug> "); + if (fgets(buffer, sizeof(buffer), stdin) == 0 || + strcmp(buffer, "cont\n") == 0) + return 0; + if (luaL_loadbuffer(L, buffer, strlen(buffer), "=(debug command)") || + lua_pcall(L, 0, 0, 0)) + lua_writestringerror("%s\n", lua_tostring(L, -1)); + lua_settop(L, 0); /* remove eventual returns */ + } +} + + +static int db_traceback (lua_State *L) { + int arg; + lua_State *L1 = getthread(L, &arg); + const char *msg = lua_tostring(L, arg + 1); + if (msg == NULL && !lua_isnoneornil(L, arg + 1)) /* non-string 'msg'? */ + lua_pushvalue(L, arg + 1); /* return it untouched */ + else { + int level = (int)luaL_optinteger(L, arg + 2, (L == L1) ? 1 : 0); + luaL_traceback(L, L1, msg, level); + } + return 1; +} + + +static const luaL_Reg dblib[] = { + {"debug", db_debug}, + {"getuservalue", db_getuservalue}, + {"gethook", db_gethook}, + {"getinfo", db_getinfo}, + {"getlocal", db_getlocal}, + {"getregistry", db_getregistry}, + {"getmetatable", db_getmetatable}, + {"getupvalue", db_getupvalue}, + {"upvaluejoin", db_upvaluejoin}, + {"upvalueid", db_upvalueid}, + {"setuservalue", db_setuservalue}, + {"sethook", db_sethook}, + {"setlocal", db_setlocal}, + {"setmetatable", db_setmetatable}, + {"setupvalue", db_setupvalue}, + {"traceback", db_traceback}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_debug (lua_State *L) { + luaL_newlib(L, dblib); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/ldebug.c b/src/rcheevos/test/lua/src/ldebug.c new file mode 100644 index 000000000..4a98cd411 --- /dev/null +++ b/src/rcheevos/test/lua/src/ldebug.c @@ -0,0 +1,698 @@ +/* +** $Id: ldebug.c,v 2.121 2016/10/19 12:32:10 roberto Exp $ +** Debug Interface +** See Copyright Notice in lua.h +*/ + +#define ldebug_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + + +#define noLuaClosure(f) ((f) == NULL || (f)->c.tt == LUA_TCCL) + + +/* Active Lua function (given call info) */ +#define ci_func(ci) (clLvalue((ci)->func)) + + +static const char *funcnamefromcode (lua_State *L, CallInfo *ci, + const char **name); + + +static int currentpc (CallInfo *ci) { + lua_assert(isLua(ci)); + return pcRel(ci->u.l.savedpc, ci_func(ci)->p); +} + + +static int currentline (CallInfo *ci) { + return getfuncline(ci_func(ci)->p, currentpc(ci)); +} + + +/* +** If function yielded, its 'func' can be in the 'extra' field. The +** next function restores 'func' to its correct value for debugging +** purposes. (It exchanges 'func' and 'extra'; so, when called again, +** after debugging, it also "re-restores" ** 'func' to its altered value. +*/ +static void swapextra (lua_State *L) { + if (L->status == LUA_YIELD) { + CallInfo *ci = L->ci; /* get function that yielded */ + StkId temp = ci->func; /* exchange its 'func' and 'extra' values */ + ci->func = restorestack(L, ci->extra); + ci->extra = savestack(L, temp); + } +} + + +/* +** This function can be called asynchronously (e.g. during a signal). +** Fields 'oldpc', 'basehookcount', and 'hookcount' (set by +** 'resethookcount') are for debug only, and it is no problem if they +** get arbitrary values (causes at most one wrong hook call). 'hookmask' +** is an atomic value. We assume that pointers are atomic too (e.g., gcc +** ensures that for all platforms where it runs). Moreover, 'hook' is +** always checked before being called (see 'luaD_hook'). +*/ +LUA_API void lua_sethook (lua_State *L, lua_Hook func, int mask, int count) { + if (func == NULL || mask == 0) { /* turn off hooks? */ + mask = 0; + func = NULL; + } + if (isLua(L->ci)) + L->oldpc = L->ci->u.l.savedpc; + L->hook = func; + L->basehookcount = count; + resethookcount(L); + L->hookmask = cast_byte(mask); +} + + +LUA_API lua_Hook lua_gethook (lua_State *L) { + return L->hook; +} + + +LUA_API int lua_gethookmask (lua_State *L) { + return L->hookmask; +} + + +LUA_API int lua_gethookcount (lua_State *L) { + return L->basehookcount; +} + + +LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar) { + int status; + CallInfo *ci; + if (level < 0) return 0; /* invalid (negative) level */ + lua_lock(L); + for (ci = L->ci; level > 0 && ci != &L->base_ci; ci = ci->previous) + level--; + if (level == 0 && ci != &L->base_ci) { /* level found? */ + status = 1; + ar->i_ci = ci; + } + else status = 0; /* no such level */ + lua_unlock(L); + return status; +} + + +static const char *upvalname (Proto *p, int uv) { + TString *s = check_exp(uv < p->sizeupvalues, p->upvalues[uv].name); + if (s == NULL) return "?"; + else return getstr(s); +} + + +static const char *findvararg (CallInfo *ci, int n, StkId *pos) { + int nparams = clLvalue(ci->func)->p->numparams; + if (n >= cast_int(ci->u.l.base - ci->func) - nparams) + return NULL; /* no such vararg */ + else { + *pos = ci->func + nparams + n; + return "(*vararg)"; /* generic name for any vararg */ + } +} + + +static const char *findlocal (lua_State *L, CallInfo *ci, int n, + StkId *pos) { + const char *name = NULL; + StkId base; + if (isLua(ci)) { + if (n < 0) /* access to vararg values? */ + return findvararg(ci, -n, pos); + else { + base = ci->u.l.base; + name = luaF_getlocalname(ci_func(ci)->p, n, currentpc(ci)); + } + } + else + base = ci->func + 1; + if (name == NULL) { /* no 'standard' name? */ + StkId limit = (ci == L->ci) ? L->top : ci->next->func; + if (limit - base >= n && n > 0) /* is 'n' inside 'ci' stack? */ + name = "(*temporary)"; /* generic name for any valid slot */ + else + return NULL; /* no name */ + } + *pos = base + (n - 1); + return name; +} + + +LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n) { + const char *name; + lua_lock(L); + swapextra(L); + if (ar == NULL) { /* information about non-active function? */ + if (!isLfunction(L->top - 1)) /* not a Lua function? */ + name = NULL; + else /* consider live variables at function start (parameters) */ + name = luaF_getlocalname(clLvalue(L->top - 1)->p, n, 0); + } + else { /* active function; get information through 'ar' */ + StkId pos = NULL; /* to avoid warnings */ + name = findlocal(L, ar->i_ci, n, &pos); + if (name) { + setobj2s(L, L->top, pos); + api_incr_top(L); + } + } + swapextra(L); + lua_unlock(L); + return name; +} + + +LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n) { + StkId pos = NULL; /* to avoid warnings */ + const char *name; + lua_lock(L); + swapextra(L); + name = findlocal(L, ar->i_ci, n, &pos); + if (name) { + setobjs2s(L, pos, L->top - 1); + L->top--; /* pop value */ + } + swapextra(L); + lua_unlock(L); + return name; +} + + +static void funcinfo (lua_Debug *ar, Closure *cl) { + if (noLuaClosure(cl)) { + ar->source = "=[C]"; + ar->linedefined = -1; + ar->lastlinedefined = -1; + ar->what = "C"; + } + else { + Proto *p = cl->l.p; + ar->source = p->source ? getstr(p->source) : "=?"; + ar->linedefined = p->linedefined; + ar->lastlinedefined = p->lastlinedefined; + ar->what = (ar->linedefined == 0) ? "main" : "Lua"; + } + luaO_chunkid(ar->short_src, ar->source, LUA_IDSIZE); +} + + +static void collectvalidlines (lua_State *L, Closure *f) { + if (noLuaClosure(f)) { + setnilvalue(L->top); + api_incr_top(L); + } + else { + int i; + TValue v; + int *lineinfo = f->l.p->lineinfo; + Table *t = luaH_new(L); /* new table to store active lines */ + sethvalue(L, L->top, t); /* push it on stack */ + api_incr_top(L); + setbvalue(&v, 1); /* boolean 'true' to be the value of all indices */ + for (i = 0; i < f->l.p->sizelineinfo; i++) /* for all lines with code */ + luaH_setint(L, t, lineinfo[i], &v); /* table[line] = true */ + } +} + + +static const char *getfuncname (lua_State *L, CallInfo *ci, const char **name) { + if (ci == NULL) /* no 'ci'? */ + return NULL; /* no info */ + else if (ci->callstatus & CIST_FIN) { /* is this a finalizer? */ + *name = "__gc"; + return "metamethod"; /* report it as such */ + } + /* calling function is a known Lua function? */ + else if (!(ci->callstatus & CIST_TAIL) && isLua(ci->previous)) + return funcnamefromcode(L, ci->previous, name); + else return NULL; /* no way to find a name */ +} + + +static int auxgetinfo (lua_State *L, const char *what, lua_Debug *ar, + Closure *f, CallInfo *ci) { + int status = 1; + for (; *what; what++) { + switch (*what) { + case 'S': { + funcinfo(ar, f); + break; + } + case 'l': { + ar->currentline = (ci && isLua(ci)) ? currentline(ci) : -1; + break; + } + case 'u': { + ar->nups = (f == NULL) ? 0 : f->c.nupvalues; + if (noLuaClosure(f)) { + ar->isvararg = 1; + ar->nparams = 0; + } + else { + ar->isvararg = f->l.p->is_vararg; + ar->nparams = f->l.p->numparams; + } + break; + } + case 't': { + ar->istailcall = (ci) ? ci->callstatus & CIST_TAIL : 0; + break; + } + case 'n': { + ar->namewhat = getfuncname(L, ci, &ar->name); + if (ar->namewhat == NULL) { + ar->namewhat = ""; /* not found */ + ar->name = NULL; + } + break; + } + case 'L': + case 'f': /* handled by lua_getinfo */ + break; + default: status = 0; /* invalid option */ + } + } + return status; +} + + +LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar) { + int status; + Closure *cl; + CallInfo *ci; + StkId func; + lua_lock(L); + swapextra(L); + if (*what == '>') { + ci = NULL; + func = L->top - 1; + api_check(L, ttisfunction(func), "function expected"); + what++; /* skip the '>' */ + L->top--; /* pop function */ + } + else { + ci = ar->i_ci; + func = ci->func; + lua_assert(ttisfunction(ci->func)); + } + cl = ttisclosure(func) ? clvalue(func) : NULL; + status = auxgetinfo(L, what, ar, cl, ci); + if (strchr(what, 'f')) { + setobjs2s(L, L->top, func); + api_incr_top(L); + } + swapextra(L); /* correct before option 'L', which can raise a mem. error */ + if (strchr(what, 'L')) + collectvalidlines(L, cl); + lua_unlock(L); + return status; +} + + +/* +** {====================================================== +** Symbolic Execution +** ======================================================= +*/ + +static const char *getobjname (Proto *p, int lastpc, int reg, + const char **name); + + +/* +** find a "name" for the RK value 'c' +*/ +static void kname (Proto *p, int pc, int c, const char **name) { + if (ISK(c)) { /* is 'c' a constant? */ + TValue *kvalue = &p->k[INDEXK(c)]; + if (ttisstring(kvalue)) { /* literal constant? */ + *name = svalue(kvalue); /* it is its own name */ + return; + } + /* else no reasonable name found */ + } + else { /* 'c' is a register */ + const char *what = getobjname(p, pc, c, name); /* search for 'c' */ + if (what && *what == 'c') { /* found a constant name? */ + return; /* 'name' already filled */ + } + /* else no reasonable name found */ + } + *name = "?"; /* no reasonable name found */ +} + + +static int filterpc (int pc, int jmptarget) { + if (pc < jmptarget) /* is code conditional (inside a jump)? */ + return -1; /* cannot know who sets that register */ + else return pc; /* current position sets that register */ +} + + +/* +** try to find last instruction before 'lastpc' that modified register 'reg' +*/ +static int findsetreg (Proto *p, int lastpc, int reg) { + int pc; + int setreg = -1; /* keep last instruction that changed 'reg' */ + int jmptarget = 0; /* any code before this address is conditional */ + for (pc = 0; pc < lastpc; pc++) { + Instruction i = p->code[pc]; + OpCode op = GET_OPCODE(i); + int a = GETARG_A(i); + switch (op) { + case OP_LOADNIL: { + int b = GETARG_B(i); + if (a <= reg && reg <= a + b) /* set registers from 'a' to 'a+b' */ + setreg = filterpc(pc, jmptarget); + break; + } + case OP_TFORCALL: { + if (reg >= a + 2) /* affect all regs above its base */ + setreg = filterpc(pc, jmptarget); + break; + } + case OP_CALL: + case OP_TAILCALL: { + if (reg >= a) /* affect all registers above base */ + setreg = filterpc(pc, jmptarget); + break; + } + case OP_JMP: { + int b = GETARG_sBx(i); + int dest = pc + 1 + b; + /* jump is forward and do not skip 'lastpc'? */ + if (pc < dest && dest <= lastpc) { + if (dest > jmptarget) + jmptarget = dest; /* update 'jmptarget' */ + } + break; + } + default: + if (testAMode(op) && reg == a) /* any instruction that set A */ + setreg = filterpc(pc, jmptarget); + break; + } + } + return setreg; +} + + +static const char *getobjname (Proto *p, int lastpc, int reg, + const char **name) { + int pc; + *name = luaF_getlocalname(p, reg + 1, lastpc); + if (*name) /* is a local? */ + return "local"; + /* else try symbolic execution */ + pc = findsetreg(p, lastpc, reg); + if (pc != -1) { /* could find instruction? */ + Instruction i = p->code[pc]; + OpCode op = GET_OPCODE(i); + switch (op) { + case OP_MOVE: { + int b = GETARG_B(i); /* move from 'b' to 'a' */ + if (b < GETARG_A(i)) + return getobjname(p, pc, b, name); /* get name for 'b' */ + break; + } + case OP_GETTABUP: + case OP_GETTABLE: { + int k = GETARG_C(i); /* key index */ + int t = GETARG_B(i); /* table index */ + const char *vn = (op == OP_GETTABLE) /* name of indexed variable */ + ? luaF_getlocalname(p, t + 1, pc) + : upvalname(p, t); + kname(p, pc, k, name); + return (vn && strcmp(vn, LUA_ENV) == 0) ? "global" : "field"; + } + case OP_GETUPVAL: { + *name = upvalname(p, GETARG_B(i)); + return "upvalue"; + } + case OP_LOADK: + case OP_LOADKX: { + int b = (op == OP_LOADK) ? GETARG_Bx(i) + : GETARG_Ax(p->code[pc + 1]); + if (ttisstring(&p->k[b])) { + *name = svalue(&p->k[b]); + return "constant"; + } + break; + } + case OP_SELF: { + int k = GETARG_C(i); /* key index */ + kname(p, pc, k, name); + return "method"; + } + default: break; /* go through to return NULL */ + } + } + return NULL; /* could not find reasonable name */ +} + + +/* +** Try to find a name for a function based on the code that called it. +** (Only works when function was called by a Lua function.) +** Returns what the name is (e.g., "for iterator", "method", +** "metamethod") and sets '*name' to point to the name. +*/ +static const char *funcnamefromcode (lua_State *L, CallInfo *ci, + const char **name) { + TMS tm = (TMS)0; /* (initial value avoids warnings) */ + Proto *p = ci_func(ci)->p; /* calling function */ + int pc = currentpc(ci); /* calling instruction index */ + Instruction i = p->code[pc]; /* calling instruction */ + if (ci->callstatus & CIST_HOOKED) { /* was it called inside a hook? */ + *name = "?"; + return "hook"; + } + switch (GET_OPCODE(i)) { + case OP_CALL: + case OP_TAILCALL: + return getobjname(p, pc, GETARG_A(i), name); /* get function name */ + case OP_TFORCALL: { /* for iterator */ + *name = "for iterator"; + return "for iterator"; + } + /* other instructions can do calls through metamethods */ + case OP_SELF: case OP_GETTABUP: case OP_GETTABLE: + tm = TM_INDEX; + break; + case OP_SETTABUP: case OP_SETTABLE: + tm = TM_NEWINDEX; + break; + case OP_ADD: case OP_SUB: case OP_MUL: case OP_MOD: + case OP_POW: case OP_DIV: case OP_IDIV: case OP_BAND: + case OP_BOR: case OP_BXOR: case OP_SHL: case OP_SHR: { + int offset = cast_int(GET_OPCODE(i)) - cast_int(OP_ADD); /* ORDER OP */ + tm = cast(TMS, offset + cast_int(TM_ADD)); /* ORDER TM */ + break; + } + case OP_UNM: tm = TM_UNM; break; + case OP_BNOT: tm = TM_BNOT; break; + case OP_LEN: tm = TM_LEN; break; + case OP_CONCAT: tm = TM_CONCAT; break; + case OP_EQ: tm = TM_EQ; break; + case OP_LT: tm = TM_LT; break; + case OP_LE: tm = TM_LE; break; + default: + return NULL; /* cannot find a reasonable name */ + } + *name = getstr(G(L)->tmname[tm]); + return "metamethod"; +} + +/* }====================================================== */ + + + +/* +** The subtraction of two potentially unrelated pointers is +** not ISO C, but it should not crash a program; the subsequent +** checks are ISO C and ensure a correct result. +*/ +static int isinstack (CallInfo *ci, const TValue *o) { + ptrdiff_t i = o - ci->u.l.base; + return (0 <= i && i < (ci->top - ci->u.l.base) && ci->u.l.base + i == o); +} + + +/* +** Checks whether value 'o' came from an upvalue. (That can only happen +** with instructions OP_GETTABUP/OP_SETTABUP, which operate directly on +** upvalues.) +*/ +static const char *getupvalname (CallInfo *ci, const TValue *o, + const char **name) { + LClosure *c = ci_func(ci); + int i; + for (i = 0; i < c->nupvalues; i++) { + if (c->upvals[i]->v == o) { + *name = upvalname(c->p, i); + return "upvalue"; + } + } + return NULL; +} + + +static const char *varinfo (lua_State *L, const TValue *o) { + const char *name = NULL; /* to avoid warnings */ + CallInfo *ci = L->ci; + const char *kind = NULL; + if (isLua(ci)) { + kind = getupvalname(ci, o, &name); /* check whether 'o' is an upvalue */ + if (!kind && isinstack(ci, o)) /* no? try a register */ + kind = getobjname(ci_func(ci)->p, currentpc(ci), + cast_int(o - ci->u.l.base), &name); + } + return (kind) ? luaO_pushfstring(L, " (%s '%s')", kind, name) : ""; +} + + +l_noret luaG_typeerror (lua_State *L, const TValue *o, const char *op) { + const char *t = luaT_objtypename(L, o); + luaG_runerror(L, "attempt to %s a %s value%s", op, t, varinfo(L, o)); +} + + +l_noret luaG_concaterror (lua_State *L, const TValue *p1, const TValue *p2) { + if (ttisstring(p1) || cvt2str(p1)) p1 = p2; + luaG_typeerror(L, p1, "concatenate"); +} + + +l_noret luaG_opinterror (lua_State *L, const TValue *p1, + const TValue *p2, const char *msg) { + lua_Number temp; + if (!tonumber(p1, &temp)) /* first operand is wrong? */ + p2 = p1; /* now second is wrong */ + luaG_typeerror(L, p2, msg); +} + + +/* +** Error when both values are convertible to numbers, but not to integers +*/ +l_noret luaG_tointerror (lua_State *L, const TValue *p1, const TValue *p2) { + lua_Integer temp; + if (!tointeger(p1, &temp)) + p2 = p1; + luaG_runerror(L, "number%s has no integer representation", varinfo(L, p2)); +} + + +l_noret luaG_ordererror (lua_State *L, const TValue *p1, const TValue *p2) { + const char *t1 = luaT_objtypename(L, p1); + const char *t2 = luaT_objtypename(L, p2); + if (strcmp(t1, t2) == 0) + luaG_runerror(L, "attempt to compare two %s values", t1); + else + luaG_runerror(L, "attempt to compare %s with %s", t1, t2); +} + + +/* add src:line information to 'msg' */ +const char *luaG_addinfo (lua_State *L, const char *msg, TString *src, + int line) { + char buff[LUA_IDSIZE]; + if (src) + luaO_chunkid(buff, getstr(src), LUA_IDSIZE); + else { /* no source available; use "?" instead */ + buff[0] = '?'; buff[1] = '\0'; + } + return luaO_pushfstring(L, "%s:%d: %s", buff, line, msg); +} + + +l_noret luaG_errormsg (lua_State *L) { + if (L->errfunc != 0) { /* is there an error handling function? */ + StkId errfunc = restorestack(L, L->errfunc); + setobjs2s(L, L->top, L->top - 1); /* move argument */ + setobjs2s(L, L->top - 1, errfunc); /* push function */ + L->top++; /* assume EXTRC_STACK */ + luaD_callnoyield(L, L->top - 2, 1); /* call it */ + } + luaD_throw(L, LUA_ERRRUN); +} + + +l_noret luaG_runerror (lua_State *L, const char *fmt, ...) { + CallInfo *ci = L->ci; + const char *msg; + va_list argp; + va_start(argp, fmt); + msg = luaO_pushvfstring(L, fmt, argp); /* format message */ + va_end(argp); + if (isLua(ci)) /* if Lua function, add source:line information */ + luaG_addinfo(L, msg, ci_func(ci)->p->source, currentline(ci)); + luaG_errormsg(L); +} + + +void luaG_traceexec (lua_State *L) { + CallInfo *ci = L->ci; + lu_byte mask = L->hookmask; + int counthook = (--L->hookcount == 0 && (mask & LUA_MASKCOUNT)); + if (counthook) + resethookcount(L); /* reset count */ + else if (!(mask & LUA_MASKLINE)) + return; /* no line hook and count != 0; nothing to be done */ + if (ci->callstatus & CIST_HOOKYIELD) { /* called hook last time? */ + ci->callstatus &= ~CIST_HOOKYIELD; /* erase mark */ + return; /* do not call hook again (VM yielded, so it did not move) */ + } + if (counthook) + luaD_hook(L, LUA_HOOKCOUNT, -1); /* call count hook */ + if (mask & LUA_MASKLINE) { + Proto *p = ci_func(ci)->p; + int npc = pcRel(ci->u.l.savedpc, p); + int newline = getfuncline(p, npc); + if (npc == 0 || /* call linehook when enter a new function, */ + ci->u.l.savedpc <= L->oldpc || /* when jump back (loop), or when */ + newline != getfuncline(p, pcRel(L->oldpc, p))) /* enter a new line */ + luaD_hook(L, LUA_HOOKLINE, newline); /* call line hook */ + } + L->oldpc = ci->u.l.savedpc; + if (L->status == LUA_YIELD) { /* did hook yield? */ + if (counthook) + L->hookcount = 1; /* undo decrement to zero */ + ci->u.l.savedpc--; /* undo increment (resume will increment it again) */ + ci->callstatus |= CIST_HOOKYIELD; /* mark that it yielded */ + ci->func = L->top - 1; /* protect stack below results */ + luaD_throw(L, LUA_YIELD); + } +} + diff --git a/src/rcheevos/test/lua/src/ldebug.h b/src/rcheevos/test/lua/src/ldebug.h new file mode 100644 index 000000000..0e31546b1 --- /dev/null +++ b/src/rcheevos/test/lua/src/ldebug.h @@ -0,0 +1,39 @@ +/* +** $Id: ldebug.h,v 2.14 2015/05/22 17:45:56 roberto Exp $ +** Auxiliary functions from Debug Interface module +** See Copyright Notice in lua.h +*/ + +#ifndef ldebug_h +#define ldebug_h + + +#include "lstate.h" + + +#define pcRel(pc, p) (cast(int, (pc) - (p)->code) - 1) + +#define getfuncline(f,pc) (((f)->lineinfo) ? (f)->lineinfo[pc] : -1) + +#define resethookcount(L) (L->hookcount = L->basehookcount) + + +LUAI_FUNC l_noret luaG_typeerror (lua_State *L, const TValue *o, + const char *opname); +LUAI_FUNC l_noret luaG_concaterror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC l_noret luaG_opinterror (lua_State *L, const TValue *p1, + const TValue *p2, + const char *msg); +LUAI_FUNC l_noret luaG_tointerror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC l_noret luaG_ordererror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC l_noret luaG_runerror (lua_State *L, const char *fmt, ...); +LUAI_FUNC const char *luaG_addinfo (lua_State *L, const char *msg, + TString *src, int line); +LUAI_FUNC l_noret luaG_errormsg (lua_State *L); +LUAI_FUNC void luaG_traceexec (lua_State *L); + + +#endif diff --git a/src/rcheevos/test/lua/src/ldo.c b/src/rcheevos/test/lua/src/ldo.c new file mode 100644 index 000000000..0a0132ce9 --- /dev/null +++ b/src/rcheevos/test/lua/src/ldo.c @@ -0,0 +1,802 @@ +/* +** $Id: ldo.c,v 2.157 2016/12/13 15:52:21 roberto Exp $ +** Stack and Call structure of Lua +** See Copyright Notice in lua.h +*/ + +#define ldo_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lundump.h" +#include "lvm.h" +#include "lzio.h" + + + +#define errorstatus(s) ((s) > LUA_YIELD) + + +/* +** {====================================================== +** Error-recovery functions +** ======================================================= +*/ + +/* +** LUAI_THROW/LUAI_TRY define how Lua does exception handling. By +** default, Lua handles errors with exceptions when compiling as +** C++ code, with _longjmp/_setjmp when asked to use them, and with +** longjmp/setjmp otherwise. +*/ +#if !defined(LUAI_THROW) /* { */ + +#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP) /* { */ + +/* C++ exceptions */ +#define LUAI_THROW(L,c) throw(c) +#define LUAI_TRY(L,c,a) \ + try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; } +#define luai_jmpbuf int /* dummy variable */ + +#elif defined(LUA_USE_POSIX) /* }{ */ + +/* in POSIX, try _longjmp/_setjmp (more efficient) */ +#define LUAI_THROW(L,c) _longjmp((c)->b, 1) +#define LUAI_TRY(L,c,a) if (_setjmp((c)->b) == 0) { a } +#define luai_jmpbuf jmp_buf + +#else /* }{ */ + +/* ISO C handling with long jumps */ +#define LUAI_THROW(L,c) longjmp((c)->b, 1) +#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a } +#define luai_jmpbuf jmp_buf + +#endif /* } */ + +#endif /* } */ + + + +/* chain list of long jump buffers */ +struct lua_longjmp { + struct lua_longjmp *previous; + luai_jmpbuf b; + volatile int status; /* error code */ +}; + + +static void seterrorobj (lua_State *L, int errcode, StkId oldtop) { + switch (errcode) { + case LUA_ERRMEM: { /* memory error? */ + setsvalue2s(L, oldtop, G(L)->memerrmsg); /* reuse preregistered msg. */ + break; + } + case LUA_ERRERR: { + setsvalue2s(L, oldtop, luaS_newliteral(L, "error in error handling")); + break; + } + default: { + setobjs2s(L, oldtop, L->top - 1); /* error message on current top */ + break; + } + } + L->top = oldtop + 1; +} + + +l_noret luaD_throw (lua_State *L, int errcode) { + if (L->errorJmp) { /* thread has an error handler? */ + L->errorJmp->status = errcode; /* set status */ + LUAI_THROW(L, L->errorJmp); /* jump to it */ + } + else { /* thread has no error handler */ + global_State *g = G(L); + L->status = cast_byte(errcode); /* mark it as dead */ + if (g->mainthread->errorJmp) { /* main thread has a handler? */ + setobjs2s(L, g->mainthread->top++, L->top - 1); /* copy error obj. */ + luaD_throw(g->mainthread, errcode); /* re-throw in main thread */ + } + else { /* no handler at all; abort */ + if (g->panic) { /* panic function? */ + seterrorobj(L, errcode, L->top); /* assume EXTRC_STACK */ + if (L->ci->top < L->top) + L->ci->top = L->top; /* pushing msg. can break this invariant */ + lua_unlock(L); + g->panic(L); /* call panic function (last chance to jump out) */ + } + abort(); + } + } +} + + +int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) { + unsigned short oldnCcalls = L->nCcalls; + struct lua_longjmp lj; + lj.status = LUA_OK; + lj.previous = L->errorJmp; /* chain new error handler */ + L->errorJmp = &lj; + LUAI_TRY(L, &lj, + (*f)(L, ud); + ); + L->errorJmp = lj.previous; /* restore old error handler */ + L->nCcalls = oldnCcalls; + return lj.status; +} + +/* }====================================================== */ + + +/* +** {================================================================== +** Stack reallocation +** =================================================================== +*/ +static void correctstack (lua_State *L, TValue *oldstack) { + CallInfo *ci; + UpVal *up; + L->top = (L->top - oldstack) + L->stack; + for (up = L->openupval; up != NULL; up = up->u.open.next) + up->v = (up->v - oldstack) + L->stack; + for (ci = L->ci; ci != NULL; ci = ci->previous) { + ci->top = (ci->top - oldstack) + L->stack; + ci->func = (ci->func - oldstack) + L->stack; + if (isLua(ci)) + ci->u.l.base = (ci->u.l.base - oldstack) + L->stack; + } +} + + +/* some space for error handling */ +#define ERRORSTACKSIZE (LUAI_MAXSTACK + 200) + + +void luaD_reallocstack (lua_State *L, int newsize) { + TValue *oldstack = L->stack; + int lim = L->stacksize; + lua_assert(newsize <= LUAI_MAXSTACK || newsize == ERRORSTACKSIZE); + lua_assert(L->stack_last - L->stack == L->stacksize - EXTRC_STACK); + luaM_reallocvector(L, L->stack, L->stacksize, newsize, TValue); + for (; lim < newsize; lim++) + setnilvalue(L->stack + lim); /* erase new segment */ + L->stacksize = newsize; + L->stack_last = L->stack + newsize - EXTRC_STACK; + correctstack(L, oldstack); +} + + +void luaD_growstack (lua_State *L, int n) { + int size = L->stacksize; + if (size > LUAI_MAXSTACK) /* error after extra size? */ + luaD_throw(L, LUA_ERRERR); + else { + int needed = cast_int(L->top - L->stack) + n + EXTRC_STACK; + int newsize = 2 * size; + if (newsize > LUAI_MAXSTACK) newsize = LUAI_MAXSTACK; + if (newsize < needed) newsize = needed; + if (newsize > LUAI_MAXSTACK) { /* stack overflow? */ + luaD_reallocstack(L, ERRORSTACKSIZE); + luaG_runerror(L, "stack overflow"); + } + else + luaD_reallocstack(L, newsize); + } +} + + +static int stackinuse (lua_State *L) { + CallInfo *ci; + StkId lim = L->top; + for (ci = L->ci; ci != NULL; ci = ci->previous) { + if (lim < ci->top) lim = ci->top; + } + lua_assert(lim <= L->stack_last); + return cast_int(lim - L->stack) + 1; /* part of stack in use */ +} + + +void luaD_shrinkstack (lua_State *L) { + int inuse = stackinuse(L); + int goodsize = inuse + (inuse / 8) + 2*EXTRC_STACK; + if (goodsize > LUAI_MAXSTACK) + goodsize = LUAI_MAXSTACK; /* respect stack limit */ + if (L->stacksize > LUAI_MAXSTACK) /* had been handling stack overflow? */ + luaE_freeCI(L); /* free all CIs (list grew because of an error) */ + else + luaE_shrinkCI(L); /* shrink list */ + /* if thread is currently not handling a stack overflow and its + good size is smaller than current size, shrink its stack */ + if (inuse <= (LUAI_MAXSTACK - EXTRC_STACK) && + goodsize < L->stacksize) + luaD_reallocstack(L, goodsize); + else /* don't change stack */ + condmovestack(L,{},{}); /* (change only for debugging) */ +} + + +void luaD_inctop (lua_State *L) { + luaD_checkstack(L, 1); + L->top++; +} + +/* }================================================================== */ + + +/* +** Call a hook for the given event. Make sure there is a hook to be +** called. (Both 'L->hook' and 'L->hookmask', which triggers this +** function, can be changed asynchronously by signals.) +*/ +void luaD_hook (lua_State *L, int event, int line) { + lua_Hook hook = L->hook; + if (hook && L->allowhook) { /* make sure there is a hook */ + CallInfo *ci = L->ci; + ptrdiff_t top = savestack(L, L->top); + ptrdiff_t ci_top = savestack(L, ci->top); + lua_Debug ar; + ar.event = event; + ar.currentline = line; + ar.i_ci = ci; + luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */ + ci->top = L->top + LUA_MINSTACK; + lua_assert(ci->top <= L->stack_last); + L->allowhook = 0; /* cannot call hooks inside a hook */ + ci->callstatus |= CIST_HOOKED; + lua_unlock(L); + (*hook)(L, &ar); + lua_lock(L); + lua_assert(!L->allowhook); + L->allowhook = 1; + ci->top = restorestack(L, ci_top); + L->top = restorestack(L, top); + ci->callstatus &= ~CIST_HOOKED; + } +} + + +static void callhook (lua_State *L, CallInfo *ci) { + int hook = LUA_HOOKCALL; + ci->u.l.savedpc++; /* hooks assume 'pc' is already incremented */ + if (isLua(ci->previous) && + GET_OPCODE(*(ci->previous->u.l.savedpc - 1)) == OP_TAILCALL) { + ci->callstatus |= CIST_TAIL; + hook = LUA_HOOKTAILCALL; + } + luaD_hook(L, hook, -1); + ci->u.l.savedpc--; /* correct 'pc' */ +} + + +static StkId adjust_varargs (lua_State *L, Proto *p, int actual) { + int i; + int nfixargs = p->numparams; + StkId base, fixed; + /* move fixed parameters to final position */ + fixed = L->top - actual; /* first fixed argument */ + base = L->top; /* final position of first argument */ + for (i = 0; i < nfixargs && i < actual; i++) { + setobjs2s(L, L->top++, fixed + i); + setnilvalue(fixed + i); /* erase original copy (for GC) */ + } + for (; i < nfixargs; i++) + setnilvalue(L->top++); /* complete missing arguments */ + return base; +} + + +/* +** Check whether __call metafield of 'func' is a function. If so, put +** it in stack below original 'func' so that 'luaD_precall' can call +** it. Raise an error if __call metafield is not a function. +*/ +static void tryfuncTM (lua_State *L, StkId func) { + const TValue *tm = luaT_gettmbyobj(L, func, TM_CALL); + StkId p; + if (!ttisfunction(tm)) + luaG_typeerror(L, func, "call"); + /* Open a hole inside the stack at 'func' */ + for (p = L->top; p > func; p--) + setobjs2s(L, p, p-1); + L->top++; /* slot ensured by caller */ + setobj2s(L, func, tm); /* tag method is the new function to be called */ +} + + +/* +** Given 'nres' results at 'firstResult', move 'wanted' of them to 'res'. +** Handle most typical cases (zero results for commands, one result for +** expressions, multiple results for tail calls/single parameters) +** separated. +*/ +static int moveresults (lua_State *L, const TValue *firstResult, StkId res, + int nres, int wanted) { + switch (wanted) { /* handle typical cases separately */ + case 0: break; /* nothing to move */ + case 1: { /* one result needed */ + if (nres == 0) /* no results? */ + firstResult = luaO_nilobject; /* adjust with nil */ + setobjs2s(L, res, firstResult); /* move it to proper place */ + break; + } + case LUA_MULTRET: { + int i; + for (i = 0; i < nres; i++) /* move all results to correct place */ + setobjs2s(L, res + i, firstResult + i); + L->top = res + nres; + return 0; /* wanted == LUA_MULTRET */ + } + default: { + int i; + if (wanted <= nres) { /* enough results? */ + for (i = 0; i < wanted; i++) /* move wanted results to correct place */ + setobjs2s(L, res + i, firstResult + i); + } + else { /* not enough results; use all of them plus nils */ + for (i = 0; i < nres; i++) /* move all results to correct place */ + setobjs2s(L, res + i, firstResult + i); + for (; i < wanted; i++) /* complete wanted number of results */ + setnilvalue(res + i); + } + break; + } + } + L->top = res + wanted; /* top points after the last result */ + return 1; +} + + +/* +** Finishes a function call: calls hook if necessary, removes CallInfo, +** moves current number of results to proper place; returns 0 iff call +** wanted multiple (variable number of) results. +*/ +int luaD_poscall (lua_State *L, CallInfo *ci, StkId firstResult, int nres) { + StkId res; + int wanted = ci->nresults; + if (L->hookmask & (LUA_MASKRET | LUA_MASKLINE)) { + if (L->hookmask & LUA_MASKRET) { + ptrdiff_t fr = savestack(L, firstResult); /* hook may change stack */ + luaD_hook(L, LUA_HOOKRET, -1); + firstResult = restorestack(L, fr); + } + L->oldpc = ci->previous->u.l.savedpc; /* 'oldpc' for caller function */ + } + res = ci->func; /* res == final position of 1st result */ + L->ci = ci->previous; /* back to caller */ + /* move results to proper place */ + return moveresults(L, firstResult, res, nres, wanted); +} + + + +#define next_ci(L) (L->ci = (L->ci->next ? L->ci->next : luaE_extendCI(L))) + + +/* macro to check stack size, preserving 'p' */ +#define checkstackp(L,n,p) \ + luaD_checkstackaux(L, n, \ + ptrdiff_t t__ = savestack(L, p); /* save 'p' */ \ + luaC_checkGC(L), /* stack grow uses memory */ \ + p = restorestack(L, t__)) /* 'pos' part: restore 'p' */ + + +/* +** Prepares a function call: checks the stack, creates a new CallInfo +** entry, fills in the relevant information, calls hook if needed. +** If function is a C function, does the call, too. (Otherwise, leave +** the execution ('luaV_execute') to the caller, to allow stackless +** calls.) Returns true iff function has been executed (C function). +*/ +int luaD_precall (lua_State *L, StkId func, int nresults) { + lua_CFunction f; + CallInfo *ci; + switch (ttype(func)) { + case LUA_TCCL: /* C closure */ + f = clCvalue(func)->f; + goto Cfunc; + case LUA_TLCF: /* light C function */ + f = fvalue(func); + Cfunc: { + int n; /* number of returns */ + checkstackp(L, LUA_MINSTACK, func); /* ensure minimum stack size */ + ci = next_ci(L); /* now 'enter' new function */ + ci->nresults = nresults; + ci->func = func; + ci->top = L->top + LUA_MINSTACK; + lua_assert(ci->top <= L->stack_last); + ci->callstatus = 0; + if (L->hookmask & LUA_MASKCALL) + luaD_hook(L, LUA_HOOKCALL, -1); + lua_unlock(L); + n = (*f)(L); /* do the actual call */ + lua_lock(L); + api_checknelems(L, n); + luaD_poscall(L, ci, L->top - n, n); + return 1; + } + case LUA_TLCL: { /* Lua function: prepare its call */ + StkId base; + Proto *p = clLvalue(func)->p; + int n = cast_int(L->top - func) - 1; /* number of real arguments */ + int fsize = p->maxstacksize; /* frame size */ + checkstackp(L, fsize, func); + if (p->is_vararg) + base = adjust_varargs(L, p, n); + else { /* non vararg function */ + for (; n < p->numparams; n++) + setnilvalue(L->top++); /* complete missing arguments */ + base = func + 1; + } + ci = next_ci(L); /* now 'enter' new function */ + ci->nresults = nresults; + ci->func = func; + ci->u.l.base = base; + L->top = ci->top = base + fsize; + lua_assert(ci->top <= L->stack_last); + ci->u.l.savedpc = p->code; /* starting point */ + ci->callstatus = CIST_LUA; + if (L->hookmask & LUA_MASKCALL) + callhook(L, ci); + return 0; + } + default: { /* not a function */ + checkstackp(L, 1, func); /* ensure space for metamethod */ + tryfuncTM(L, func); /* try to get '__call' metamethod */ + return luaD_precall(L, func, nresults); /* now it must be a function */ + } + } +} + + +/* +** Check appropriate error for stack overflow ("regular" overflow or +** overflow while handling stack overflow). If 'nCalls' is larger than +** LUAI_MAXCCALLS (which means it is handling a "regular" overflow) but +** smaller than 9/8 of LUAI_MAXCCALLS, does not report an error (to +** allow overflow handling to work) +*/ +static void stackerror (lua_State *L) { + if (L->nCcalls == LUAI_MAXCCALLS) + luaG_runerror(L, "C stack overflow"); + else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3))) + luaD_throw(L, LUA_ERRERR); /* error while handing stack error */ +} + + +/* +** Call a function (C or Lua). The function to be called is at *func. +** The arguments are on the stack, right after the function. +** When returns, all the results are on the stack, starting at the original +** function position. +*/ +void luaD_call (lua_State *L, StkId func, int nResults) { + if (++L->nCcalls >= LUAI_MAXCCALLS) + stackerror(L); + if (!luaD_precall(L, func, nResults)) /* is a Lua function? */ + luaV_execute(L); /* call it */ + L->nCcalls--; +} + + +/* +** Similar to 'luaD_call', but does not allow yields during the call +*/ +void luaD_callnoyield (lua_State *L, StkId func, int nResults) { + L->nny++; + luaD_call(L, func, nResults); + L->nny--; +} + + +/* +** Completes the execution of an interrupted C function, calling its +** continuation function. +*/ +static void finishCcall (lua_State *L, int status) { + CallInfo *ci = L->ci; + int n; + /* must have a continuation and must be able to call it */ + lua_assert(ci->u.c.k != NULL && L->nny == 0); + /* error status can only happen in a protected call */ + lua_assert((ci->callstatus & CIST_YPCALL) || status == LUA_YIELD); + if (ci->callstatus & CIST_YPCALL) { /* was inside a pcall? */ + ci->callstatus &= ~CIST_YPCALL; /* continuation is also inside it */ + L->errfunc = ci->u.c.old_errfunc; /* with the same error function */ + } + /* finish 'lua_callk'/'lua_pcall'; CIST_YPCALL and 'errfunc' already + handled */ + adjustresults(L, ci->nresults); + lua_unlock(L); + n = (*ci->u.c.k)(L, status, ci->u.c.ctx); /* call continuation function */ + lua_lock(L); + api_checknelems(L, n); + luaD_poscall(L, ci, L->top - n, n); /* finish 'luaD_precall' */ +} + + +/* +** Executes "full continuation" (everything in the stack) of a +** previously interrupted coroutine until the stack is empty (or another +** interruption long-jumps out of the loop). If the coroutine is +** recovering from an error, 'ud' points to the error status, which must +** be passed to the first continuation function (otherwise the default +** status is LUA_YIELD). +*/ +static void unroll (lua_State *L, void *ud) { + if (ud != NULL) /* error status? */ + finishCcall(L, *(int *)ud); /* finish 'lua_pcallk' callee */ + while (L->ci != &L->base_ci) { /* something in the stack */ + if (!isLua(L->ci)) /* C function? */ + finishCcall(L, LUA_YIELD); /* complete its execution */ + else { /* Lua function */ + luaV_finishOp(L); /* finish interrupted instruction */ + luaV_execute(L); /* execute down to higher C 'boundary' */ + } + } +} + + +/* +** Try to find a suspended protected call (a "recover point") for the +** given thread. +*/ +static CallInfo *findpcall (lua_State *L) { + CallInfo *ci; + for (ci = L->ci; ci != NULL; ci = ci->previous) { /* search for a pcall */ + if (ci->callstatus & CIST_YPCALL) + return ci; + } + return NULL; /* no pending pcall */ +} + + +/* +** Recovers from an error in a coroutine. Finds a recover point (if +** there is one) and completes the execution of the interrupted +** 'luaD_pcall'. If there is no recover point, returns zero. +*/ +static int recover (lua_State *L, int status) { + StkId oldtop; + CallInfo *ci = findpcall(L); + if (ci == NULL) return 0; /* no recovery point */ + /* "finish" luaD_pcall */ + oldtop = restorestack(L, ci->extra); + luaF_close(L, oldtop); + seterrorobj(L, status, oldtop); + L->ci = ci; + L->allowhook = getoah(ci->callstatus); /* restore original 'allowhook' */ + L->nny = 0; /* should be zero to be yieldable */ + luaD_shrinkstack(L); + L->errfunc = ci->u.c.old_errfunc; + return 1; /* continue running the coroutine */ +} + + +/* +** Signal an error in the call to 'lua_resume', not in the execution +** of the coroutine itself. (Such errors should not be handled by any +** coroutine error handler and should not kill the coroutine.) +*/ +static int resume_error (lua_State *L, const char *msg, int narg) { + L->top -= narg; /* remove args from the stack */ + setsvalue2s(L, L->top, luaS_new(L, msg)); /* push error message */ + api_incr_top(L); + lua_unlock(L); + return LUA_ERRRUN; +} + + +/* +** Do the work for 'lua_resume' in protected mode. Most of the work +** depends on the status of the coroutine: initial state, suspended +** inside a hook, or regularly suspended (optionally with a continuation +** function), plus erroneous cases: non-suspended coroutine or dead +** coroutine. +*/ +static void resume (lua_State *L, void *ud) { + int n = *(cast(int*, ud)); /* number of arguments */ + StkId firstArg = L->top - n; /* first argument */ + CallInfo *ci = L->ci; + if (L->status == LUA_OK) { /* starting a coroutine? */ + if (!luaD_precall(L, firstArg - 1, LUA_MULTRET)) /* Lua function? */ + luaV_execute(L); /* call it */ + } + else { /* resuming from previous yield */ + lua_assert(L->status == LUA_YIELD); + L->status = LUA_OK; /* mark that it is running (again) */ + ci->func = restorestack(L, ci->extra); + if (isLua(ci)) /* yielded inside a hook? */ + luaV_execute(L); /* just continue running Lua code */ + else { /* 'common' yield */ + if (ci->u.c.k != NULL) { /* does it have a continuation function? */ + lua_unlock(L); + n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */ + lua_lock(L); + api_checknelems(L, n); + firstArg = L->top - n; /* yield results come from continuation */ + } + luaD_poscall(L, ci, firstArg, n); /* finish 'luaD_precall' */ + } + unroll(L, NULL); /* run continuation */ + } +} + + +LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs) { + int status; + unsigned short oldnny = L->nny; /* save "number of non-yieldable" calls */ + lua_lock(L); + if (L->status == LUA_OK) { /* may be starting a coroutine */ + if (L->ci != &L->base_ci) /* not in base level? */ + return resume_error(L, "cannot resume non-suspended coroutine", nargs); + } + else if (L->status != LUA_YIELD) + return resume_error(L, "cannot resume dead coroutine", nargs); + L->nCcalls = (from) ? from->nCcalls + 1 : 1; + if (L->nCcalls >= LUAI_MAXCCALLS) + return resume_error(L, "C stack overflow", nargs); + luai_userstateresume(L, nargs); + L->nny = 0; /* allow yields */ + api_checknelems(L, (L->status == LUA_OK) ? nargs + 1 : nargs); + status = luaD_rawrunprotected(L, resume, &nargs); + if (status == -1) /* error calling 'lua_resume'? */ + status = LUA_ERRRUN; + else { /* continue running after recoverable errors */ + while (errorstatus(status) && recover(L, status)) { + /* unroll continuation */ + status = luaD_rawrunprotected(L, unroll, &status); + } + if (errorstatus(status)) { /* unrecoverable error? */ + L->status = cast_byte(status); /* mark thread as 'dead' */ + seterrorobj(L, status, L->top); /* push error message */ + L->ci->top = L->top; + } + else lua_assert(status == L->status); /* normal end or yield */ + } + L->nny = oldnny; /* restore 'nny' */ + L->nCcalls--; + lua_assert(L->nCcalls == ((from) ? from->nCcalls : 0)); + lua_unlock(L); + return status; +} + + +LUA_API int lua_isyieldable (lua_State *L) { + return (L->nny == 0); +} + + +LUA_API int lua_yieldk (lua_State *L, int nresults, lua_KContext ctx, + lua_KFunction k) { + CallInfo *ci = L->ci; + luai_userstateyield(L, nresults); + lua_lock(L); + api_checknelems(L, nresults); + if (L->nny > 0) { + if (L != G(L)->mainthread) + luaG_runerror(L, "attempt to yield across a C-call boundary"); + else + luaG_runerror(L, "attempt to yield from outside a coroutine"); + } + L->status = LUA_YIELD; + ci->extra = savestack(L, ci->func); /* save current 'func' */ + if (isLua(ci)) { /* inside a hook? */ + api_check(L, k == NULL, "hooks cannot continue after yielding"); + } + else { + if ((ci->u.c.k = k) != NULL) /* is there a continuation? */ + ci->u.c.ctx = ctx; /* save context */ + ci->func = L->top - nresults - 1; /* protect stack below results */ + luaD_throw(L, LUA_YIELD); + } + lua_assert(ci->callstatus & CIST_HOOKED); /* must be inside a hook */ + lua_unlock(L); + return 0; /* return to 'luaD_hook' */ +} + + +int luaD_pcall (lua_State *L, Pfunc func, void *u, + ptrdiff_t old_top, ptrdiff_t ef) { + int status; + CallInfo *old_ci = L->ci; + lu_byte old_allowhooks = L->allowhook; + unsigned short old_nny = L->nny; + ptrdiff_t old_errfunc = L->errfunc; + L->errfunc = ef; + status = luaD_rawrunprotected(L, func, u); + if (status != LUA_OK) { /* an error occurred? */ + StkId oldtop = restorestack(L, old_top); + luaF_close(L, oldtop); /* close possible pending closures */ + seterrorobj(L, status, oldtop); + L->ci = old_ci; + L->allowhook = old_allowhooks; + L->nny = old_nny; + luaD_shrinkstack(L); + } + L->errfunc = old_errfunc; + return status; +} + + + +/* +** Execute a protected parser. +*/ +struct SParser { /* data to 'f_parser' */ + ZIO *z; + Mbuffer buff; /* dynamic structure used by the scanner */ + Dyndata dyd; /* dynamic structures used by the parser */ + const char *mode; + const char *name; +}; + + +static void checkmode (lua_State *L, const char *mode, const char *x) { + if (mode && strchr(mode, x[0]) == NULL) { + luaO_pushfstring(L, + "attempt to load a %s chunk (mode is '%s')", x, mode); + luaD_throw(L, LUA_ERRSYNTAX); + } +} + + +static void f_parser (lua_State *L, void *ud) { + LClosure *cl; + struct SParser *p = cast(struct SParser *, ud); + int c = zgetc(p->z); /* read first character */ + if (c == LUA_SIGNATURE[0]) { + checkmode(L, p->mode, "binary"); + cl = luaU_undump(L, p->z, p->name); + } + else { + checkmode(L, p->mode, "text"); + cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c); + } + lua_assert(cl->nupvalues == cl->p->sizeupvalues); + luaF_initupvals(L, cl); +} + + +int luaD_protectedparser (lua_State *L, ZIO *z, const char *name, + const char *mode) { + struct SParser p; + int status; + L->nny++; /* cannot yield during parsing */ + p.z = z; p.name = name; p.mode = mode; + p.dyd.actvar.arr = NULL; p.dyd.actvar.size = 0; + p.dyd.gt.arr = NULL; p.dyd.gt.size = 0; + p.dyd.label.arr = NULL; p.dyd.label.size = 0; + luaZ_initbuffer(L, &p.buff); + status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc); + luaZ_freebuffer(L, &p.buff); + luaM_freearray(L, p.dyd.actvar.arr, p.dyd.actvar.size); + luaM_freearray(L, p.dyd.gt.arr, p.dyd.gt.size); + luaM_freearray(L, p.dyd.label.arr, p.dyd.label.size); + L->nny--; + return status; +} + + diff --git a/src/rcheevos/test/lua/src/ldo.h b/src/rcheevos/test/lua/src/ldo.h new file mode 100644 index 000000000..4f5d51c3c --- /dev/null +++ b/src/rcheevos/test/lua/src/ldo.h @@ -0,0 +1,58 @@ +/* +** $Id: ldo.h,v 2.29 2015/12/21 13:02:14 roberto Exp $ +** Stack and Call structure of Lua +** See Copyright Notice in lua.h +*/ + +#ifndef ldo_h +#define ldo_h + + +#include "lobject.h" +#include "lstate.h" +#include "lzio.h" + + +/* +** Macro to check stack size and grow stack if needed. Parameters +** 'pre'/'pos' allow the macro to preserve a pointer into the +** stack across reallocations, doing the work only when needed. +** 'condmovestack' is used in heavy tests to force a stack reallocation +** at every check. +*/ +#define luaD_checkstackaux(L,n,pre,pos) \ + if (L->stack_last - L->top <= (n)) \ + { pre; luaD_growstack(L, n); pos; } else { condmovestack(L,pre,pos); } + +/* In general, 'pre'/'pos' are empty (nothing to save) */ +#define luaD_checkstack(L,n) luaD_checkstackaux(L,n,(void)0,(void)0) + + + +#define savestack(L,p) ((char *)(p) - (char *)L->stack) +#define restorestack(L,n) ((TValue *)((char *)L->stack + (n))) + + +/* type of protected functions, to be ran by 'runprotected' */ +typedef void (*Pfunc) (lua_State *L, void *ud); + +LUAI_FUNC int luaD_protectedparser (lua_State *L, ZIO *z, const char *name, + const char *mode); +LUAI_FUNC void luaD_hook (lua_State *L, int event, int line); +LUAI_FUNC int luaD_precall (lua_State *L, StkId func, int nresults); +LUAI_FUNC void luaD_call (lua_State *L, StkId func, int nResults); +LUAI_FUNC void luaD_callnoyield (lua_State *L, StkId func, int nResults); +LUAI_FUNC int luaD_pcall (lua_State *L, Pfunc func, void *u, + ptrdiff_t oldtop, ptrdiff_t ef); +LUAI_FUNC int luaD_poscall (lua_State *L, CallInfo *ci, StkId firstResult, + int nres); +LUAI_FUNC void luaD_reallocstack (lua_State *L, int newsize); +LUAI_FUNC void luaD_growstack (lua_State *L, int n); +LUAI_FUNC void luaD_shrinkstack (lua_State *L); +LUAI_FUNC void luaD_inctop (lua_State *L); + +LUAI_FUNC l_noret luaD_throw (lua_State *L, int errcode); +LUAI_FUNC int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud); + +#endif + diff --git a/src/rcheevos/test/lua/src/ldump.c b/src/rcheevos/test/lua/src/ldump.c new file mode 100644 index 000000000..016e30082 --- /dev/null +++ b/src/rcheevos/test/lua/src/ldump.c @@ -0,0 +1,215 @@ +/* +** $Id: ldump.c,v 2.37 2015/10/08 15:53:49 roberto Exp $ +** save precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#define ldump_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lobject.h" +#include "lstate.h" +#include "lundump.h" + + +typedef struct { + lua_State *L; + lua_Writer writer; + void *data; + int strip; + int status; +} DumpState; + + +/* +** All high-level dumps go through DumpVector; you can change it to +** change the endianness of the result +*/ +#define DumpVector(v,n,D) DumpBlock(v,(n)*sizeof((v)[0]),D) + +#define DumpLiteral(s,D) DumpBlock(s, sizeof(s) - sizeof(char), D) + + +static void DumpBlock (const void *b, size_t size, DumpState *D) { + if (D->status == 0 && size > 0) { + lua_unlock(D->L); + D->status = (*D->writer)(D->L, b, size, D->data); + lua_lock(D->L); + } +} + + +#define DumpVar(x,D) DumpVector(&x,1,D) + + +static void DumpByte (int y, DumpState *D) { + lu_byte x = (lu_byte)y; + DumpVar(x, D); +} + + +static void DumpInt (int x, DumpState *D) { + DumpVar(x, D); +} + + +static void DumpNumber (lua_Number x, DumpState *D) { + DumpVar(x, D); +} + + +static void DumpInteger (lua_Integer x, DumpState *D) { + DumpVar(x, D); +} + + +static void DumpString (const TString *s, DumpState *D) { + if (s == NULL) + DumpByte(0, D); + else { + size_t size = tsslen(s) + 1; /* include trailing '\0' */ + const char *str = getstr(s); + if (size < 0xFF) + DumpByte(cast_int(size), D); + else { + DumpByte(0xFF, D); + DumpVar(size, D); + } + DumpVector(str, size - 1, D); /* no need to save '\0' */ + } +} + + +static void DumpCode (const Proto *f, DumpState *D) { + DumpInt(f->sizecode, D); + DumpVector(f->code, f->sizecode, D); +} + + +static void DumpFunction(const Proto *f, TString *psource, DumpState *D); + +static void DumpConstants (const Proto *f, DumpState *D) { + int i; + int n = f->sizek; + DumpInt(n, D); + for (i = 0; i < n; i++) { + const TValue *o = &f->k[i]; + DumpByte(ttype(o), D); + switch (ttype(o)) { + case LUA_TNIL: + break; + case LUA_TBOOLEAN: + DumpByte(bvalue(o), D); + break; + case LUA_TNUMFLT: + DumpNumber(fltvalue(o), D); + break; + case LUA_TNUMINT: + DumpInteger(ivalue(o), D); + break; + case LUA_TSHRSTR: + case LUA_TLNGSTR: + DumpString(tsvalue(o), D); + break; + default: + lua_assert(0); + } + } +} + + +static void DumpProtos (const Proto *f, DumpState *D) { + int i; + int n = f->sizep; + DumpInt(n, D); + for (i = 0; i < n; i++) + DumpFunction(f->p[i], f->source, D); +} + + +static void DumpUpvalues (const Proto *f, DumpState *D) { + int i, n = f->sizeupvalues; + DumpInt(n, D); + for (i = 0; i < n; i++) { + DumpByte(f->upvalues[i].instack, D); + DumpByte(f->upvalues[i].idx, D); + } +} + + +static void DumpDebug (const Proto *f, DumpState *D) { + int i, n; + n = (D->strip) ? 0 : f->sizelineinfo; + DumpInt(n, D); + DumpVector(f->lineinfo, n, D); + n = (D->strip) ? 0 : f->sizelocvars; + DumpInt(n, D); + for (i = 0; i < n; i++) { + DumpString(f->locvars[i].varname, D); + DumpInt(f->locvars[i].startpc, D); + DumpInt(f->locvars[i].endpc, D); + } + n = (D->strip) ? 0 : f->sizeupvalues; + DumpInt(n, D); + for (i = 0; i < n; i++) + DumpString(f->upvalues[i].name, D); +} + + +static void DumpFunction (const Proto *f, TString *psource, DumpState *D) { + if (D->strip || f->source == psource) + DumpString(NULL, D); /* no debug info or same source as its parent */ + else + DumpString(f->source, D); + DumpInt(f->linedefined, D); + DumpInt(f->lastlinedefined, D); + DumpByte(f->numparams, D); + DumpByte(f->is_vararg, D); + DumpByte(f->maxstacksize, D); + DumpCode(f, D); + DumpConstants(f, D); + DumpUpvalues(f, D); + DumpProtos(f, D); + DumpDebug(f, D); +} + + +static void DumpHeader (DumpState *D) { + DumpLiteral(LUA_SIGNATURE, D); + DumpByte(LUAC_VERSION, D); + DumpByte(LUAC_FORMAT, D); + DumpLiteral(LUAC_DATA, D); + DumpByte(sizeof(int), D); + DumpByte(sizeof(size_t), D); + DumpByte(sizeof(Instruction), D); + DumpByte(sizeof(lua_Integer), D); + DumpByte(sizeof(lua_Number), D); + DumpInteger(LUAC_INT, D); + DumpNumber(LUAC_NUM, D); +} + + +/* +** dump Lua function as precompiled chunk +*/ +int luaU_dump(lua_State *L, const Proto *f, lua_Writer w, void *data, + int strip) { + DumpState D; + D.L = L; + D.writer = w; + D.data = data; + D.strip = strip; + D.status = 0; + DumpHeader(&D); + DumpByte(f->sizeupvalues, &D); + DumpFunction(f, NULL, &D); + return D.status; +} + diff --git a/src/rcheevos/test/lua/src/lfunc.c b/src/rcheevos/test/lua/src/lfunc.c new file mode 100644 index 000000000..67967dab3 --- /dev/null +++ b/src/rcheevos/test/lua/src/lfunc.c @@ -0,0 +1,151 @@ +/* +** $Id: lfunc.c,v 2.45 2014/11/02 19:19:04 roberto Exp $ +** Auxiliary functions to manipulate prototypes and closures +** See Copyright Notice in lua.h +*/ + +#define lfunc_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" + + + +CClosure *luaF_newCclosure (lua_State *L, int n) { + GCObject *o = luaC_newobj(L, LUA_TCCL, sizeCclosure(n)); + CClosure *c = gco2ccl(o); + c->nupvalues = cast_byte(n); + return c; +} + + +LClosure *luaF_newLclosure (lua_State *L, int n) { + GCObject *o = luaC_newobj(L, LUA_TLCL, sizeLclosure(n)); + LClosure *c = gco2lcl(o); + c->p = NULL; + c->nupvalues = cast_byte(n); + while (n--) c->upvals[n] = NULL; + return c; +} + +/* +** fill a closure with new closed upvalues +*/ +void luaF_initupvals (lua_State *L, LClosure *cl) { + int i; + for (i = 0; i < cl->nupvalues; i++) { + UpVal *uv = luaM_new(L, UpVal); + uv->refcount = 1; + uv->v = &uv->u.value; /* make it closed */ + setnilvalue(uv->v); + cl->upvals[i] = uv; + } +} + + +UpVal *luaF_findupval (lua_State *L, StkId level) { + UpVal **pp = &L->openupval; + UpVal *p; + UpVal *uv; + lua_assert(isintwups(L) || L->openupval == NULL); + while (*pp != NULL && (p = *pp)->v >= level) { + lua_assert(upisopen(p)); + if (p->v == level) /* found a corresponding upvalue? */ + return p; /* return it */ + pp = &p->u.open.next; + } + /* not found: create a new upvalue */ + uv = luaM_new(L, UpVal); + uv->refcount = 0; + uv->u.open.next = *pp; /* link it to list of open upvalues */ + uv->u.open.touched = 1; + *pp = uv; + uv->v = level; /* current value lives in the stack */ + if (!isintwups(L)) { /* thread not in list of threads with upvalues? */ + L->twups = G(L)->twups; /* link it to the list */ + G(L)->twups = L; + } + return uv; +} + + +void luaF_close (lua_State *L, StkId level) { + UpVal *uv; + while (L->openupval != NULL && (uv = L->openupval)->v >= level) { + lua_assert(upisopen(uv)); + L->openupval = uv->u.open.next; /* remove from 'open' list */ + if (uv->refcount == 0) /* no references? */ + luaM_free(L, uv); /* free upvalue */ + else { + setobj(L, &uv->u.value, uv->v); /* move value to upvalue slot */ + uv->v = &uv->u.value; /* now current value lives here */ + luaC_upvalbarrier(L, uv); + } + } +} + + +Proto *luaF_newproto (lua_State *L) { + GCObject *o = luaC_newobj(L, LUA_TPROTO, sizeof(Proto)); + Proto *f = gco2p(o); + f->k = NULL; + f->sizek = 0; + f->p = NULL; + f->sizep = 0; + f->code = NULL; + f->cache = NULL; + f->sizecode = 0; + f->lineinfo = NULL; + f->sizelineinfo = 0; + f->upvalues = NULL; + f->sizeupvalues = 0; + f->numparams = 0; + f->is_vararg = 0; + f->maxstacksize = 0; + f->locvars = NULL; + f->sizelocvars = 0; + f->linedefined = 0; + f->lastlinedefined = 0; + f->source = NULL; + return f; +} + + +void luaF_freeproto (lua_State *L, Proto *f) { + luaM_freearray(L, f->code, f->sizecode); + luaM_freearray(L, f->p, f->sizep); + luaM_freearray(L, f->k, f->sizek); + luaM_freearray(L, f->lineinfo, f->sizelineinfo); + luaM_freearray(L, f->locvars, f->sizelocvars); + luaM_freearray(L, f->upvalues, f->sizeupvalues); + luaM_free(L, f); +} + + +/* +** Look for n-th local variable at line 'line' in function 'func'. +** Returns NULL if not found. +*/ +const char *luaF_getlocalname (const Proto *f, int local_number, int pc) { + int i; + for (i = 0; isizelocvars && f->locvars[i].startpc <= pc; i++) { + if (pc < f->locvars[i].endpc) { /* is variable active? */ + local_number--; + if (local_number == 0) + return getstr(f->locvars[i].varname); + } + } + return NULL; /* not found */ +} + diff --git a/src/rcheevos/test/lua/src/lfunc.h b/src/rcheevos/test/lua/src/lfunc.h new file mode 100644 index 000000000..2eeb0d5a4 --- /dev/null +++ b/src/rcheevos/test/lua/src/lfunc.h @@ -0,0 +1,61 @@ +/* +** $Id: lfunc.h,v 2.15 2015/01/13 15:49:11 roberto Exp $ +** Auxiliary functions to manipulate prototypes and closures +** See Copyright Notice in lua.h +*/ + +#ifndef lfunc_h +#define lfunc_h + + +#include "lobject.h" + + +#define sizeCclosure(n) (cast(int, sizeof(CClosure)) + \ + cast(int, sizeof(TValue)*((n)-1))) + +#define sizeLclosure(n) (cast(int, sizeof(LClosure)) + \ + cast(int, sizeof(TValue *)*((n)-1))) + + +/* test whether thread is in 'twups' list */ +#define isintwups(L) (L->twups != L) + + +/* +** maximum number of upvalues in a closure (both C and Lua). (Value +** must fit in a VM register.) +*/ +#define MAXUPVAL 255 + + +/* +** Upvalues for Lua closures +*/ +struct UpVal { + TValue *v; /* points to stack or to its own value */ + lu_mem refcount; /* reference counter */ + union { + struct { /* (when open) */ + UpVal *next; /* linked list */ + int touched; /* mark to avoid cycles with dead threads */ + } open; + TValue value; /* the value (when closed) */ + } u; +}; + +#define upisopen(up) ((up)->v != &(up)->u.value) + + +LUAI_FUNC Proto *luaF_newproto (lua_State *L); +LUAI_FUNC CClosure *luaF_newCclosure (lua_State *L, int nelems); +LUAI_FUNC LClosure *luaF_newLclosure (lua_State *L, int nelems); +LUAI_FUNC void luaF_initupvals (lua_State *L, LClosure *cl); +LUAI_FUNC UpVal *luaF_findupval (lua_State *L, StkId level); +LUAI_FUNC void luaF_close (lua_State *L, StkId level); +LUAI_FUNC void luaF_freeproto (lua_State *L, Proto *f); +LUAI_FUNC const char *luaF_getlocalname (const Proto *func, int local_number, + int pc); + + +#endif diff --git a/src/rcheevos/test/lua/src/lgc.c b/src/rcheevos/test/lua/src/lgc.c new file mode 100644 index 000000000..ba2c19e14 --- /dev/null +++ b/src/rcheevos/test/lua/src/lgc.c @@ -0,0 +1,1178 @@ +/* +** $Id: lgc.c,v 2.215 2016/12/22 13:08:50 roberto Exp $ +** Garbage Collector +** See Copyright Notice in lua.h +*/ + +#define lgc_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + +/* +** internal state for collector while inside the atomic phase. The +** collector should never be in this state while running regular code. +*/ +#define GCSinsideatomic (GCSpause + 1) + +/* +** cost of sweeping one element (the size of a small object divided +** by some adjust for the sweep speed) +*/ +#define GCSWEEPCOST ((sizeof(TString) + 4) / 4) + +/* maximum number of elements to sweep in each single step */ +#define GCSWEEPMAX (cast_int((GCSTEPSIZE / GCSWEEPCOST) / 4)) + +/* cost of calling one finalizer */ +#define GCFINALIZECOST GCSWEEPCOST + + +/* +** macro to adjust 'stepmul': 'stepmul' is actually used like +** 'stepmul / STEPMULADJ' (value chosen by tests) +*/ +#define STEPMULADJ 200 + + +/* +** macro to adjust 'pause': 'pause' is actually used like +** 'pause / PAUSEADJ' (value chosen by tests) +*/ +#define PAUSEADJ 100 + + +/* +** 'makewhite' erases all color bits then sets only the current white +** bit +*/ +#define maskcolors (~(bitmask(BLACKBIT) | WHITEBITS)) +#define makewhite(g,x) \ + (x->marked = cast_byte((x->marked & maskcolors) | luaC_white(g))) + +#define white2gray(x) resetbits(x->marked, WHITEBITS) +#define black2gray(x) resetbit(x->marked, BLACKBIT) + + +#define valiswhite(x) (iscollectable(x) && iswhite(gcvalue(x))) + +#define checkdeadkey(n) lua_assert(!ttisdeadkey(gkey(n)) || ttisnil(gval(n))) + + +#define checkconsistency(obj) \ + lua_longassert(!iscollectable(obj) || righttt(obj)) + + +#define markvalue(g,o) { checkconsistency(o); \ + if (valiswhite(o)) reallymarkobject(g,gcvalue(o)); } + +#define markobject(g,t) { if (iswhite(t)) reallymarkobject(g, obj2gco(t)); } + +/* +** mark an object that can be NULL (either because it is really optional, +** or it was stripped as debug info, or inside an uncompleted structure) +*/ +#define markobjectN(g,t) { if (t) markobject(g,t); } + +static void reallymarkobject (global_State *g, GCObject *o); + + +/* +** {====================================================== +** Generic functions +** ======================================================= +*/ + + +/* +** one after last element in a hash array +*/ +#define gnodelast(h) gnode(h, cast(size_t, sizenode(h))) + + +/* +** link collectable object 'o' into list pointed by 'p' +*/ +#define linkgclist(o,p) ((o)->gclist = (p), (p) = obj2gco(o)) + + +/* +** If key is not marked, mark its entry as dead. This allows key to be +** collected, but keeps its entry in the table. A dead node is needed +** when Lua looks up for a key (it may be part of a chain) and when +** traversing a weak table (key might be removed from the table during +** traversal). Other places never manipulate dead keys, because its +** associated nil value is enough to signal that the entry is logically +** empty. +*/ +static void removeentry (Node *n) { + lua_assert(ttisnil(gval(n))); + if (valiswhite(gkey(n))) + setdeadvalue(wgkey(n)); /* unused and unmarked key; remove it */ +} + + +/* +** tells whether a key or value can be cleared from a weak +** table. Non-collectable objects are never removed from weak +** tables. Strings behave as 'values', so are never removed too. for +** other objects: if really collected, cannot keep them; for objects +** being finalized, keep them in keys, but not in values +*/ +static int iscleared (global_State *g, const TValue *o) { + if (!iscollectable(o)) return 0; + else if (ttisstring(o)) { + markobject(g, tsvalue(o)); /* strings are 'values', so are never weak */ + return 0; + } + else return iswhite(gcvalue(o)); +} + + +/* +** barrier that moves collector forward, that is, mark the white object +** being pointed by a black object. (If in sweep phase, clear the black +** object to white [sweep it] to avoid other barrier calls for this +** same object.) +*/ +void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v) { + global_State *g = G(L); + lua_assert(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o)); + if (keepinvariant(g)) /* must keep invariant? */ + reallymarkobject(g, v); /* restore invariant */ + else { /* sweep phase */ + lua_assert(issweepphase(g)); + makewhite(g, o); /* mark main obj. as white to avoid other barriers */ + } +} + + +/* +** barrier that moves collector backward, that is, mark the black object +** pointing to a white object as gray again. +*/ +void luaC_barrierback_ (lua_State *L, Table *t) { + global_State *g = G(L); + lua_assert(isblack(t) && !isdead(g, t)); + black2gray(t); /* make table gray (again) */ + linkgclist(t, g->grayagain); +} + + +/* +** barrier for assignments to closed upvalues. Because upvalues are +** shared among closures, it is impossible to know the color of all +** closures pointing to it. So, we assume that the object being assigned +** must be marked. +*/ +void luaC_upvalbarrier_ (lua_State *L, UpVal *uv) { + global_State *g = G(L); + GCObject *o = gcvalue(uv->v); + lua_assert(!upisopen(uv)); /* ensured by macro luaC_upvalbarrier */ + if (keepinvariant(g)) + markobject(g, o); +} + + +void luaC_fix (lua_State *L, GCObject *o) { + global_State *g = G(L); + lua_assert(g->allgc == o); /* object must be 1st in 'allgc' list! */ + white2gray(o); /* they will be gray forever */ + g->allgc = o->next; /* remove object from 'allgc' list */ + o->next = g->fixedgc; /* link it to 'fixedgc' list */ + g->fixedgc = o; +} + + +/* +** create a new collectable object (with given type and size) and link +** it to 'allgc' list. +*/ +GCObject *luaC_newobj (lua_State *L, int tt, size_t sz) { + global_State *g = G(L); + GCObject *o = cast(GCObject *, luaM_newobject(L, novariant(tt), sz)); + o->marked = luaC_white(g); + o->tt = tt; + o->next = g->allgc; + g->allgc = o; + return o; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** Mark functions +** ======================================================= +*/ + + +/* +** mark an object. Userdata, strings, and closed upvalues are visited +** and turned black here. Other objects are marked gray and added +** to appropriate list to be visited (and turned black) later. (Open +** upvalues are already linked in 'headuv' list.) +*/ +static void reallymarkobject (global_State *g, GCObject *o) { + reentry: + white2gray(o); + switch (o->tt) { + case LUA_TSHRSTR: { + gray2black(o); + g->GCmemtrav += sizelstring(gco2ts(o)->shrlen); + break; + } + case LUA_TLNGSTR: { + gray2black(o); + g->GCmemtrav += sizelstring(gco2ts(o)->u.lnglen); + break; + } + case LUA_TUSERDATA: { + TValue uvalue; + markobjectN(g, gco2u(o)->metatable); /* mark its metatable */ + gray2black(o); + g->GCmemtrav += sizeudata(gco2u(o)); + getuservalue(g->mainthread, gco2u(o), &uvalue); + if (valiswhite(&uvalue)) { /* markvalue(g, &uvalue); */ + o = gcvalue(&uvalue); + goto reentry; + } + break; + } + case LUA_TLCL: { + linkgclist(gco2lcl(o), g->gray); + break; + } + case LUA_TCCL: { + linkgclist(gco2ccl(o), g->gray); + break; + } + case LUA_TTABLE: { + linkgclist(gco2t(o), g->gray); + break; + } + case LUA_TTHREAD: { + linkgclist(gco2th(o), g->gray); + break; + } + case LUA_TPROTO: { + linkgclist(gco2p(o), g->gray); + break; + } + default: lua_assert(0); break; + } +} + + +/* +** mark metamethods for basic types +*/ +static void markmt (global_State *g) { + int i; + for (i=0; i < LUA_NUMTAGS; i++) + markobjectN(g, g->mt[i]); +} + + +/* +** mark all objects in list of being-finalized +*/ +static void markbeingfnz (global_State *g) { + GCObject *o; + for (o = g->tobefnz; o != NULL; o = o->next) + markobject(g, o); +} + + +/* +** Mark all values stored in marked open upvalues from non-marked threads. +** (Values from marked threads were already marked when traversing the +** thread.) Remove from the list threads that no longer have upvalues and +** not-marked threads. +*/ +static void remarkupvals (global_State *g) { + lua_State *thread; + lua_State **p = &g->twups; + while ((thread = *p) != NULL) { + lua_assert(!isblack(thread)); /* threads are never black */ + if (isgray(thread) && thread->openupval != NULL) + p = &thread->twups; /* keep marked thread with upvalues in the list */ + else { /* thread is not marked or without upvalues */ + UpVal *uv; + *p = thread->twups; /* remove thread from the list */ + thread->twups = thread; /* mark that it is out of list */ + for (uv = thread->openupval; uv != NULL; uv = uv->u.open.next) { + if (uv->u.open.touched) { + markvalue(g, uv->v); /* remark upvalue's value */ + uv->u.open.touched = 0; + } + } + } + } +} + + +/* +** mark root set and reset all gray lists, to start a new collection +*/ +static void restartcollection (global_State *g) { + g->gray = g->grayagain = NULL; + g->weak = g->allweak = g->ephemeron = NULL; + markobject(g, g->mainthread); + markvalue(g, &g->l_registry); + markmt(g); + markbeingfnz(g); /* mark any finalizing object left from previous cycle */ +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Traverse functions +** ======================================================= +*/ + +/* +** Traverse a table with weak values and link it to proper list. During +** propagate phase, keep it in 'grayagain' list, to be revisited in the +** atomic phase. In the atomic phase, if table has any white value, +** put it in 'weak' list, to be cleared. +*/ +static void traverseweakvalue (global_State *g, Table *h) { + Node *n, *limit = gnodelast(h); + /* if there is array part, assume it may have white values (it is not + worth traversing it now just to check) */ + int hasclears = (h->sizearray > 0); + for (n = gnode(h, 0); n < limit; n++) { /* traverse hash part */ + checkdeadkey(n); + if (ttisnil(gval(n))) /* entry is empty? */ + removeentry(n); /* remove it */ + else { + lua_assert(!ttisnil(gkey(n))); + markvalue(g, gkey(n)); /* mark key */ + if (!hasclears && iscleared(g, gval(n))) /* is there a white value? */ + hasclears = 1; /* table will have to be cleared */ + } + } + if (g->gcstate == GCSpropagate) + linkgclist(h, g->grayagain); /* must retraverse it in atomic phase */ + else if (hasclears) + linkgclist(h, g->weak); /* has to be cleared later */ +} + + +/* +** Traverse an ephemeron table and link it to proper list. Returns true +** iff any object was marked during this traversal (which implies that +** convergence has to continue). During propagation phase, keep table +** in 'grayagain' list, to be visited again in the atomic phase. In +** the atomic phase, if table has any white->white entry, it has to +** be revisited during ephemeron convergence (as that key may turn +** black). Otherwise, if it has any white key, table has to be cleared +** (in the atomic phase). +*/ +static int traverseephemeron (global_State *g, Table *h) { + int marked = 0; /* true if an object is marked in this traversal */ + int hasclears = 0; /* true if table has white keys */ + int hasww = 0; /* true if table has entry "white-key -> white-value" */ + Node *n, *limit = gnodelast(h); + unsigned int i; + /* traverse array part */ + for (i = 0; i < h->sizearray; i++) { + if (valiswhite(&h->array[i])) { + marked = 1; + reallymarkobject(g, gcvalue(&h->array[i])); + } + } + /* traverse hash part */ + for (n = gnode(h, 0); n < limit; n++) { + checkdeadkey(n); + if (ttisnil(gval(n))) /* entry is empty? */ + removeentry(n); /* remove it */ + else if (iscleared(g, gkey(n))) { /* key is not marked (yet)? */ + hasclears = 1; /* table must be cleared */ + if (valiswhite(gval(n))) /* value not marked yet? */ + hasww = 1; /* white-white entry */ + } + else if (valiswhite(gval(n))) { /* value not marked yet? */ + marked = 1; + reallymarkobject(g, gcvalue(gval(n))); /* mark it now */ + } + } + /* link table into proper list */ + if (g->gcstate == GCSpropagate) + linkgclist(h, g->grayagain); /* must retraverse it in atomic phase */ + else if (hasww) /* table has white->white entries? */ + linkgclist(h, g->ephemeron); /* have to propagate again */ + else if (hasclears) /* table has white keys? */ + linkgclist(h, g->allweak); /* may have to clean white keys */ + return marked; +} + + +static void traversestrongtable (global_State *g, Table *h) { + Node *n, *limit = gnodelast(h); + unsigned int i; + for (i = 0; i < h->sizearray; i++) /* traverse array part */ + markvalue(g, &h->array[i]); + for (n = gnode(h, 0); n < limit; n++) { /* traverse hash part */ + checkdeadkey(n); + if (ttisnil(gval(n))) /* entry is empty? */ + removeentry(n); /* remove it */ + else { + lua_assert(!ttisnil(gkey(n))); + markvalue(g, gkey(n)); /* mark key */ + markvalue(g, gval(n)); /* mark value */ + } + } +} + + +static lu_mem traversetable (global_State *g, Table *h) { + const char *weakkey, *weakvalue; + const TValue *mode = gfasttm(g, h->metatable, TM_MODE); + markobjectN(g, h->metatable); + if (mode && ttisstring(mode) && /* is there a weak mode? */ + ((weakkey = strchr(svalue(mode), 'k')), + (weakvalue = strchr(svalue(mode), 'v')), + (weakkey || weakvalue))) { /* is really weak? */ + black2gray(h); /* keep table gray */ + if (!weakkey) /* strong keys? */ + traverseweakvalue(g, h); + else if (!weakvalue) /* strong values? */ + traverseephemeron(g, h); + else /* all weak */ + linkgclist(h, g->allweak); /* nothing to traverse now */ + } + else /* not weak */ + traversestrongtable(g, h); + return sizeof(Table) + sizeof(TValue) * h->sizearray + + sizeof(Node) * cast(size_t, allocsizenode(h)); +} + + +/* +** Traverse a prototype. (While a prototype is being build, its +** arrays can be larger than needed; the extra slots are filled with +** NULL, so the use of 'markobjectN') +*/ +static int traverseproto (global_State *g, Proto *f) { + int i; + if (f->cache && iswhite(f->cache)) + f->cache = NULL; /* allow cache to be collected */ + markobjectN(g, f->source); + for (i = 0; i < f->sizek; i++) /* mark literals */ + markvalue(g, &f->k[i]); + for (i = 0; i < f->sizeupvalues; i++) /* mark upvalue names */ + markobjectN(g, f->upvalues[i].name); + for (i = 0; i < f->sizep; i++) /* mark nested protos */ + markobjectN(g, f->p[i]); + for (i = 0; i < f->sizelocvars; i++) /* mark local-variable names */ + markobjectN(g, f->locvars[i].varname); + return sizeof(Proto) + sizeof(Instruction) * f->sizecode + + sizeof(Proto *) * f->sizep + + sizeof(TValue) * f->sizek + + sizeof(int) * f->sizelineinfo + + sizeof(LocVar) * f->sizelocvars + + sizeof(Upvaldesc) * f->sizeupvalues; +} + + +static lu_mem traverseCclosure (global_State *g, CClosure *cl) { + int i; + for (i = 0; i < cl->nupvalues; i++) /* mark its upvalues */ + markvalue(g, &cl->upvalue[i]); + return sizeCclosure(cl->nupvalues); +} + +/* +** open upvalues point to values in a thread, so those values should +** be marked when the thread is traversed except in the atomic phase +** (because then the value cannot be changed by the thread and the +** thread may not be traversed again) +*/ +static lu_mem traverseLclosure (global_State *g, LClosure *cl) { + int i; + markobjectN(g, cl->p); /* mark its prototype */ + for (i = 0; i < cl->nupvalues; i++) { /* mark its upvalues */ + UpVal *uv = cl->upvals[i]; + if (uv != NULL) { + if (upisopen(uv) && g->gcstate != GCSinsideatomic) + uv->u.open.touched = 1; /* can be marked in 'remarkupvals' */ + else + markvalue(g, uv->v); + } + } + return sizeLclosure(cl->nupvalues); +} + + +static lu_mem traversethread (global_State *g, lua_State *th) { + StkId o = th->stack; + if (o == NULL) + return 1; /* stack not completely built yet */ + lua_assert(g->gcstate == GCSinsideatomic || + th->openupval == NULL || isintwups(th)); + for (; o < th->top; o++) /* mark live elements in the stack */ + markvalue(g, o); + if (g->gcstate == GCSinsideatomic) { /* final traversal? */ + StkId lim = th->stack + th->stacksize; /* real end of stack */ + for (; o < lim; o++) /* clear not-marked stack slice */ + setnilvalue(o); + /* 'remarkupvals' may have removed thread from 'twups' list */ + if (!isintwups(th) && th->openupval != NULL) { + th->twups = g->twups; /* link it back to the list */ + g->twups = th; + } + } + else if (g->gckind != KGC_EMERGENCY) + luaD_shrinkstack(th); /* do not change stack in emergency cycle */ + return (sizeof(lua_State) + sizeof(TValue) * th->stacksize + + sizeof(CallInfo) * th->nci); +} + + +/* +** traverse one gray object, turning it to black (except for threads, +** which are always gray). +*/ +static void propagatemark (global_State *g) { + lu_mem size; + GCObject *o = g->gray; + lua_assert(isgray(o)); + gray2black(o); + switch (o->tt) { + case LUA_TTABLE: { + Table *h = gco2t(o); + g->gray = h->gclist; /* remove from 'gray' list */ + size = traversetable(g, h); + break; + } + case LUA_TLCL: { + LClosure *cl = gco2lcl(o); + g->gray = cl->gclist; /* remove from 'gray' list */ + size = traverseLclosure(g, cl); + break; + } + case LUA_TCCL: { + CClosure *cl = gco2ccl(o); + g->gray = cl->gclist; /* remove from 'gray' list */ + size = traverseCclosure(g, cl); + break; + } + case LUA_TTHREAD: { + lua_State *th = gco2th(o); + g->gray = th->gclist; /* remove from 'gray' list */ + linkgclist(th, g->grayagain); /* insert into 'grayagain' list */ + black2gray(o); + size = traversethread(g, th); + break; + } + case LUA_TPROTO: { + Proto *p = gco2p(o); + g->gray = p->gclist; /* remove from 'gray' list */ + size = traverseproto(g, p); + break; + } + default: lua_assert(0); return; + } + g->GCmemtrav += size; +} + + +static void propagateall (global_State *g) { + while (g->gray) propagatemark(g); +} + + +static void convergeephemerons (global_State *g) { + int changed; + do { + GCObject *w; + GCObject *next = g->ephemeron; /* get ephemeron list */ + g->ephemeron = NULL; /* tables may return to this list when traversed */ + changed = 0; + while ((w = next) != NULL) { + next = gco2t(w)->gclist; + if (traverseephemeron(g, gco2t(w))) { /* traverse marked some value? */ + propagateall(g); /* propagate changes */ + changed = 1; /* will have to revisit all ephemeron tables */ + } + } + } while (changed); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Sweep Functions +** ======================================================= +*/ + + +/* +** clear entries with unmarked keys from all weaktables in list 'l' up +** to element 'f' +*/ +static void clearkeys (global_State *g, GCObject *l, GCObject *f) { + for (; l != f; l = gco2t(l)->gclist) { + Table *h = gco2t(l); + Node *n, *limit = gnodelast(h); + for (n = gnode(h, 0); n < limit; n++) { + if (!ttisnil(gval(n)) && (iscleared(g, gkey(n)))) { + setnilvalue(gval(n)); /* remove value ... */ + removeentry(n); /* and remove entry from table */ + } + } + } +} + + +/* +** clear entries with unmarked values from all weaktables in list 'l' up +** to element 'f' +*/ +static void clearvalues (global_State *g, GCObject *l, GCObject *f) { + for (; l != f; l = gco2t(l)->gclist) { + Table *h = gco2t(l); + Node *n, *limit = gnodelast(h); + unsigned int i; + for (i = 0; i < h->sizearray; i++) { + TValue *o = &h->array[i]; + if (iscleared(g, o)) /* value was collected? */ + setnilvalue(o); /* remove value */ + } + for (n = gnode(h, 0); n < limit; n++) { + if (!ttisnil(gval(n)) && iscleared(g, gval(n))) { + setnilvalue(gval(n)); /* remove value ... */ + removeentry(n); /* and remove entry from table */ + } + } + } +} + + +void luaC_upvdeccount (lua_State *L, UpVal *uv) { + lua_assert(uv->refcount > 0); + uv->refcount--; + if (uv->refcount == 0 && !upisopen(uv)) + luaM_free(L, uv); +} + + +static void freeLclosure (lua_State *L, LClosure *cl) { + int i; + for (i = 0; i < cl->nupvalues; i++) { + UpVal *uv = cl->upvals[i]; + if (uv) + luaC_upvdeccount(L, uv); + } + luaM_freemem(L, cl, sizeLclosure(cl->nupvalues)); +} + + +static void freeobj (lua_State *L, GCObject *o) { + switch (o->tt) { + case LUA_TPROTO: luaF_freeproto(L, gco2p(o)); break; + case LUA_TLCL: { + freeLclosure(L, gco2lcl(o)); + break; + } + case LUA_TCCL: { + luaM_freemem(L, o, sizeCclosure(gco2ccl(o)->nupvalues)); + break; + } + case LUA_TTABLE: luaH_free(L, gco2t(o)); break; + case LUA_TTHREAD: luaE_freethread(L, gco2th(o)); break; + case LUA_TUSERDATA: luaM_freemem(L, o, sizeudata(gco2u(o))); break; + case LUA_TSHRSTR: + luaS_remove(L, gco2ts(o)); /* remove it from hash table */ + luaM_freemem(L, o, sizelstring(gco2ts(o)->shrlen)); + break; + case LUA_TLNGSTR: { + luaM_freemem(L, o, sizelstring(gco2ts(o)->u.lnglen)); + break; + } + default: lua_assert(0); + } +} + + +#define sweepwholelist(L,p) sweeplist(L,p,MAX_LUMEM) +static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count); + + +/* +** sweep at most 'count' elements from a list of GCObjects erasing dead +** objects, where a dead object is one marked with the old (non current) +** white; change all non-dead objects back to white, preparing for next +** collection cycle. Return where to continue the traversal or NULL if +** list is finished. +*/ +static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { + global_State *g = G(L); + int ow = otherwhite(g); + int white = luaC_white(g); /* current white */ + while (*p != NULL && count-- > 0) { + GCObject *curr = *p; + int marked = curr->marked; + if (isdeadm(ow, marked)) { /* is 'curr' dead? */ + *p = curr->next; /* remove 'curr' from list */ + freeobj(L, curr); /* erase 'curr' */ + } + else { /* change mark to 'white' */ + curr->marked = cast_byte((marked & maskcolors) | white); + p = &curr->next; /* go to next element */ + } + } + return (*p == NULL) ? NULL : p; +} + + +/* +** sweep a list until a live object (or end of list) +*/ +static GCObject **sweeptolive (lua_State *L, GCObject **p) { + GCObject **old = p; + do { + p = sweeplist(L, p, 1); + } while (p == old); + return p; +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Finalization +** ======================================================= +*/ + +/* +** If possible, shrink string table +*/ +static void checkSizes (lua_State *L, global_State *g) { + if (g->gckind != KGC_EMERGENCY) { + l_mem olddebt = g->GCdebt; + if (g->strt.nuse < g->strt.size / 4) /* string table too big? */ + luaS_resize(L, g->strt.size / 2); /* shrink it a little */ + g->GCestimate += g->GCdebt - olddebt; /* update estimate */ + } +} + + +static GCObject *udata2finalize (global_State *g) { + GCObject *o = g->tobefnz; /* get first element */ + lua_assert(tofinalize(o)); + g->tobefnz = o->next; /* remove it from 'tobefnz' list */ + o->next = g->allgc; /* return it to 'allgc' list */ + g->allgc = o; + resetbit(o->marked, FINALIZEDBIT); /* object is "normal" again */ + if (issweepphase(g)) + makewhite(g, o); /* "sweep" object */ + return o; +} + + +static void dothecall (lua_State *L, void *ud) { + UNUSED(ud); + luaD_callnoyield(L, L->top - 2, 0); +} + + +static void GCTM (lua_State *L, int propagateerrors) { + global_State *g = G(L); + const TValue *tm; + TValue v; + setgcovalue(L, &v, udata2finalize(g)); + tm = luaT_gettmbyobj(L, &v, TM_GC); + if (tm != NULL && ttisfunction(tm)) { /* is there a finalizer? */ + int status; + lu_byte oldah = L->allowhook; + int running = g->gcrunning; + L->allowhook = 0; /* stop debug hooks during GC metamethod */ + g->gcrunning = 0; /* avoid GC steps */ + setobj2s(L, L->top, tm); /* push finalizer... */ + setobj2s(L, L->top + 1, &v); /* ... and its argument */ + L->top += 2; /* and (next line) call the finalizer */ + L->ci->callstatus |= CIST_FIN; /* will run a finalizer */ + status = luaD_pcall(L, dothecall, NULL, savestack(L, L->top - 2), 0); + L->ci->callstatus &= ~CIST_FIN; /* not running a finalizer anymore */ + L->allowhook = oldah; /* restore hooks */ + g->gcrunning = running; /* restore state */ + if (status != LUA_OK && propagateerrors) { /* error while running __gc? */ + if (status == LUA_ERRRUN) { /* is there an error object? */ + const char *msg = (ttisstring(L->top - 1)) + ? svalue(L->top - 1) + : "no message"; + luaO_pushfstring(L, "error in __gc metamethod (%s)", msg); + status = LUA_ERRGCMM; /* error in __gc metamethod */ + } + luaD_throw(L, status); /* re-throw error */ + } + } +} + + +/* +** call a few (up to 'g->gcfinnum') finalizers +*/ +static int runafewfinalizers (lua_State *L) { + global_State *g = G(L); + unsigned int i; + lua_assert(!g->tobefnz || g->gcfinnum > 0); + for (i = 0; g->tobefnz && i < g->gcfinnum; i++) + GCTM(L, 1); /* call one finalizer */ + g->gcfinnum = (!g->tobefnz) ? 0 /* nothing more to finalize? */ + : g->gcfinnum * 2; /* else call a few more next time */ + return i; +} + + +/* +** call all pending finalizers +*/ +static void callallpendingfinalizers (lua_State *L) { + global_State *g = G(L); + while (g->tobefnz) + GCTM(L, 0); +} + + +/* +** find last 'next' field in list 'p' list (to add elements in its end) +*/ +static GCObject **findlast (GCObject **p) { + while (*p != NULL) + p = &(*p)->next; + return p; +} + + +/* +** move all unreachable objects (or 'all' objects) that need +** finalization from list 'finobj' to list 'tobefnz' (to be finalized) +*/ +static void separatetobefnz (global_State *g, int all) { + GCObject *curr; + GCObject **p = &g->finobj; + GCObject **lastnext = findlast(&g->tobefnz); + while ((curr = *p) != NULL) { /* traverse all finalizable objects */ + lua_assert(tofinalize(curr)); + if (!(iswhite(curr) || all)) /* not being collected? */ + p = &curr->next; /* don't bother with it */ + else { + *p = curr->next; /* remove 'curr' from 'finobj' list */ + curr->next = *lastnext; /* link at the end of 'tobefnz' list */ + *lastnext = curr; + lastnext = &curr->next; + } + } +} + + +/* +** if object 'o' has a finalizer, remove it from 'allgc' list (must +** search the list to find it) and link it in 'finobj' list. +*/ +void luaC_checkfinalizer (lua_State *L, GCObject *o, Table *mt) { + global_State *g = G(L); + if (tofinalize(o) || /* obj. is already marked... */ + gfasttm(g, mt, TM_GC) == NULL) /* or has no finalizer? */ + return; /* nothing to be done */ + else { /* move 'o' to 'finobj' list */ + GCObject **p; + if (issweepphase(g)) { + makewhite(g, o); /* "sweep" object 'o' */ + if (g->sweepgc == &o->next) /* should not remove 'sweepgc' object */ + g->sweepgc = sweeptolive(L, g->sweepgc); /* change 'sweepgc' */ + } + /* search for pointer pointing to 'o' */ + for (p = &g->allgc; *p != o; p = &(*p)->next) { /* empty */ } + *p = o->next; /* remove 'o' from 'allgc' list */ + o->next = g->finobj; /* link it in 'finobj' list */ + g->finobj = o; + l_setbit(o->marked, FINALIZEDBIT); /* mark it as such */ + } +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** GC control +** ======================================================= +*/ + + +/* +** Set a reasonable "time" to wait before starting a new GC cycle; cycle +** will start when memory use hits threshold. (Division by 'estimate' +** should be OK: it cannot be zero (because Lua cannot even start with +** less than PAUSEADJ bytes). +*/ +static void setpause (global_State *g) { + l_mem threshold, debt; + l_mem estimate = g->GCestimate / PAUSEADJ; /* adjust 'estimate' */ + lua_assert(estimate > 0); + threshold = (g->gcpause < MAX_LMEM / estimate) /* overflow? */ + ? estimate * g->gcpause /* no overflow */ + : MAX_LMEM; /* overflow; truncate to maximum */ + debt = gettotalbytes(g) - threshold; + luaE_setdebt(g, debt); +} + + +/* +** Enter first sweep phase. +** The call to 'sweeplist' tries to make pointer point to an object +** inside the list (instead of to the header), so that the real sweep do +** not need to skip objects created between "now" and the start of the +** real sweep. +*/ +static void entersweep (lua_State *L) { + global_State *g = G(L); + g->gcstate = GCSswpallgc; + lua_assert(g->sweepgc == NULL); + g->sweepgc = sweeplist(L, &g->allgc, 1); +} + + +void luaC_freeallobjects (lua_State *L) { + global_State *g = G(L); + separatetobefnz(g, 1); /* separate all objects with finalizers */ + lua_assert(g->finobj == NULL); + callallpendingfinalizers(L); + lua_assert(g->tobefnz == NULL); + g->currentwhite = WHITEBITS; /* this "white" makes all objects look dead */ + g->gckind = KGC_NORMAL; + sweepwholelist(L, &g->finobj); + sweepwholelist(L, &g->allgc); + sweepwholelist(L, &g->fixedgc); /* collect fixed objects */ + lua_assert(g->strt.nuse == 0); +} + + +static l_mem atomic (lua_State *L) { + global_State *g = G(L); + l_mem work; + GCObject *origweak, *origall; + GCObject *grayagain = g->grayagain; /* save original list */ + lua_assert(g->ephemeron == NULL && g->weak == NULL); + lua_assert(!iswhite(g->mainthread)); + g->gcstate = GCSinsideatomic; + g->GCmemtrav = 0; /* start counting work */ + markobject(g, L); /* mark running thread */ + /* registry and global metatables may be changed by API */ + markvalue(g, &g->l_registry); + markmt(g); /* mark global metatables */ + /* remark occasional upvalues of (maybe) dead threads */ + remarkupvals(g); + propagateall(g); /* propagate changes */ + work = g->GCmemtrav; /* stop counting (do not recount 'grayagain') */ + g->gray = grayagain; + propagateall(g); /* traverse 'grayagain' list */ + g->GCmemtrav = 0; /* restart counting */ + convergeephemerons(g); + /* at this point, all strongly accessible objects are marked. */ + /* Clear values from weak tables, before checking finalizers */ + clearvalues(g, g->weak, NULL); + clearvalues(g, g->allweak, NULL); + origweak = g->weak; origall = g->allweak; + work += g->GCmemtrav; /* stop counting (objects being finalized) */ + separatetobefnz(g, 0); /* separate objects to be finalized */ + g->gcfinnum = 1; /* there may be objects to be finalized */ + markbeingfnz(g); /* mark objects that will be finalized */ + propagateall(g); /* remark, to propagate 'resurrection' */ + g->GCmemtrav = 0; /* restart counting */ + convergeephemerons(g); + /* at this point, all resurrected objects are marked. */ + /* remove dead objects from weak tables */ + clearkeys(g, g->ephemeron, NULL); /* clear keys from all ephemeron tables */ + clearkeys(g, g->allweak, NULL); /* clear keys from all 'allweak' tables */ + /* clear values from resurrected weak tables */ + clearvalues(g, g->weak, origweak); + clearvalues(g, g->allweak, origall); + luaS_clearcache(g); + g->currentwhite = cast_byte(otherwhite(g)); /* flip current white */ + work += g->GCmemtrav; /* complete counting */ + return work; /* estimate of memory marked by 'atomic' */ +} + + +static lu_mem sweepstep (lua_State *L, global_State *g, + int nextstate, GCObject **nextlist) { + if (g->sweepgc) { + l_mem olddebt = g->GCdebt; + g->sweepgc = sweeplist(L, g->sweepgc, GCSWEEPMAX); + g->GCestimate += g->GCdebt - olddebt; /* update estimate */ + if (g->sweepgc) /* is there still something to sweep? */ + return (GCSWEEPMAX * GCSWEEPCOST); + } + /* else enter next state */ + g->gcstate = nextstate; + g->sweepgc = nextlist; + return 0; +} + + +static lu_mem singlestep (lua_State *L) { + global_State *g = G(L); + switch (g->gcstate) { + case GCSpause: { + g->GCmemtrav = g->strt.size * sizeof(GCObject*); + restartcollection(g); + g->gcstate = GCSpropagate; + return g->GCmemtrav; + } + case GCSpropagate: { + g->GCmemtrav = 0; + lua_assert(g->gray); + propagatemark(g); + if (g->gray == NULL) /* no more gray objects? */ + g->gcstate = GCSatomic; /* finish propagate phase */ + return g->GCmemtrav; /* memory traversed in this step */ + } + case GCSatomic: { + lu_mem work; + propagateall(g); /* make sure gray list is empty */ + work = atomic(L); /* work is what was traversed by 'atomic' */ + entersweep(L); + g->GCestimate = gettotalbytes(g); /* first estimate */; + return work; + } + case GCSswpallgc: { /* sweep "regular" objects */ + return sweepstep(L, g, GCSswpfinobj, &g->finobj); + } + case GCSswpfinobj: { /* sweep objects with finalizers */ + return sweepstep(L, g, GCSswptobefnz, &g->tobefnz); + } + case GCSswptobefnz: { /* sweep objects to be finalized */ + return sweepstep(L, g, GCSswpend, NULL); + } + case GCSswpend: { /* finish sweeps */ + makewhite(g, g->mainthread); /* sweep main thread */ + checkSizes(L, g); + g->gcstate = GCScallfin; + return 0; + } + case GCScallfin: { /* call remaining finalizers */ + if (g->tobefnz && g->gckind != KGC_EMERGENCY) { + int n = runafewfinalizers(L); + return (n * GCFINALIZECOST); + } + else { /* emergency mode or no more finalizers */ + g->gcstate = GCSpause; /* finish collection */ + return 0; + } + } + default: lua_assert(0); return 0; + } +} + + +/* +** advances the garbage collector until it reaches a state allowed +** by 'statemask' +*/ +void luaC_runtilstate (lua_State *L, int statesmask) { + global_State *g = G(L); + while (!testbit(statesmask, g->gcstate)) + singlestep(L); +} + + +/* +** get GC debt and convert it from Kb to 'work units' (avoid zero debt +** and overflows) +*/ +static l_mem getdebt (global_State *g) { + l_mem debt = g->GCdebt; + int stepmul = g->gcstepmul; + if (debt <= 0) return 0; /* minimal debt */ + else { + debt = (debt / STEPMULADJ) + 1; + debt = (debt < MAX_LMEM / stepmul) ? debt * stepmul : MAX_LMEM; + return debt; + } +} + +/* +** performs a basic GC step when collector is running +*/ +void luaC_step (lua_State *L) { + global_State *g = G(L); + l_mem debt = getdebt(g); /* GC deficit (be paid now) */ + if (!g->gcrunning) { /* not running? */ + luaE_setdebt(g, -GCSTEPSIZE * 10); /* avoid being called too often */ + return; + } + do { /* repeat until pause or enough "credit" (negative debt) */ + lu_mem work = singlestep(L); /* perform one single step */ + debt -= work; + } while (debt > -GCSTEPSIZE && g->gcstate != GCSpause); + if (g->gcstate == GCSpause) + setpause(g); /* pause until next cycle */ + else { + debt = (debt / g->gcstepmul) * STEPMULADJ; /* convert 'work units' to Kb */ + luaE_setdebt(g, debt); + runafewfinalizers(L); + } +} + + +/* +** Performs a full GC cycle; if 'isemergency', set a flag to avoid +** some operations which could change the interpreter state in some +** unexpected ways (running finalizers and shrinking some structures). +** Before running the collection, check 'keepinvariant'; if it is true, +** there may be some objects marked as black, so the collector has +** to sweep all objects to turn them back to white (as white has not +** changed, nothing will be collected). +*/ +void luaC_fullgc (lua_State *L, int isemergency) { + global_State *g = G(L); + lua_assert(g->gckind == KGC_NORMAL); + if (isemergency) g->gckind = KGC_EMERGENCY; /* set flag */ + if (keepinvariant(g)) { /* black objects? */ + entersweep(L); /* sweep everything to turn them back to white */ + } + /* finish any pending sweep phase to start a new cycle */ + luaC_runtilstate(L, bitmask(GCSpause)); + luaC_runtilstate(L, ~bitmask(GCSpause)); /* start new collection */ + luaC_runtilstate(L, bitmask(GCScallfin)); /* run up to finalizers */ + /* estimate must be correct after a full GC cycle */ + lua_assert(g->GCestimate == gettotalbytes(g)); + luaC_runtilstate(L, bitmask(GCSpause)); /* finish collection */ + g->gckind = KGC_NORMAL; + setpause(g); +} + +/* }====================================================== */ + + diff --git a/src/rcheevos/test/lua/src/lgc.h b/src/rcheevos/test/lua/src/lgc.h new file mode 100644 index 000000000..aed3e18a5 --- /dev/null +++ b/src/rcheevos/test/lua/src/lgc.h @@ -0,0 +1,147 @@ +/* +** $Id: lgc.h,v 2.91 2015/12/21 13:02:14 roberto Exp $ +** Garbage Collector +** See Copyright Notice in lua.h +*/ + +#ifndef lgc_h +#define lgc_h + + +#include "lobject.h" +#include "lstate.h" + +/* +** Collectable objects may have one of three colors: white, which +** means the object is not marked; gray, which means the +** object is marked, but its references may be not marked; and +** black, which means that the object and all its references are marked. +** The main invariant of the garbage collector, while marking objects, +** is that a black object can never point to a white one. Moreover, +** any gray object must be in a "gray list" (gray, grayagain, weak, +** allweak, ephemeron) so that it can be visited again before finishing +** the collection cycle. These lists have no meaning when the invariant +** is not being enforced (e.g., sweep phase). +*/ + + + +/* how much to allocate before next GC step */ +#if !defined(GCSTEPSIZE) +/* ~100 small strings */ +#define GCSTEPSIZE (cast_int(100 * sizeof(TString))) +#endif + + +/* +** Possible states of the Garbage Collector +*/ +#define GCSpropagate 0 +#define GCSatomic 1 +#define GCSswpallgc 2 +#define GCSswpfinobj 3 +#define GCSswptobefnz 4 +#define GCSswpend 5 +#define GCScallfin 6 +#define GCSpause 7 + + +#define issweepphase(g) \ + (GCSswpallgc <= (g)->gcstate && (g)->gcstate <= GCSswpend) + + +/* +** macro to tell when main invariant (white objects cannot point to black +** ones) must be kept. During a collection, the sweep +** phase may break the invariant, as objects turned white may point to +** still-black objects. The invariant is restored when sweep ends and +** all objects are white again. +*/ + +#define keepinvariant(g) ((g)->gcstate <= GCSatomic) + + +/* +** some useful bit tricks +*/ +#define resetbits(x,m) ((x) &= cast(lu_byte, ~(m))) +#define setbits(x,m) ((x) |= (m)) +#define testbits(x,m) ((x) & (m)) +#define bitmask(b) (1<<(b)) +#define bit2mask(b1,b2) (bitmask(b1) | bitmask(b2)) +#define l_setbit(x,b) setbits(x, bitmask(b)) +#define resetbit(x,b) resetbits(x, bitmask(b)) +#define testbit(x,b) testbits(x, bitmask(b)) + + +/* Layout for bit use in 'marked' field: */ +#define WHITE0BIT 0 /* object is white (type 0) */ +#define WHITE1BIT 1 /* object is white (type 1) */ +#define BLACKBIT 2 /* object is black */ +#define FINALIZEDBIT 3 /* object has been marked for finalization */ +/* bit 7 is currently used by tests (luaL_checkmemory) */ + +#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT) + + +#define iswhite(x) testbits((x)->marked, WHITEBITS) +#define isblack(x) testbit((x)->marked, BLACKBIT) +#define isgray(x) /* neither white nor black */ \ + (!testbits((x)->marked, WHITEBITS | bitmask(BLACKBIT))) + +#define tofinalize(x) testbit((x)->marked, FINALIZEDBIT) + +#define otherwhite(g) ((g)->currentwhite ^ WHITEBITS) +#define isdeadm(ow,m) (!(((m) ^ WHITEBITS) & (ow))) +#define isdead(g,v) isdeadm(otherwhite(g), (v)->marked) + +#define changewhite(x) ((x)->marked ^= WHITEBITS) +#define gray2black(x) l_setbit((x)->marked, BLACKBIT) + +#define luaC_white(g) cast(lu_byte, (g)->currentwhite & WHITEBITS) + + +/* +** Does one step of collection when debt becomes positive. 'pre'/'pos' +** allows some adjustments to be done only when needed. macro +** 'condchangemem' is used only for heavy tests (forcing a full +** GC cycle on every opportunity) +*/ +#define luaC_condGC(L,pre,pos) \ + { if (G(L)->GCdebt > 0) { pre; luaC_step(L); pos;}; \ + condchangemem(L,pre,pos); } + +/* more often than not, 'pre'/'pos' are empty */ +#define luaC_checkGC(L) luaC_condGC(L,(void)0,(void)0) + + +#define luaC_barrier(L,p,v) ( \ + (iscollectable(v) && isblack(p) && iswhite(gcvalue(v))) ? \ + luaC_barrier_(L,obj2gco(p),gcvalue(v)) : cast_void(0)) + +#define luaC_barrierback(L,p,v) ( \ + (iscollectable(v) && isblack(p) && iswhite(gcvalue(v))) ? \ + luaC_barrierback_(L,p) : cast_void(0)) + +#define luaC_objbarrier(L,p,o) ( \ + (isblack(p) && iswhite(o)) ? \ + luaC_barrier_(L,obj2gco(p),obj2gco(o)) : cast_void(0)) + +#define luaC_upvalbarrier(L,uv) ( \ + (iscollectable((uv)->v) && !upisopen(uv)) ? \ + luaC_upvalbarrier_(L,uv) : cast_void(0)) + +LUAI_FUNC void luaC_fix (lua_State *L, GCObject *o); +LUAI_FUNC void luaC_freeallobjects (lua_State *L); +LUAI_FUNC void luaC_step (lua_State *L); +LUAI_FUNC void luaC_runtilstate (lua_State *L, int statesmask); +LUAI_FUNC void luaC_fullgc (lua_State *L, int isemergency); +LUAI_FUNC GCObject *luaC_newobj (lua_State *L, int tt, size_t sz); +LUAI_FUNC void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v); +LUAI_FUNC void luaC_barrierback_ (lua_State *L, Table *o); +LUAI_FUNC void luaC_upvalbarrier_ (lua_State *L, UpVal *uv); +LUAI_FUNC void luaC_checkfinalizer (lua_State *L, GCObject *o, Table *mt); +LUAI_FUNC void luaC_upvdeccount (lua_State *L, UpVal *uv); + + +#endif diff --git a/src/rcheevos/test/lua/src/linit.c b/src/rcheevos/test/lua/src/linit.c new file mode 100644 index 000000000..afcaf98b2 --- /dev/null +++ b/src/rcheevos/test/lua/src/linit.c @@ -0,0 +1,68 @@ +/* +** $Id: linit.c,v 1.39 2016/12/04 20:17:24 roberto Exp $ +** Initialization of libraries for lua.c and other clients +** See Copyright Notice in lua.h +*/ + + +#define linit_c +#define LUA_LIB + +/* +** If you embed Lua in your program and need to open the standard +** libraries, call luaL_openlibs in your program. If you need a +** different set of libraries, copy this file to your project and edit +** it to suit your needs. +** +** You can also *preload* libraries, so that a later 'require' can +** open the library, which is already linked to the application. +** For that, do the following code: +** +** luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE); +** lua_pushcfunction(L, luaopen_modname); +** lua_setfield(L, -2, modname); +** lua_pop(L, 1); // remove PRELOAD table +*/ + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lualib.h" +#include "lauxlib.h" + + +/* +** these libs are loaded by lua.c and are readily available to any Lua +** program +*/ +static const luaL_Reg loadedlibs[] = { + {"_G", luaopen_base}, + {LUA_LOADLIBNAME, luaopen_package}, + {LUA_COLIBNAME, luaopen_coroutine}, + {LUA_TABLIBNAME, luaopen_table}, + {LUA_IOLIBNAME, luaopen_io}, + {LUA_OSLIBNAME, luaopen_os}, + {LUA_STRLIBNAME, luaopen_string}, + {LUA_MATHLIBNAME, luaopen_math}, + {LUA_UTF8LIBNAME, luaopen_utf8}, + {LUA_DBLIBNAME, luaopen_debug}, +#if defined(LUA_COMPAT_BITLIB) + {LUA_BITLIBNAME, luaopen_bit32}, +#endif + {NULL, NULL} +}; + + +LUALIB_API void luaL_openlibs (lua_State *L) { + const luaL_Reg *lib; + /* "require" functions from 'loadedlibs' and set results to global table */ + for (lib = loadedlibs; lib->func; lib++) { + luaL_requiref(L, lib->name, lib->func, 1); + lua_pop(L, 1); /* remove lib */ + } +} + diff --git a/src/rcheevos/test/lua/src/liolib.c b/src/rcheevos/test/lua/src/liolib.c new file mode 100644 index 000000000..156840358 --- /dev/null +++ b/src/rcheevos/test/lua/src/liolib.c @@ -0,0 +1,771 @@ +/* +** $Id: liolib.c,v 2.151 2016/12/20 18:37:00 roberto Exp $ +** Standard I/O (and system) library +** See Copyright Notice in lua.h +*/ + +#define liolib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + + + +/* +** Change this macro to accept other modes for 'fopen' besides +** the standard ones. +*/ +#if !defined(l_checkmode) + +/* accepted extensions to 'mode' in 'fopen' */ +#if !defined(L_MODEEXT) +#define L_MODEEXT "b" +#endif + +/* Check whether 'mode' matches '[rwa]%+?[L_MODEEXT]*' */ +static int l_checkmode (const char *mode) { + return (*mode != '\0' && strchr("rwa", *(mode++)) != NULL && + (*mode != '+' || (++mode, 1)) && /* skip if char is '+' */ + (strspn(mode, L_MODEEXT) == strlen(mode))); /* check extensions */ +} + +#endif + +/* +** {====================================================== +** l_popen spawns a new process connected to the current +** one through the file streams. +** ======================================================= +*/ + +#if !defined(l_popen) /* { */ + +#if defined(LUA_USE_POSIX) /* { */ + +#define l_popen(L,c,m) (fflush(NULL), popen(c,m)) +#define l_pclose(L,file) (pclose(file)) + +#elif defined(LUA_USE_WINDOWS) /* }{ */ + +#define l_popen(L,c,m) (_popen(c,m)) +#define l_pclose(L,file) (_pclose(file)) + +#else /* }{ */ + +/* ISO C definitions */ +#define l_popen(L,c,m) \ + ((void)((void)c, m), \ + luaL_error(L, "'popen' not supported"), \ + (FILE*)0) +#define l_pclose(L,file) ((void)L, (void)file, -1) + +#endif /* } */ + +#endif /* } */ + +/* }====================================================== */ + + +#if !defined(l_getc) /* { */ + +#if defined(LUA_USE_POSIX) +#define l_getc(f) getc_unlocked(f) +#define l_lockfile(f) flockfile(f) +#define l_unlockfile(f) funlockfile(f) +#else +#define l_getc(f) getc(f) +#define l_lockfile(f) ((void)0) +#define l_unlockfile(f) ((void)0) +#endif + +#endif /* } */ + + +/* +** {====================================================== +** l_fseek: configuration for longer offsets +** ======================================================= +*/ + +#if !defined(l_fseek) /* { */ + +#if defined(LUA_USE_POSIX) /* { */ + +#include + +#define l_fseek(f,o,w) fseeko(f,o,w) +#define l_ftell(f) ftello(f) +#define l_seeknum off_t + +#elif defined(LUA_USE_WINDOWS) && !defined(_CRTIMP_TYPEINFO) \ + && defined(_MSC_VER) && (_MSC_VER >= 1400) /* }{ */ + +/* Windows (but not DDK) and Visual C++ 2005 or higher */ +#define l_fseek(f,o,w) _fseeki64(f,o,w) +#define l_ftell(f) _ftelli64(f) +#define l_seeknum __int64 + +#else /* }{ */ + +/* ISO C definitions */ +#define l_fseek(f,o,w) fseek(f,o,w) +#define l_ftell(f) ftell(f) +#define l_seeknum long + +#endif /* } */ + +#endif /* } */ + +/* }====================================================== */ + + +#define IO_PREFIX "_IO_" +#define IOPREF_LEN (sizeof(IO_PREFIX)/sizeof(char) - 1) +#define IO_INPUT (IO_PREFIX "input") +#define IO_OUTPUT (IO_PREFIX "output") + + +typedef luaL_Stream LStream; + + +#define tolstream(L) ((LStream *)luaL_checkudata(L, 1, LUA_FILEHANDLE)) + +#define isclosed(p) ((p)->closef == NULL) + + +static int io_type (lua_State *L) { + LStream *p; + luaL_checkany(L, 1); + p = (LStream *)luaL_testudata(L, 1, LUA_FILEHANDLE); + if (p == NULL) + lua_pushnil(L); /* not a file */ + else if (isclosed(p)) + lua_pushliteral(L, "closed file"); + else + lua_pushliteral(L, "file"); + return 1; +} + + +static int f_tostring (lua_State *L) { + LStream *p = tolstream(L); + if (isclosed(p)) + lua_pushliteral(L, "file (closed)"); + else + lua_pushfstring(L, "file (%p)", p->f); + return 1; +} + + +static FILE *tofile (lua_State *L) { + LStream *p = tolstream(L); + if (isclosed(p)) + luaL_error(L, "attempt to use a closed file"); + lua_assert(p->f); + return p->f; +} + + +/* +** When creating file handles, always creates a 'closed' file handle +** before opening the actual file; so, if there is a memory error, the +** handle is in a consistent state. +*/ +static LStream *newprefile (lua_State *L) { + LStream *p = (LStream *)lua_newuserdata(L, sizeof(LStream)); + p->closef = NULL; /* mark file handle as 'closed' */ + luaL_setmetatable(L, LUA_FILEHANDLE); + return p; +} + + +/* +** Calls the 'close' function from a file handle. The 'volatile' avoids +** a bug in some versions of the Clang compiler (e.g., clang 3.0 for +** 32 bits). +*/ +static int aux_close (lua_State *L) { + LStream *p = tolstream(L); + volatile lua_CFunction cf = p->closef; + p->closef = NULL; /* mark stream as closed */ + return (*cf)(L); /* close it */ +} + + +static int io_close (lua_State *L) { + if (lua_isnone(L, 1)) /* no argument? */ + lua_getfield(L, LUA_REGISTRYINDEX, IO_OUTPUT); /* use standard output */ + tofile(L); /* make sure argument is an open stream */ + return aux_close(L); +} + + +static int f_gc (lua_State *L) { + LStream *p = tolstream(L); + if (!isclosed(p) && p->f != NULL) + aux_close(L); /* ignore closed and incompletely open files */ + return 0; +} + + +/* +** function to close regular files +*/ +static int io_fclose (lua_State *L) { + LStream *p = tolstream(L); + int res = fclose(p->f); + return luaL_fileresult(L, (res == 0), NULL); +} + + +static LStream *newfile (lua_State *L) { + LStream *p = newprefile(L); + p->f = NULL; + p->closef = &io_fclose; + return p; +} + + +static void opencheck (lua_State *L, const char *fname, const char *mode) { + LStream *p = newfile(L); + p->f = fopen(fname, mode); + if (p->f == NULL) + luaL_error(L, "cannot open file '%s' (%s)", fname, strerror(errno)); +} + + +static int io_open (lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + const char *mode = luaL_optstring(L, 2, "r"); + LStream *p = newfile(L); + const char *md = mode; /* to traverse/check mode */ + luaL_argcheck(L, l_checkmode(md), 2, "invalid mode"); + p->f = fopen(filename, mode); + return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1; +} + + +/* +** function to close 'popen' files +*/ +static int io_pclose (lua_State *L) { + LStream *p = tolstream(L); + return luaL_execresult(L, l_pclose(L, p->f)); +} + + +static int io_popen (lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + const char *mode = luaL_optstring(L, 2, "r"); + LStream *p = newprefile(L); + p->f = l_popen(L, filename, mode); + p->closef = &io_pclose; + return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1; +} + + +static int io_tmpfile (lua_State *L) { + LStream *p = newfile(L); + p->f = tmpfile(); + return (p->f == NULL) ? luaL_fileresult(L, 0, NULL) : 1; +} + + +static FILE *getiofile (lua_State *L, const char *findex) { + LStream *p; + lua_getfield(L, LUA_REGISTRYINDEX, findex); + p = (LStream *)lua_touserdata(L, -1); + if (isclosed(p)) + luaL_error(L, "standard %s file is closed", findex + IOPREF_LEN); + return p->f; +} + + +static int g_iofile (lua_State *L, const char *f, const char *mode) { + if (!lua_isnoneornil(L, 1)) { + const char *filename = lua_tostring(L, 1); + if (filename) + opencheck(L, filename, mode); + else { + tofile(L); /* check that it's a valid file handle */ + lua_pushvalue(L, 1); + } + lua_setfield(L, LUA_REGISTRYINDEX, f); + } + /* return current value */ + lua_getfield(L, LUA_REGISTRYINDEX, f); + return 1; +} + + +static int io_input (lua_State *L) { + return g_iofile(L, IO_INPUT, "r"); +} + + +static int io_output (lua_State *L) { + return g_iofile(L, IO_OUTPUT, "w"); +} + + +static int io_readline (lua_State *L); + + +/* +** maximum number of arguments to 'f:lines'/'io.lines' (it + 3 must fit +** in the limit for upvalues of a closure) +*/ +#define MAXARGLINE 250 + +static void aux_lines (lua_State *L, int toclose) { + int n = lua_gettop(L) - 1; /* number of arguments to read */ + luaL_argcheck(L, n <= MAXARGLINE, MAXARGLINE + 2, "too many arguments"); + lua_pushinteger(L, n); /* number of arguments to read */ + lua_pushboolean(L, toclose); /* close/not close file when finished */ + lua_rotate(L, 2, 2); /* move 'n' and 'toclose' to their positions */ + lua_pushcclosure(L, io_readline, 3 + n); +} + + +static int f_lines (lua_State *L) { + tofile(L); /* check that it's a valid file handle */ + aux_lines(L, 0); + return 1; +} + + +static int io_lines (lua_State *L) { + int toclose; + if (lua_isnone(L, 1)) lua_pushnil(L); /* at least one argument */ + if (lua_isnil(L, 1)) { /* no file name? */ + lua_getfield(L, LUA_REGISTRYINDEX, IO_INPUT); /* get default input */ + lua_replace(L, 1); /* put it at index 1 */ + tofile(L); /* check that it's a valid file handle */ + toclose = 0; /* do not close it after iteration */ + } + else { /* open a new file */ + const char *filename = luaL_checkstring(L, 1); + opencheck(L, filename, "r"); + lua_replace(L, 1); /* put file at index 1 */ + toclose = 1; /* close it after iteration */ + } + aux_lines(L, toclose); + return 1; +} + + +/* +** {====================================================== +** READ +** ======================================================= +*/ + + +/* maximum length of a numeral */ +#if !defined (L_MAXLENNUM) +#define L_MAXLENNUM 200 +#endif + + +/* auxiliary structure used by 'read_number' */ +typedef struct { + FILE *f; /* file being read */ + int c; /* current character (look ahead) */ + int n; /* number of elements in buffer 'buff' */ + char buff[L_MAXLENNUM + 1]; /* +1 for ending '\0' */ +} RN; + + +/* +** Add current char to buffer (if not out of space) and read next one +*/ +static int nextc (RN *rn) { + if (rn->n >= L_MAXLENNUM) { /* buffer overflow? */ + rn->buff[0] = '\0'; /* invalidate result */ + return 0; /* fail */ + } + else { + rn->buff[rn->n++] = rn->c; /* save current char */ + rn->c = l_getc(rn->f); /* read next one */ + return 1; + } +} + + +/* +** Accept current char if it is in 'set' (of size 2) +*/ +static int test2 (RN *rn, const char *set) { + if (rn->c == set[0] || rn->c == set[1]) + return nextc(rn); + else return 0; +} + + +/* +** Read a sequence of (hex)digits +*/ +static int readdigits (RN *rn, int hex) { + int count = 0; + while ((hex ? isxdigit(rn->c) : isdigit(rn->c)) && nextc(rn)) + count++; + return count; +} + + +/* +** Read a number: first reads a valid prefix of a numeral into a buffer. +** Then it calls 'lua_stringtonumber' to check whether the format is +** correct and to convert it to a Lua number +*/ +static int read_number (lua_State *L, FILE *f) { + RN rn; + int count = 0; + int hex = 0; + char decp[2]; + rn.f = f; rn.n = 0; + decp[0] = lua_getlocaledecpoint(); /* get decimal point from locale */ + decp[1] = '.'; /* always accept a dot */ + l_lockfile(rn.f); + do { rn.c = l_getc(rn.f); } while (isspace(rn.c)); /* skip spaces */ + test2(&rn, "-+"); /* optional signal */ + if (test2(&rn, "00")) { + if (test2(&rn, "xX")) hex = 1; /* numeral is hexadecimal */ + else count = 1; /* count initial '0' as a valid digit */ + } + count += readdigits(&rn, hex); /* integral part */ + if (test2(&rn, decp)) /* decimal point? */ + count += readdigits(&rn, hex); /* fractional part */ + if (count > 0 && test2(&rn, (hex ? "pP" : "eE"))) { /* exponent mark? */ + test2(&rn, "-+"); /* exponent signal */ + readdigits(&rn, 0); /* exponent digits */ + } + ungetc(rn.c, rn.f); /* unread look-ahead char */ + l_unlockfile(rn.f); + rn.buff[rn.n] = '\0'; /* finish string */ + if (lua_stringtonumber(L, rn.buff)) /* is this a valid number? */ + return 1; /* ok */ + else { /* invalid format */ + lua_pushnil(L); /* "result" to be removed */ + return 0; /* read fails */ + } +} + + +static int test_eof (lua_State *L, FILE *f) { + int c = getc(f); + ungetc(c, f); /* no-op when c == EOF */ + lua_pushliteral(L, ""); + return (c != EOF); +} + + +static int read_line (lua_State *L, FILE *f, int chop) { + luaL_Buffer b; + int c = '\0'; + luaL_buffinit(L, &b); + while (c != EOF && c != '\n') { /* repeat until end of line */ + char *buff = luaL_prepbuffer(&b); /* preallocate buffer */ + int i = 0; + l_lockfile(f); /* no memory errors can happen inside the lock */ + while (i < LUAL_BUFFERSIZE && (c = l_getc(f)) != EOF && c != '\n') + buff[i++] = c; + l_unlockfile(f); + luaL_addsize(&b, i); + } + if (!chop && c == '\n') /* want a newline and have one? */ + luaL_addchar(&b, c); /* add ending newline to result */ + luaL_pushresult(&b); /* close buffer */ + /* return ok if read something (either a newline or something else) */ + return (c == '\n' || lua_rawlen(L, -1) > 0); +} + + +static void read_all (lua_State *L, FILE *f) { + size_t nr; + luaL_Buffer b; + luaL_buffinit(L, &b); + do { /* read file in chunks of LUAL_BUFFERSIZE bytes */ + char *p = luaL_prepbuffer(&b); + nr = fread(p, sizeof(char), LUAL_BUFFERSIZE, f); + luaL_addsize(&b, nr); + } while (nr == LUAL_BUFFERSIZE); + luaL_pushresult(&b); /* close buffer */ +} + + +static int read_chars (lua_State *L, FILE *f, size_t n) { + size_t nr; /* number of chars actually read */ + char *p; + luaL_Buffer b; + luaL_buffinit(L, &b); + p = luaL_prepbuffsize(&b, n); /* prepare buffer to read whole block */ + nr = fread(p, sizeof(char), n, f); /* try to read 'n' chars */ + luaL_addsize(&b, nr); + luaL_pushresult(&b); /* close buffer */ + return (nr > 0); /* true iff read something */ +} + + +static int g_read (lua_State *L, FILE *f, int first) { + int nargs = lua_gettop(L) - 1; + int success; + int n; + clearerr(f); + if (nargs == 0) { /* no arguments? */ + success = read_line(L, f, 1); + n = first+1; /* to return 1 result */ + } + else { /* ensure stack space for all results and for auxlib's buffer */ + luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments"); + success = 1; + for (n = first; nargs-- && success; n++) { + if (lua_type(L, n) == LUA_TNUMBER) { + size_t l = (size_t)luaL_checkinteger(L, n); + success = (l == 0) ? test_eof(L, f) : read_chars(L, f, l); + } + else { + const char *p = luaL_checkstring(L, n); + if (*p == '*') p++; /* skip optional '*' (for compatibility) */ + switch (*p) { + case 'n': /* number */ + success = read_number(L, f); + break; + case 'l': /* line */ + success = read_line(L, f, 1); + break; + case 'L': /* line with end-of-line */ + success = read_line(L, f, 0); + break; + case 'a': /* file */ + read_all(L, f); /* read entire file */ + success = 1; /* always success */ + break; + default: + return luaL_argerror(L, n, "invalid format"); + } + } + } + } + if (ferror(f)) + return luaL_fileresult(L, 0, NULL); + if (!success) { + lua_pop(L, 1); /* remove last result */ + lua_pushnil(L); /* push nil instead */ + } + return n - first; +} + + +static int io_read (lua_State *L) { + return g_read(L, getiofile(L, IO_INPUT), 1); +} + + +static int f_read (lua_State *L) { + return g_read(L, tofile(L), 2); +} + + +static int io_readline (lua_State *L) { + LStream *p = (LStream *)lua_touserdata(L, lua_upvalueindex(1)); + int i; + int n = (int)lua_tointeger(L, lua_upvalueindex(2)); + if (isclosed(p)) /* file is already closed? */ + return luaL_error(L, "file is already closed"); + lua_settop(L , 1); + luaL_checkstack(L, n, "too many arguments"); + for (i = 1; i <= n; i++) /* push arguments to 'g_read' */ + lua_pushvalue(L, lua_upvalueindex(3 + i)); + n = g_read(L, p->f, 2); /* 'n' is number of results */ + lua_assert(n > 0); /* should return at least a nil */ + if (lua_toboolean(L, -n)) /* read at least one value? */ + return n; /* return them */ + else { /* first result is nil: EOF or error */ + if (n > 1) { /* is there error information? */ + /* 2nd result is error message */ + return luaL_error(L, "%s", lua_tostring(L, -n + 1)); + } + if (lua_toboolean(L, lua_upvalueindex(3))) { /* generator created file? */ + lua_settop(L, 0); + lua_pushvalue(L, lua_upvalueindex(1)); + aux_close(L); /* close it */ + } + return 0; + } +} + +/* }====================================================== */ + + +static int g_write (lua_State *L, FILE *f, int arg) { + int nargs = lua_gettop(L) - arg; + int status = 1; + for (; nargs--; arg++) { + if (lua_type(L, arg) == LUA_TNUMBER) { + /* optimization: could be done exactly as for strings */ + int len = lua_isinteger(L, arg) + ? fprintf(f, LUA_INTEGER_FMT, + (LUAI_UACINT)lua_tointeger(L, arg)) + : fprintf(f, LUA_NUMBER_FMT, + (LUAI_UACNUMBER)lua_tonumber(L, arg)); + status = status && (len > 0); + } + else { + size_t l; + const char *s = luaL_checklstring(L, arg, &l); + status = status && (fwrite(s, sizeof(char), l, f) == l); + } + } + if (status) return 1; /* file handle already on stack top */ + else return luaL_fileresult(L, status, NULL); +} + + +static int io_write (lua_State *L) { + return g_write(L, getiofile(L, IO_OUTPUT), 1); +} + + +static int f_write (lua_State *L) { + FILE *f = tofile(L); + lua_pushvalue(L, 1); /* push file at the stack top (to be returned) */ + return g_write(L, f, 2); +} + + +static int f_seek (lua_State *L) { + static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END}; + static const char *const modenames[] = {"set", "cur", "end", NULL}; + FILE *f = tofile(L); + int op = luaL_checkoption(L, 2, "cur", modenames); + lua_Integer p3 = luaL_optinteger(L, 3, 0); + l_seeknum offset = (l_seeknum)p3; + luaL_argcheck(L, (lua_Integer)offset == p3, 3, + "not an integer in proper range"); + op = l_fseek(f, offset, mode[op]); + if (op) + return luaL_fileresult(L, 0, NULL); /* error */ + else { + lua_pushinteger(L, (lua_Integer)l_ftell(f)); + return 1; + } +} + + +static int f_setvbuf (lua_State *L) { + static const int mode[] = {_IONBF, _IOFBF, _IOLBF}; + static const char *const modenames[] = {"no", "full", "line", NULL}; + FILE *f = tofile(L); + int op = luaL_checkoption(L, 2, NULL, modenames); + lua_Integer sz = luaL_optinteger(L, 3, LUAL_BUFFERSIZE); + int res = setvbuf(f, NULL, mode[op], (size_t)sz); + return luaL_fileresult(L, res == 0, NULL); +} + + + +static int io_flush (lua_State *L) { + return luaL_fileresult(L, fflush(getiofile(L, IO_OUTPUT)) == 0, NULL); +} + + +static int f_flush (lua_State *L) { + return luaL_fileresult(L, fflush(tofile(L)) == 0, NULL); +} + + +/* +** functions for 'io' library +*/ +static const luaL_Reg iolib[] = { + {"close", io_close}, + {"flush", io_flush}, + {"input", io_input}, + {"lines", io_lines}, + {"open", io_open}, + {"output", io_output}, + {"popen", io_popen}, + {"read", io_read}, + {"tmpfile", io_tmpfile}, + {"type", io_type}, + {"write", io_write}, + {NULL, NULL} +}; + + +/* +** methods for file handles +*/ +static const luaL_Reg flib[] = { + {"close", io_close}, + {"flush", f_flush}, + {"lines", f_lines}, + {"read", f_read}, + {"seek", f_seek}, + {"setvbuf", f_setvbuf}, + {"write", f_write}, + {"__gc", f_gc}, + {"__tostring", f_tostring}, + {NULL, NULL} +}; + + +static void createmeta (lua_State *L) { + luaL_newmetatable(L, LUA_FILEHANDLE); /* create metatable for file handles */ + lua_pushvalue(L, -1); /* push metatable */ + lua_setfield(L, -2, "__index"); /* metatable.__index = metatable */ + luaL_setfuncs(L, flib, 0); /* add file methods to new metatable */ + lua_pop(L, 1); /* pop new metatable */ +} + + +/* +** function to (not) close the standard files stdin, stdout, and stderr +*/ +static int io_noclose (lua_State *L) { + LStream *p = tolstream(L); + p->closef = &io_noclose; /* keep file opened */ + lua_pushnil(L); + lua_pushliteral(L, "cannot close standard file"); + return 2; +} + + +static void createstdfile (lua_State *L, FILE *f, const char *k, + const char *fname) { + LStream *p = newprefile(L); + p->f = f; + p->closef = &io_noclose; + if (k != NULL) { + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, k); /* add file to registry */ + } + lua_setfield(L, -2, fname); /* add file to module */ +} + + +LUAMOD_API int luaopen_io (lua_State *L) { + luaL_newlib(L, iolib); /* new module */ + createmeta(L); + /* create (and set) default files */ + createstdfile(L, stdin, IO_INPUT, "stdin"); + createstdfile(L, stdout, IO_OUTPUT, "stdout"); + createstdfile(L, stderr, NULL, "stderr"); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/llex.c b/src/rcheevos/test/lua/src/llex.c new file mode 100644 index 000000000..70328273f --- /dev/null +++ b/src/rcheevos/test/lua/src/llex.c @@ -0,0 +1,565 @@ +/* +** $Id: llex.c,v 2.96 2016/05/02 14:02:12 roberto Exp $ +** Lexical Analyzer +** See Copyright Notice in lua.h +*/ + +#define llex_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lctype.h" +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "llex.h" +#include "lobject.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lzio.h" + + + +#define next(ls) (ls->current = zgetc(ls->z)) + + + +#define currIsNewline(ls) (ls->current == '\n' || ls->current == '\r') + + +/* ORDER RESERVED */ +static const char *const luaX_tokens [] = { + "and", "break", "do", "else", "elseif", + "end", "false", "for", "function", "goto", "if", + "in", "local", "nil", "not", "or", "repeat", + "return", "then", "true", "until", "while", + "//", "..", "...", "==", ">=", "<=", "~=", + "<<", ">>", "::", "", + "", "", "", "" +}; + + +#define save_and_next(ls) (save(ls, ls->current), next(ls)) + + +static l_noret lexerror (LexState *ls, const char *msg, int token); + + +static void save (LexState *ls, int c) { + Mbuffer *b = ls->buff; + if (luaZ_bufflen(b) + 1 > luaZ_sizebuffer(b)) { + size_t newsize; + if (luaZ_sizebuffer(b) >= MAX_SIZE/2) + lexerror(ls, "lexical element too long", 0); + newsize = luaZ_sizebuffer(b) * 2; + luaZ_resizebuffer(ls->L, b, newsize); + } + b->buffer[luaZ_bufflen(b)++] = cast(char, c); +} + + +void luaX_init (lua_State *L) { + int i; + TString *e = luaS_newliteral(L, LUA_ENV); /* create env name */ + luaC_fix(L, obj2gco(e)); /* never collect this name */ + for (i=0; iextra = cast_byte(i+1); /* reserved word */ + } +} + + +const char *luaX_token2str (LexState *ls, int token) { + if (token < FIRST_RESERVED) { /* single-byte symbols? */ + lua_assert(token == cast_uchar(token)); + return luaO_pushfstring(ls->L, "'%c'", token); + } + else { + const char *s = luaX_tokens[token - FIRST_RESERVED]; + if (token < TK_EOS) /* fixed format (symbols and reserved words)? */ + return luaO_pushfstring(ls->L, "'%s'", s); + else /* names, strings, and numerals */ + return s; + } +} + + +static const char *txtToken (LexState *ls, int token) { + switch (token) { + case TK_NAME: case TK_STRING: + case TK_FLT: case TK_INT: + save(ls, '\0'); + return luaO_pushfstring(ls->L, "'%s'", luaZ_buffer(ls->buff)); + default: + return luaX_token2str(ls, token); + } +} + + +static l_noret lexerror (LexState *ls, const char *msg, int token) { + msg = luaG_addinfo(ls->L, msg, ls->source, ls->linenumber); + if (token) + luaO_pushfstring(ls->L, "%s near %s", msg, txtToken(ls, token)); + luaD_throw(ls->L, LUA_ERRSYNTAX); +} + + +l_noret luaX_syntaxerror (LexState *ls, const char *msg) { + lexerror(ls, msg, ls->t.token); +} + + +/* +** creates a new string and anchors it in scanner's table so that +** it will not be collected until the end of the compilation +** (by that time it should be anchored somewhere) +*/ +TString *luaX_newstring (LexState *ls, const char *str, size_t l) { + lua_State *L = ls->L; + TValue *o; /* entry for 'str' */ + TString *ts = luaS_newlstr(L, str, l); /* create new string */ + setsvalue2s(L, L->top++, ts); /* temporarily anchor it in stack */ + o = luaH_set(L, ls->h, L->top - 1); + if (ttisnil(o)) { /* not in use yet? */ + /* boolean value does not need GC barrier; + table has no metatable, so it does not need to invalidate cache */ + setbvalue(o, 1); /* t[string] = true */ + luaC_checkGC(L); + } + else { /* string already present */ + ts = tsvalue(keyfromval(o)); /* re-use value previously stored */ + } + L->top--; /* remove string from stack */ + return ts; +} + + +/* +** increment line number and skips newline sequence (any of +** \n, \r, \n\r, or \r\n) +*/ +static void inclinenumber (LexState *ls) { + int old = ls->current; + lua_assert(currIsNewline(ls)); + next(ls); /* skip '\n' or '\r' */ + if (currIsNewline(ls) && ls->current != old) + next(ls); /* skip '\n\r' or '\r\n' */ + if (++ls->linenumber >= MAX_INT) + lexerror(ls, "chunk has too many lines", 0); +} + + +void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source, + int firstchar) { + ls->t.token = 0; + ls->L = L; + ls->current = firstchar; + ls->lookahead.token = TK_EOS; /* no look-ahead token */ + ls->z = z; + ls->fs = NULL; + ls->linenumber = 1; + ls->lastline = 1; + ls->source = source; + ls->envn = luaS_newliteral(L, LUA_ENV); /* get env name */ + luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER); /* initialize buffer */ +} + + + +/* +** ======================================================= +** LEXICAL ANALYZER +** ======================================================= +*/ + + +static int check_next1 (LexState *ls, int c) { + if (ls->current == c) { + next(ls); + return 1; + } + else return 0; +} + + +/* +** Check whether current char is in set 'set' (with two chars) and +** saves it +*/ +static int check_next2 (LexState *ls, const char *set) { + lua_assert(set[2] == '\0'); + if (ls->current == set[0] || ls->current == set[1]) { + save_and_next(ls); + return 1; + } + else return 0; +} + + +/* LUA_NUMBER */ +/* +** this function is quite liberal in what it accepts, as 'luaO_str2num' +** will reject ill-formed numerals. +*/ +static int read_numeral (LexState *ls, SemInfo *seminfo) { + TValue obj; + const char *expo = "Ee"; + int first = ls->current; + lua_assert(lisdigit(ls->current)); + save_and_next(ls); + if (first == '0' && check_next2(ls, "xX")) /* hexadecimal? */ + expo = "Pp"; + for (;;) { + if (check_next2(ls, expo)) /* exponent part? */ + check_next2(ls, "-+"); /* optional exponent sign */ + if (lisxdigit(ls->current)) + save_and_next(ls); + else if (ls->current == '.') + save_and_next(ls); + else break; + } + save(ls, '\0'); + if (luaO_str2num(luaZ_buffer(ls->buff), &obj) == 0) /* format error? */ + lexerror(ls, "malformed number", TK_FLT); + if (ttisinteger(&obj)) { + seminfo->i = ivalue(&obj); + return TK_INT; + } + else { + lua_assert(ttisfloat(&obj)); + seminfo->r = fltvalue(&obj); + return TK_FLT; + } +} + + +/* +** skip a sequence '[=*[' or ']=*]'; if sequence is well formed, return +** its number of '='s; otherwise, return a negative number (-1 iff there +** are no '='s after initial bracket) +*/ +static int skip_sep (LexState *ls) { + int count = 0; + int s = ls->current; + lua_assert(s == '[' || s == ']'); + save_and_next(ls); + while (ls->current == '=') { + save_and_next(ls); + count++; + } + return (ls->current == s) ? count : (-count) - 1; +} + + +static void read_long_string (LexState *ls, SemInfo *seminfo, int sep) { + int line = ls->linenumber; /* initial line (for error message) */ + save_and_next(ls); /* skip 2nd '[' */ + if (currIsNewline(ls)) /* string starts with a newline? */ + inclinenumber(ls); /* skip it */ + for (;;) { + switch (ls->current) { + case EOZ: { /* error */ + const char *what = (seminfo ? "string" : "comment"); + const char *msg = luaO_pushfstring(ls->L, + "unfinished long %s (starting at line %d)", what, line); + lexerror(ls, msg, TK_EOS); + break; /* to avoid warnings */ + } + case ']': { + if (skip_sep(ls) == sep) { + save_and_next(ls); /* skip 2nd ']' */ + goto endloop; + } + break; + } + case '\n': case '\r': { + save(ls, '\n'); + inclinenumber(ls); + if (!seminfo) luaZ_resetbuffer(ls->buff); /* avoid wasting space */ + break; + } + default: { + if (seminfo) save_and_next(ls); + else next(ls); + } + } + } endloop: + if (seminfo) + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + (2 + sep), + luaZ_bufflen(ls->buff) - 2*(2 + sep)); +} + + +static void esccheck (LexState *ls, int c, const char *msg) { + if (!c) { + if (ls->current != EOZ) + save_and_next(ls); /* add current to buffer for error message */ + lexerror(ls, msg, TK_STRING); + } +} + + +static int gethexa (LexState *ls) { + save_and_next(ls); + esccheck (ls, lisxdigit(ls->current), "hexadecimal digit expected"); + return luaO_hexavalue(ls->current); +} + + +static int readhexaesc (LexState *ls) { + int r = gethexa(ls); + r = (r << 4) + gethexa(ls); + luaZ_buffremove(ls->buff, 2); /* remove saved chars from buffer */ + return r; +} + + +static unsigned long readutf8esc (LexState *ls) { + unsigned long r; + int i = 4; /* chars to be removed: '\', 'u', '{', and first digit */ + save_and_next(ls); /* skip 'u' */ + esccheck(ls, ls->current == '{', "missing '{'"); + r = gethexa(ls); /* must have at least one digit */ + while ((save_and_next(ls), lisxdigit(ls->current))) { + i++; + r = (r << 4) + luaO_hexavalue(ls->current); + esccheck(ls, r <= 0x10FFFF, "UTF-8 value too large"); + } + esccheck(ls, ls->current == '}', "missing '}'"); + next(ls); /* skip '}' */ + luaZ_buffremove(ls->buff, i); /* remove saved chars from buffer */ + return r; +} + + +static void utf8esc (LexState *ls) { + char buff[UTF8BUFFSZ]; + int n = luaO_utf8esc(buff, readutf8esc(ls)); + for (; n > 0; n--) /* add 'buff' to string */ + save(ls, buff[UTF8BUFFSZ - n]); +} + + +static int readdecesc (LexState *ls) { + int i; + int r = 0; /* result accumulator */ + for (i = 0; i < 3 && lisdigit(ls->current); i++) { /* read up to 3 digits */ + r = 10*r + ls->current - '0'; + save_and_next(ls); + } + esccheck(ls, r <= UCHAR_MAX, "decimal escape too large"); + luaZ_buffremove(ls->buff, i); /* remove read digits from buffer */ + return r; +} + + +static void read_string (LexState *ls, int del, SemInfo *seminfo) { + save_and_next(ls); /* keep delimiter (for error messages) */ + while (ls->current != del) { + switch (ls->current) { + case EOZ: + lexerror(ls, "unfinished string", TK_EOS); + break; /* to avoid warnings */ + case '\n': + case '\r': + lexerror(ls, "unfinished string", TK_STRING); + break; /* to avoid warnings */ + case '\\': { /* escape sequences */ + int c; /* final character to be saved */ + save_and_next(ls); /* keep '\\' for error messages */ + switch (ls->current) { + case 'a': c = '\a'; goto read_save; + case 'b': c = '\b'; goto read_save; + case 'f': c = '\f'; goto read_save; + case 'n': c = '\n'; goto read_save; + case 'r': c = '\r'; goto read_save; + case 't': c = '\t'; goto read_save; + case 'v': c = '\v'; goto read_save; + case 'x': c = readhexaesc(ls); goto read_save; + case 'u': utf8esc(ls); goto no_save; + case '\n': case '\r': + inclinenumber(ls); c = '\n'; goto only_save; + case '\\': case '\"': case '\'': + c = ls->current; goto read_save; + case EOZ: goto no_save; /* will raise an error next loop */ + case 'z': { /* zap following span of spaces */ + luaZ_buffremove(ls->buff, 1); /* remove '\\' */ + next(ls); /* skip the 'z' */ + while (lisspace(ls->current)) { + if (currIsNewline(ls)) inclinenumber(ls); + else next(ls); + } + goto no_save; + } + default: { + esccheck(ls, lisdigit(ls->current), "invalid escape sequence"); + c = readdecesc(ls); /* digital escape '\ddd' */ + goto only_save; + } + } + read_save: + next(ls); + /* go through */ + only_save: + luaZ_buffremove(ls->buff, 1); /* remove '\\' */ + save(ls, c); + /* go through */ + no_save: break; + } + default: + save_and_next(ls); + } + } + save_and_next(ls); /* skip delimiter */ + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1, + luaZ_bufflen(ls->buff) - 2); +} + + +static int llex (LexState *ls, SemInfo *seminfo) { + luaZ_resetbuffer(ls->buff); + for (;;) { + switch (ls->current) { + case '\n': case '\r': { /* line breaks */ + inclinenumber(ls); + break; + } + case ' ': case '\f': case '\t': case '\v': { /* spaces */ + next(ls); + break; + } + case '-': { /* '-' or '--' (comment) */ + next(ls); + if (ls->current != '-') return '-'; + /* else is a comment */ + next(ls); + if (ls->current == '[') { /* long comment? */ + int sep = skip_sep(ls); + luaZ_resetbuffer(ls->buff); /* 'skip_sep' may dirty the buffer */ + if (sep >= 0) { + read_long_string(ls, NULL, sep); /* skip long comment */ + luaZ_resetbuffer(ls->buff); /* previous call may dirty the buff. */ + break; + } + } + /* else short comment */ + while (!currIsNewline(ls) && ls->current != EOZ) + next(ls); /* skip until end of line (or end of file) */ + break; + } + case '[': { /* long string or simply '[' */ + int sep = skip_sep(ls); + if (sep >= 0) { + read_long_string(ls, seminfo, sep); + return TK_STRING; + } + else if (sep != -1) /* '[=...' missing second bracket */ + lexerror(ls, "invalid long string delimiter", TK_STRING); + return '['; + } + case '=': { + next(ls); + if (check_next1(ls, '=')) return TK_EQ; + else return '='; + } + case '<': { + next(ls); + if (check_next1(ls, '=')) return TK_LE; + else if (check_next1(ls, '<')) return TK_SHL; + else return '<'; + } + case '>': { + next(ls); + if (check_next1(ls, '=')) return TK_GE; + else if (check_next1(ls, '>')) return TK_SHR; + else return '>'; + } + case '/': { + next(ls); + if (check_next1(ls, '/')) return TK_IDIV; + else return '/'; + } + case '~': { + next(ls); + if (check_next1(ls, '=')) return TK_NE; + else return '~'; + } + case ':': { + next(ls); + if (check_next1(ls, ':')) return TK_DBCOLON; + else return ':'; + } + case '"': case '\'': { /* short literal strings */ + read_string(ls, ls->current, seminfo); + return TK_STRING; + } + case '.': { /* '.', '..', '...', or number */ + save_and_next(ls); + if (check_next1(ls, '.')) { + if (check_next1(ls, '.')) + return TK_DOTS; /* '...' */ + else return TK_CONCAT; /* '..' */ + } + else if (!lisdigit(ls->current)) return '.'; + else return read_numeral(ls, seminfo); + } + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + return read_numeral(ls, seminfo); + } + case EOZ: { + return TK_EOS; + } + default: { + if (lislalpha(ls->current)) { /* identifier or reserved word? */ + TString *ts; + do { + save_and_next(ls); + } while (lislalnum(ls->current)); + ts = luaX_newstring(ls, luaZ_buffer(ls->buff), + luaZ_bufflen(ls->buff)); + seminfo->ts = ts; + if (isreserved(ts)) /* reserved word? */ + return ts->extra - 1 + FIRST_RESERVED; + else { + return TK_NAME; + } + } + else { /* single-char tokens (+ - / ...) */ + int c = ls->current; + next(ls); + return c; + } + } + } + } +} + + +void luaX_next (LexState *ls) { + ls->lastline = ls->linenumber; + if (ls->lookahead.token != TK_EOS) { /* is there a look-ahead token? */ + ls->t = ls->lookahead; /* use this one */ + ls->lookahead.token = TK_EOS; /* and discharge it */ + } + else + ls->t.token = llex(ls, &ls->t.seminfo); /* read next token */ +} + + +int luaX_lookahead (LexState *ls) { + lua_assert(ls->lookahead.token == TK_EOS); + ls->lookahead.token = llex(ls, &ls->lookahead.seminfo); + return ls->lookahead.token; +} + diff --git a/src/rcheevos/test/lua/src/llex.h b/src/rcheevos/test/lua/src/llex.h new file mode 100644 index 000000000..2363d87e4 --- /dev/null +++ b/src/rcheevos/test/lua/src/llex.h @@ -0,0 +1,85 @@ +/* +** $Id: llex.h,v 1.79 2016/05/02 14:02:12 roberto Exp $ +** Lexical Analyzer +** See Copyright Notice in lua.h +*/ + +#ifndef llex_h +#define llex_h + +#include "lobject.h" +#include "lzio.h" + + +#define FIRST_RESERVED 257 + + +#if !defined(LUA_ENV) +#define LUA_ENV "_ENV" +#endif + + +/* +* WARNING: if you change the order of this enumeration, +* grep "ORDER RESERVED" +*/ +enum RESERVED { + /* terminal symbols denoted by reserved words */ + TK_AND = FIRST_RESERVED, TK_BREAK, + TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION, + TK_GOTO, TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT, + TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE, + /* other terminal symbols */ + TK_IDIV, TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE, + TK_SHL, TK_SHR, + TK_DBCOLON, TK_EOS, + TK_FLT, TK_INT, TK_NAME, TK_STRING +}; + +/* number of reserved words */ +#define NUM_RESERVED (cast(int, TK_WHILE-FIRST_RESERVED+1)) + + +typedef union { + lua_Number r; + lua_Integer i; + TString *ts; +} SemInfo; /* semantics information */ + + +typedef struct Token { + int token; + SemInfo seminfo; +} Token; + + +/* state of the lexer plus state of the parser when shared by all + functions */ +typedef struct LexState { + int current; /* current character (charint) */ + int linenumber; /* input line counter */ + int lastline; /* line of last token 'consumed' */ + Token t; /* current token */ + Token lookahead; /* look ahead token */ + struct FuncState *fs; /* current function (parser) */ + struct lua_State *L; + ZIO *z; /* input stream */ + Mbuffer *buff; /* buffer for tokens */ + Table *h; /* to avoid collection/reuse strings */ + struct Dyndata *dyd; /* dynamic structures used by the parser */ + TString *source; /* current source name */ + TString *envn; /* environment variable name */ +} LexState; + + +LUAI_FUNC void luaX_init (lua_State *L); +LUAI_FUNC void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, + TString *source, int firstchar); +LUAI_FUNC TString *luaX_newstring (LexState *ls, const char *str, size_t l); +LUAI_FUNC void luaX_next (LexState *ls); +LUAI_FUNC int luaX_lookahead (LexState *ls); +LUAI_FUNC l_noret luaX_syntaxerror (LexState *ls, const char *s); +LUAI_FUNC const char *luaX_token2str (LexState *ls, int token); + + +#endif diff --git a/src/rcheevos/test/lua/src/llimits.h b/src/rcheevos/test/lua/src/llimits.h new file mode 100644 index 000000000..f21377fef --- /dev/null +++ b/src/rcheevos/test/lua/src/llimits.h @@ -0,0 +1,323 @@ +/* +** $Id: llimits.h,v 1.141 2015/11/19 19:16:22 roberto Exp $ +** Limits, basic types, and some other 'installation-dependent' definitions +** See Copyright Notice in lua.h +*/ + +#ifndef llimits_h +#define llimits_h + + +#include +#include + + +#include "lua.h" + +/* +** 'lu_mem' and 'l_mem' are unsigned/signed integers big enough to count +** the total memory used by Lua (in bytes). Usually, 'size_t' and +** 'ptrdiff_t' should work, but we use 'long' for 16-bit machines. +*/ +#if defined(LUAI_MEM) /* { external definitions? */ +typedef LUAI_UMEM lu_mem; +typedef LUAI_MEM l_mem; +#elif LUAI_BITSINT >= 32 /* }{ */ +typedef size_t lu_mem; +typedef ptrdiff_t l_mem; +#else /* 16-bit ints */ /* }{ */ +typedef unsigned long lu_mem; +typedef long l_mem; +#endif /* } */ + + +/* chars used as small naturals (so that 'char' is reserved for characters) */ +typedef unsigned char lu_byte; + + +/* maximum value for size_t */ +#define MAX_SIZET ((size_t)(~(size_t)0)) + +/* maximum size visible for Lua (must be representable in a lua_Integer */ +#define MAX_SIZE (sizeof(size_t) < sizeof(lua_Integer) ? MAX_SIZET \ + : (size_t)(LUA_MAXINTEGER)) + + +#define MAX_LUMEM ((lu_mem)(~(lu_mem)0)) + +#define MAX_LMEM ((l_mem)(MAX_LUMEM >> 1)) + + +#define MAX_INT INT_MAX /* maximum value of an int */ + + +/* +** conversion of pointer to unsigned integer: +** this is for hashing only; there is no problem if the integer +** cannot hold the whole pointer value +*/ +#define point2uint(p) ((unsigned int)((size_t)(p) & UINT_MAX)) + + + +/* type to ensure maximum alignment */ +#if defined(LUAI_USER_ALIGNMENT_T) +typedef LUAI_USER_ALIGNMENT_T L_Umaxalign; +#else +typedef union { + lua_Number n; + double u; + void *s; + lua_Integer i; + long l; +} L_Umaxalign; +#endif + + + +/* types of 'usual argument conversions' for lua_Number and lua_Integer */ +typedef LUAI_UACNUMBER l_uacNumber; +typedef LUAI_UACINT l_uacInt; + + +/* internal assertions for in-house debugging */ +#if defined(lua_assert) +#define check_exp(c,e) (lua_assert(c), (e)) +/* to avoid problems with conditions too long */ +#define lua_longassert(c) ((c) ? (void)0 : lua_assert(0)) +#else +#define lua_assert(c) ((void)0) +#define check_exp(c,e) (e) +#define lua_longassert(c) ((void)0) +#endif + +/* +** assertion for checking API calls +*/ +#if !defined(luai_apicheck) +#define luai_apicheck(l,e) lua_assert(e) +#endif + +#define api_check(l,e,msg) luai_apicheck(l,(e) && msg) + + +/* macro to avoid warnings about unused variables */ +#if !defined(UNUSED) +#define UNUSED(x) ((void)(x)) +#endif + + +/* type casts (a macro highlights casts in the code) */ +#define cast(t, exp) ((t)(exp)) + +#define cast_void(i) cast(void, (i)) +#define cast_byte(i) cast(lu_byte, (i)) +#define cast_num(i) cast(lua_Number, (i)) +#define cast_int(i) cast(int, (i)) +#define cast_uchar(i) cast(unsigned char, (i)) + + +/* cast a signed lua_Integer to lua_Unsigned */ +#if !defined(l_castS2U) +#define l_castS2U(i) ((lua_Unsigned)(i)) +#endif + +/* +** cast a lua_Unsigned to a signed lua_Integer; this cast is +** not strict ISO C, but two-complement architectures should +** work fine. +*/ +#if !defined(l_castU2S) +#define l_castU2S(i) ((lua_Integer)(i)) +#endif + + +/* +** non-return type +*/ +#if defined(__GNUC__) +#define l_noret void __attribute__((noreturn)) +#elif defined(_MSC_VER) && _MSC_VER >= 1200 +#define l_noret void __declspec(noreturn) +#else +#define l_noret void +#endif + + + +/* +** maximum depth for nested C calls and syntactical nested non-terminals +** in a program. (Value must fit in an unsigned short int.) +*/ +#if !defined(LUAI_MAXCCALLS) +#define LUAI_MAXCCALLS 200 +#endif + + + +/* +** type for virtual-machine instructions; +** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h) +*/ +#if LUAI_BITSINT >= 32 +typedef unsigned int Instruction; +#else +typedef unsigned long Instruction; +#endif + + + +/* +** Maximum length for short strings, that is, strings that are +** internalized. (Cannot be smaller than reserved words or tags for +** metamethods, as these strings must be internalized; +** #("function") = 8, #("__newindex") = 10.) +*/ +#if !defined(LUAI_MAXSHORTLEN) +#define LUAI_MAXSHORTLEN 40 +#endif + + +/* +** Initial size for the string table (must be power of 2). +** The Lua core alone registers ~50 strings (reserved words + +** metaevent keys + a few others). Libraries would typically add +** a few dozens more. +*/ +#if !defined(MINSTRTABSIZE) +#define MINSTRTABSIZE 128 +#endif + + +/* +** Size of cache for strings in the API. 'N' is the number of +** sets (better be a prime) and "M" is the size of each set (M == 1 +** makes a direct cache.) +*/ +#if !defined(STRCACHE_N) +#define STRCACHE_N 53 +#define STRCACHE_M 2 +#endif + + +/* minimum size for string buffer */ +#if !defined(LUA_MINBUFFER) +#define LUA_MINBUFFER 32 +#endif + + +/* +** macros that are executed whenever program enters the Lua core +** ('lua_lock') and leaves the core ('lua_unlock') +*/ +#if !defined(lua_lock) +#define lua_lock(L) ((void) 0) +#define lua_unlock(L) ((void) 0) +#endif + +/* +** macro executed during Lua functions at points where the +** function can yield. +*/ +#if !defined(luai_threadyield) +#define luai_threadyield(L) {lua_unlock(L); lua_lock(L);} +#endif + + +/* +** these macros allow user-specific actions on threads when you defined +** LUAI_EXTRASPACE and need to do something extra when a thread is +** created/deleted/resumed/yielded. +*/ +#if !defined(luai_userstateopen) +#define luai_userstateopen(L) ((void)L) +#endif + +#if !defined(luai_userstateclose) +#define luai_userstateclose(L) ((void)L) +#endif + +#if !defined(luai_userstatethread) +#define luai_userstatethread(L,L1) ((void)L) +#endif + +#if !defined(luai_userstatefree) +#define luai_userstatefree(L,L1) ((void)L) +#endif + +#if !defined(luai_userstateresume) +#define luai_userstateresume(L,n) ((void)L) +#endif + +#if !defined(luai_userstateyield) +#define luai_userstateyield(L,n) ((void)L) +#endif + + + +/* +** The luai_num* macros define the primitive operations over numbers. +*/ + +/* floor division (defined as 'floor(a/b)') */ +#if !defined(luai_numidiv) +#define luai_numidiv(L,a,b) ((void)L, l_floor(luai_numdiv(L,a,b))) +#endif + +/* float division */ +#if !defined(luai_numdiv) +#define luai_numdiv(L,a,b) ((a)/(b)) +#endif + +/* +** modulo: defined as 'a - floor(a/b)*b'; this definition gives NaN when +** 'b' is huge, but the result should be 'a'. 'fmod' gives the result of +** 'a - trunc(a/b)*b', and therefore must be corrected when 'trunc(a/b) +** ~= floor(a/b)'. That happens when the division has a non-integer +** negative result, which is equivalent to the test below. +*/ +#if !defined(luai_nummod) +#define luai_nummod(L,a,b,m) \ + { (m) = l_mathop(fmod)(a,b); if ((m)*(b) < 0) (m) += (b); } +#endif + +/* exponentiation */ +#if !defined(luai_numpow) +#define luai_numpow(L,a,b) ((void)L, l_mathop(pow)(a,b)) +#endif + +/* the others are quite standard operations */ +#if !defined(luai_numadd) +#define luai_numadd(L,a,b) ((a)+(b)) +#define luai_numsub(L,a,b) ((a)-(b)) +#define luai_nummul(L,a,b) ((a)*(b)) +#define luai_numunm(L,a) (-(a)) +#define luai_numeq(a,b) ((a)==(b)) +#define luai_numlt(a,b) ((a)<(b)) +#define luai_numle(a,b) ((a)<=(b)) +#define luai_numisnan(a) (!luai_numeq((a), (a))) +#endif + + + + + +/* +** macro to control inclusion of some hard tests on stack reallocation +*/ +#if !defined(HARDSTACKTESTS) +#define condmovestack(L,pre,pos) ((void)0) +#else +/* realloc stack keeping its size */ +#define condmovestack(L,pre,pos) \ + { int sz_ = (L)->stacksize; pre; luaD_reallocstack((L), sz_); pos; } +#endif + +#if !defined(HARDMEMTESTS) +#define condchangemem(L,pre,pos) ((void)0) +#else +#define condchangemem(L,pre,pos) \ + { if (G(L)->gcrunning) { pre; luaC_fullgc(L, 0); pos; } } +#endif + +#endif diff --git a/src/rcheevos/test/lua/src/lmathlib.c b/src/rcheevos/test/lua/src/lmathlib.c new file mode 100644 index 000000000..b7f8baee0 --- /dev/null +++ b/src/rcheevos/test/lua/src/lmathlib.c @@ -0,0 +1,410 @@ +/* +** $Id: lmathlib.c,v 1.119 2016/12/22 13:08:50 roberto Exp $ +** Standard mathematical library +** See Copyright Notice in lua.h +*/ + +#define lmathlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +#undef PI +#define PI (l_mathop(3.141592653589793238462643383279502884)) + + +#if !defined(l_rand) /* { */ +#if defined(LUA_USE_POSIX) +#define l_rand() random() +#define l_srand(x) srandom(x) +#define L_RANDMAX 2147483647 /* (2^31 - 1), following POSIX */ +#else +#define l_rand() rand() +#define l_srand(x) srand(x) +#define L_RANDMAX RAND_MAX +#endif +#endif /* } */ + + +static int math_abs (lua_State *L) { + if (lua_isinteger(L, 1)) { + lua_Integer n = lua_tointeger(L, 1); + if (n < 0) n = (lua_Integer)(0u - (lua_Unsigned)n); + lua_pushinteger(L, n); + } + else + lua_pushnumber(L, l_mathop(fabs)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sin (lua_State *L) { + lua_pushnumber(L, l_mathop(sin)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_cos (lua_State *L) { + lua_pushnumber(L, l_mathop(cos)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tan (lua_State *L) { + lua_pushnumber(L, l_mathop(tan)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_asin (lua_State *L) { + lua_pushnumber(L, l_mathop(asin)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_acos (lua_State *L) { + lua_pushnumber(L, l_mathop(acos)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_atan (lua_State *L) { + lua_Number y = luaL_checknumber(L, 1); + lua_Number x = luaL_optnumber(L, 2, 1); + lua_pushnumber(L, l_mathop(atan2)(y, x)); + return 1; +} + + +static int math_toint (lua_State *L) { + int valid; + lua_Integer n = lua_tointegerx(L, 1, &valid); + if (valid) + lua_pushinteger(L, n); + else { + luaL_checkany(L, 1); + lua_pushnil(L); /* value is not convertible to integer */ + } + return 1; +} + + +static void pushnumint (lua_State *L, lua_Number d) { + lua_Integer n; + if (lua_numbertointeger(d, &n)) /* does 'd' fit in an integer? */ + lua_pushinteger(L, n); /* result is integer */ + else + lua_pushnumber(L, d); /* result is float */ +} + + +static int math_floor (lua_State *L) { + if (lua_isinteger(L, 1)) + lua_settop(L, 1); /* integer is its own floor */ + else { + lua_Number d = l_mathop(floor)(luaL_checknumber(L, 1)); + pushnumint(L, d); + } + return 1; +} + + +static int math_ceil (lua_State *L) { + if (lua_isinteger(L, 1)) + lua_settop(L, 1); /* integer is its own ceil */ + else { + lua_Number d = l_mathop(ceil)(luaL_checknumber(L, 1)); + pushnumint(L, d); + } + return 1; +} + + +static int math_fmod (lua_State *L) { + if (lua_isinteger(L, 1) && lua_isinteger(L, 2)) { + lua_Integer d = lua_tointeger(L, 2); + if ((lua_Unsigned)d + 1u <= 1u) { /* special cases: -1 or 0 */ + luaL_argcheck(L, d != 0, 2, "zero"); + lua_pushinteger(L, 0); /* avoid overflow with 0x80000... / -1 */ + } + else + lua_pushinteger(L, lua_tointeger(L, 1) % d); + } + else + lua_pushnumber(L, l_mathop(fmod)(luaL_checknumber(L, 1), + luaL_checknumber(L, 2))); + return 1; +} + + +/* +** next function does not use 'modf', avoiding problems with 'double*' +** (which is not compatible with 'float*') when lua_Number is not +** 'double'. +*/ +static int math_modf (lua_State *L) { + if (lua_isinteger(L ,1)) { + lua_settop(L, 1); /* number is its own integer part */ + lua_pushnumber(L, 0); /* no fractional part */ + } + else { + lua_Number n = luaL_checknumber(L, 1); + /* integer part (rounds toward zero) */ + lua_Number ip = (n < 0) ? l_mathop(ceil)(n) : l_mathop(floor)(n); + pushnumint(L, ip); + /* fractional part (test needed for inf/-inf) */ + lua_pushnumber(L, (n == ip) ? l_mathop(0.0) : (n - ip)); + } + return 2; +} + + +static int math_sqrt (lua_State *L) { + lua_pushnumber(L, l_mathop(sqrt)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_ult (lua_State *L) { + lua_Integer a = luaL_checkinteger(L, 1); + lua_Integer b = luaL_checkinteger(L, 2); + lua_pushboolean(L, (lua_Unsigned)a < (lua_Unsigned)b); + return 1; +} + +static int math_log (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + lua_Number res; + if (lua_isnoneornil(L, 2)) + res = l_mathop(log)(x); + else { + lua_Number base = luaL_checknumber(L, 2); +#if !defined(LUA_USE_C89) + if (base == l_mathop(2.0)) + res = l_mathop(log2)(x); else +#endif + if (base == l_mathop(10.0)) + res = l_mathop(log10)(x); + else + res = l_mathop(log)(x)/l_mathop(log)(base); + } + lua_pushnumber(L, res); + return 1; +} + +static int math_exp (lua_State *L) { + lua_pushnumber(L, l_mathop(exp)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_deg (lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1) * (l_mathop(180.0) / PI)); + return 1; +} + +static int math_rad (lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1) * (PI / l_mathop(180.0))); + return 1; +} + + +static int math_min (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int imin = 1; /* index of current minimum value */ + int i; + luaL_argcheck(L, n >= 1, 1, "value expected"); + for (i = 2; i <= n; i++) { + if (lua_compare(L, i, imin, LUA_OPLT)) + imin = i; + } + lua_pushvalue(L, imin); + return 1; +} + + +static int math_max (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int imax = 1; /* index of current maximum value */ + int i; + luaL_argcheck(L, n >= 1, 1, "value expected"); + for (i = 2; i <= n; i++) { + if (lua_compare(L, imax, i, LUA_OPLT)) + imax = i; + } + lua_pushvalue(L, imax); + return 1; +} + +/* +** This function uses 'double' (instead of 'lua_Number') to ensure that +** all bits from 'l_rand' can be represented, and that 'RANDMAX + 1.0' +** will keep full precision (ensuring that 'r' is always less than 1.0.) +*/ +static int math_random (lua_State *L) { + lua_Integer low, up; + double r = (double)l_rand() * (1.0 / ((double)L_RANDMAX + 1.0)); + switch (lua_gettop(L)) { /* check number of arguments */ + case 0: { /* no arguments */ + lua_pushnumber(L, (lua_Number)r); /* Number between 0 and 1 */ + return 1; + } + case 1: { /* only upper limit */ + low = 1; + up = luaL_checkinteger(L, 1); + break; + } + case 2: { /* lower and upper limits */ + low = luaL_checkinteger(L, 1); + up = luaL_checkinteger(L, 2); + break; + } + default: return luaL_error(L, "wrong number of arguments"); + } + /* random integer in the interval [low, up] */ + luaL_argcheck(L, low <= up, 1, "interval is empty"); + luaL_argcheck(L, low >= 0 || up <= LUA_MAXINTEGER + low, 1, + "interval too large"); + r *= (double)(up - low) + 1.0; + lua_pushinteger(L, (lua_Integer)r + low); + return 1; +} + + +static int math_randomseed (lua_State *L) { + l_srand((unsigned int)(lua_Integer)luaL_checknumber(L, 1)); + (void)l_rand(); /* discard first value to avoid undesirable correlations */ + return 0; +} + + +static int math_type (lua_State *L) { + if (lua_type(L, 1) == LUA_TNUMBER) { + if (lua_isinteger(L, 1)) + lua_pushliteral(L, "integer"); + else + lua_pushliteral(L, "float"); + } + else { + luaL_checkany(L, 1); + lua_pushnil(L); + } + return 1; +} + + +/* +** {================================================================== +** Deprecated functions (for compatibility only) +** =================================================================== +*/ +#if defined(LUA_COMPAT_MATHLIB) + +static int math_cosh (lua_State *L) { + lua_pushnumber(L, l_mathop(cosh)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sinh (lua_State *L) { + lua_pushnumber(L, l_mathop(sinh)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tanh (lua_State *L) { + lua_pushnumber(L, l_mathop(tanh)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_pow (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + lua_Number y = luaL_checknumber(L, 2); + lua_pushnumber(L, l_mathop(pow)(x, y)); + return 1; +} + +static int math_frexp (lua_State *L) { + int e; + lua_pushnumber(L, l_mathop(frexp)(luaL_checknumber(L, 1), &e)); + lua_pushinteger(L, e); + return 2; +} + +static int math_ldexp (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + int ep = (int)luaL_checkinteger(L, 2); + lua_pushnumber(L, l_mathop(ldexp)(x, ep)); + return 1; +} + +static int math_log10 (lua_State *L) { + lua_pushnumber(L, l_mathop(log10)(luaL_checknumber(L, 1))); + return 1; +} + +#endif +/* }================================================================== */ + + + +static const luaL_Reg mathlib[] = { + {"abs", math_abs}, + {"acos", math_acos}, + {"asin", math_asin}, + {"atan", math_atan}, + {"ceil", math_ceil}, + {"cos", math_cos}, + {"deg", math_deg}, + {"exp", math_exp}, + {"tointeger", math_toint}, + {"floor", math_floor}, + {"fmod", math_fmod}, + {"ult", math_ult}, + {"log", math_log}, + {"max", math_max}, + {"min", math_min}, + {"modf", math_modf}, + {"rad", math_rad}, + {"random", math_random}, + {"randomseed", math_randomseed}, + {"sin", math_sin}, + {"sqrt", math_sqrt}, + {"tan", math_tan}, + {"type", math_type}, +#if defined(LUA_COMPAT_MATHLIB) + {"atan2", math_atan}, + {"cosh", math_cosh}, + {"sinh", math_sinh}, + {"tanh", math_tanh}, + {"pow", math_pow}, + {"frexp", math_frexp}, + {"ldexp", math_ldexp}, + {"log10", math_log10}, +#endif + /* placeholders */ + {"pi", NULL}, + {"huge", NULL}, + {"maxinteger", NULL}, + {"mininteger", NULL}, + {NULL, NULL} +}; + + +/* +** Open math library +*/ +LUAMOD_API int luaopen_math (lua_State *L) { + luaL_newlib(L, mathlib); + lua_pushnumber(L, PI); + lua_setfield(L, -2, "pi"); + lua_pushnumber(L, (lua_Number)HUGE_VAL); + lua_setfield(L, -2, "huge"); + lua_pushinteger(L, LUA_MAXINTEGER); + lua_setfield(L, -2, "maxinteger"); + lua_pushinteger(L, LUA_MININTEGER); + lua_setfield(L, -2, "mininteger"); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/lmem.c b/src/rcheevos/test/lua/src/lmem.c new file mode 100644 index 000000000..0a0476cc7 --- /dev/null +++ b/src/rcheevos/test/lua/src/lmem.c @@ -0,0 +1,100 @@ +/* +** $Id: lmem.c,v 1.91 2015/03/06 19:45:54 roberto Exp $ +** Interface to Memory Manager +** See Copyright Notice in lua.h +*/ + +#define lmem_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" + + + +/* +** About the realloc function: +** void * frealloc (void *ud, void *ptr, size_t osize, size_t nsize); +** ('osize' is the old size, 'nsize' is the new size) +** +** * frealloc(ud, NULL, x, s) creates a new block of size 's' (no +** matter 'x'). +** +** * frealloc(ud, p, x, 0) frees the block 'p' +** (in this specific case, frealloc must return NULL); +** particularly, frealloc(ud, NULL, 0, 0) does nothing +** (which is equivalent to free(NULL) in ISO C) +** +** frealloc returns NULL if it cannot create or reallocate the area +** (any reallocation to an equal or smaller size cannot fail!) +*/ + + + +#define MINSIZEARRAY 4 + + +void *luaM_growaux_ (lua_State *L, void *block, int *size, size_t size_elems, + int limit, const char *what) { + void *newblock; + int newsize; + if (*size >= limit/2) { /* cannot double it? */ + if (*size >= limit) /* cannot grow even a little? */ + luaG_runerror(L, "too many %s (limit is %d)", what, limit); + newsize = limit; /* still have at least one free place */ + } + else { + newsize = (*size)*2; + if (newsize < MINSIZEARRAY) + newsize = MINSIZEARRAY; /* minimum size */ + } + newblock = luaM_reallocv(L, block, *size, newsize, size_elems); + *size = newsize; /* update only when everything else is OK */ + return newblock; +} + + +l_noret luaM_toobig (lua_State *L) { + luaG_runerror(L, "memory allocation error: block too big"); +} + + + +/* +** generic allocation routine. +*/ +void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) { + void *newblock; + global_State *g = G(L); + size_t realosize = (block) ? osize : 0; + lua_assert((realosize == 0) == (block == NULL)); +#if defined(HARDMEMTESTS) + if (nsize > realosize && g->gcrunning) + luaC_fullgc(L, 1); /* force a GC whenever possible */ +#endif + newblock = (*g->frealloc)(g->ud, block, osize, nsize); + if (newblock == NULL && nsize > 0) { + lua_assert(nsize > realosize); /* cannot fail when shrinking a block */ + if (g->version) { /* is state fully built? */ + luaC_fullgc(L, 1); /* try to free some memory... */ + newblock = (*g->frealloc)(g->ud, block, osize, nsize); /* try again */ + } + if (newblock == NULL) + luaD_throw(L, LUA_ERRMEM); + } + lua_assert((nsize == 0) == (newblock == NULL)); + g->GCdebt = (g->GCdebt + nsize) - realosize; + return newblock; +} + diff --git a/src/rcheevos/test/lua/src/lmem.h b/src/rcheevos/test/lua/src/lmem.h new file mode 100644 index 000000000..30f484895 --- /dev/null +++ b/src/rcheevos/test/lua/src/lmem.h @@ -0,0 +1,69 @@ +/* +** $Id: lmem.h,v 1.43 2014/12/19 17:26:14 roberto Exp $ +** Interface to Memory Manager +** See Copyright Notice in lua.h +*/ + +#ifndef lmem_h +#define lmem_h + + +#include + +#include "llimits.h" +#include "lua.h" + + +/* +** This macro reallocs a vector 'b' from 'on' to 'n' elements, where +** each element has size 'e'. In case of arithmetic overflow of the +** product 'n'*'e', it raises an error (calling 'luaM_toobig'). Because +** 'e' is always constant, it avoids the runtime division MAX_SIZET/(e). +** +** (The macro is somewhat complex to avoid warnings: The 'sizeof' +** comparison avoids a runtime comparison when overflow cannot occur. +** The compiler should be able to optimize the real test by itself, but +** when it does it, it may give a warning about "comparison is always +** false due to limited range of data type"; the +1 tricks the compiler, +** avoiding this warning but also this optimization.) +*/ +#define luaM_reallocv(L,b,on,n,e) \ + (((sizeof(n) >= sizeof(size_t) && cast(size_t, (n)) + 1 > MAX_SIZET/(e)) \ + ? luaM_toobig(L) : cast_void(0)) , \ + luaM_realloc_(L, (b), (on)*(e), (n)*(e))) + +/* +** Arrays of chars do not need any test +*/ +#define luaM_reallocvchar(L,b,on,n) \ + cast(char *, luaM_realloc_(L, (b), (on)*sizeof(char), (n)*sizeof(char))) + +#define luaM_freemem(L, b, s) luaM_realloc_(L, (b), (s), 0) +#define luaM_free(L, b) luaM_realloc_(L, (b), sizeof(*(b)), 0) +#define luaM_freearray(L, b, n) luaM_realloc_(L, (b), (n)*sizeof(*(b)), 0) + +#define luaM_malloc(L,s) luaM_realloc_(L, NULL, 0, (s)) +#define luaM_new(L,t) cast(t *, luaM_malloc(L, sizeof(t))) +#define luaM_newvector(L,n,t) \ + cast(t *, luaM_reallocv(L, NULL, 0, n, sizeof(t))) + +#define luaM_newobject(L,tag,s) luaM_realloc_(L, NULL, tag, (s)) + +#define luaM_growvector(L,v,nelems,size,t,limit,e) \ + if ((nelems)+1 > (size)) \ + ((v)=cast(t *, luaM_growaux_(L,v,&(size),sizeof(t),limit,e))) + +#define luaM_reallocvector(L, v,oldn,n,t) \ + ((v)=cast(t *, luaM_reallocv(L, v, oldn, n, sizeof(t)))) + +LUAI_FUNC l_noret luaM_toobig (lua_State *L); + +/* not to be called directly */ +LUAI_FUNC void *luaM_realloc_ (lua_State *L, void *block, size_t oldsize, + size_t size); +LUAI_FUNC void *luaM_growaux_ (lua_State *L, void *block, int *size, + size_t size_elem, int limit, + const char *what); + +#endif + diff --git a/src/rcheevos/test/lua/src/loadlib.c b/src/rcheevos/test/lua/src/loadlib.c new file mode 100644 index 000000000..4791e748b --- /dev/null +++ b/src/rcheevos/test/lua/src/loadlib.c @@ -0,0 +1,790 @@ +/* +** $Id: loadlib.c,v 1.130 2017/01/12 17:14:26 roberto Exp $ +** Dynamic library loader for Lua +** See Copyright Notice in lua.h +** +** This module contains an implementation of loadlib for Unix systems +** that have dlfcn, an implementation for Windows, and a stub for other +** systems. +*/ + +#define loadlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* +** LUA_IGMARK is a mark to ignore all before it when building the +** luaopen_ function name. +*/ +#if !defined (LUA_IGMARK) +#define LUA_IGMARK "-" +#endif + + +/* +** LUA_CSUBSEP is the character that replaces dots in submodule names +** when searching for a C loader. +** LUA_LSUBSEP is the character that replaces dots in submodule names +** when searching for a Lua loader. +*/ +#if !defined(LUA_CSUBSEP) +#define LUA_CSUBSEP LUA_DIRSEP +#endif + +#if !defined(LUA_LSUBSEP) +#define LUA_LSUBSEP LUA_DIRSEP +#endif + + +/* prefix for open functions in C libraries */ +#define LUA_POF "luaopen_" + +/* separator for open functions in C libraries */ +#define LUA_OFSEP "_" + + +/* +** unique key for table in the registry that keeps handles +** for all loaded C libraries +*/ +static const int CLIBS = 0; + +#define LIB_FAIL "open" + + +#define setprogdir(L) ((void)0) + + +/* +** system-dependent functions +*/ + +/* +** unload library 'lib' +*/ +static void lsys_unloadlib (void *lib); + +/* +** load C library in file 'path'. If 'seeglb', load with all names in +** the library global. +** Returns the library; in case of error, returns NULL plus an +** error string in the stack. +*/ +static void *lsys_load (lua_State *L, const char *path, int seeglb); + +/* +** Try to find a function named 'sym' in library 'lib'. +** Returns the function; in case of error, returns NULL plus an +** error string in the stack. +*/ +static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym); + + + + +#if defined(LUA_USE_DLOPEN) /* { */ +/* +** {======================================================================== +** This is an implementation of loadlib based on the dlfcn interface. +** The dlfcn interface is available in Linux, SunOS, Solaris, IRIX, FreeBSD, +** NetBSD, AIX 4.2, HPUX 11, and probably most other Unix flavors, at least +** as an emulation layer on top of native functions. +** ========================================================================= +*/ + +#include + +/* +** Macro to convert pointer-to-void* to pointer-to-function. This cast +** is undefined according to ISO C, but POSIX assumes that it works. +** (The '__extension__' in gnu compilers is only to avoid warnings.) +*/ +#if defined(__GNUC__) +#define cast_func(p) (__extension__ (lua_CFunction)(p)) +#else +#define cast_func(p) ((lua_CFunction)(p)) +#endif + + +static void lsys_unloadlib (void *lib) { + dlclose(lib); +} + + +static void *lsys_load (lua_State *L, const char *path, int seeglb) { + void *lib = dlopen(path, RTLD_NOW | (seeglb ? RTLD_GLOBAL : RTLD_LOCAL)); + if (lib == NULL) lua_pushstring(L, dlerror()); + return lib; +} + + +static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym) { + lua_CFunction f = cast_func(dlsym(lib, sym)); + if (f == NULL) lua_pushstring(L, dlerror()); + return f; +} + +/* }====================================================== */ + + + +#elif defined(LUA_DL_DLL) /* }{ */ +/* +** {====================================================================== +** This is an implementation of loadlib for Windows using native functions. +** ======================================================================= +*/ + +#include + + +/* +** optional flags for LoadLibraryEx +*/ +#if !defined(LUA_LLE_FLAGS) +#define LUA_LLE_FLAGS 0 +#endif + + +#undef setprogdir + + +/* +** Replace in the path (on the top of the stack) any occurrence +** of LUA_EXEC_DIR with the executable's path. +*/ +static void setprogdir (lua_State *L) { + char buff[MAX_PATH + 1]; + char *lb; + DWORD nsize = sizeof(buff)/sizeof(char); + DWORD n = GetModuleFileNameA(NULL, buff, nsize); /* get exec. name */ + if (n == 0 || n == nsize || (lb = strrchr(buff, '\\')) == NULL) + luaL_error(L, "unable to get ModuleFileName"); + else { + *lb = '\0'; /* cut name on the last '\\' to get the path */ + luaL_gsub(L, lua_tostring(L, -1), LUA_EXEC_DIR, buff); + lua_remove(L, -2); /* remove original string */ + } +} + + + + +static void pusherror (lua_State *L) { + int error = GetLastError(); + char buffer[128]; + if (FormatMessageA(FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, + NULL, error, 0, buffer, sizeof(buffer)/sizeof(char), NULL)) + lua_pushstring(L, buffer); + else + lua_pushfstring(L, "system error %d\n", error); +} + +static void lsys_unloadlib (void *lib) { + FreeLibrary((HMODULE)lib); +} + + +static void *lsys_load (lua_State *L, const char *path, int seeglb) { + HMODULE lib = LoadLibraryExA(path, NULL, LUA_LLE_FLAGS); + (void)(seeglb); /* not used: symbols are 'global' by default */ + if (lib == NULL) pusherror(L); + return lib; +} + + +static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym) { + lua_CFunction f = (lua_CFunction)GetProcAddress((HMODULE)lib, sym); + if (f == NULL) pusherror(L); + return f; +} + +/* }====================================================== */ + + +#else /* }{ */ +/* +** {====================================================== +** Fallback for other systems +** ======================================================= +*/ + +#undef LIB_FAIL +#define LIB_FAIL "absent" + + +#define DLMSG "dynamic libraries not enabled; check your Lua installation" + + +static void lsys_unloadlib (void *lib) { + (void)(lib); /* not used */ +} + + +static void *lsys_load (lua_State *L, const char *path, int seeglb) { + (void)(path); (void)(seeglb); /* not used */ + lua_pushliteral(L, DLMSG); + return NULL; +} + + +static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym) { + (void)(lib); (void)(sym); /* not used */ + lua_pushliteral(L, DLMSG); + return NULL; +} + +/* }====================================================== */ +#endif /* } */ + + +/* +** {================================================================== +** Set Paths +** =================================================================== +*/ + +/* +** LUA_PATH_VAR and LUA_CPATH_VAR are the names of the environment +** variables that Lua check to set its paths. +*/ +#if !defined(LUA_PATH_VAR) +#define LUA_PATH_VAR "LUA_PATH" +#endif + +#if !defined(LUA_CPATH_VAR) +#define LUA_CPATH_VAR "LUA_CPATH" +#endif + + +#define AUXMARK "\1" /* auxiliary mark */ + + +/* +** return registry.LUA_NOENV as a boolean +*/ +static int noenv (lua_State *L) { + int b; + lua_getfield(L, LUA_REGISTRYINDEX, "LUA_NOENV"); + b = lua_toboolean(L, -1); + lua_pop(L, 1); /* remove value */ + return b; +} + + +/* +** Set a path +*/ +static void setpath (lua_State *L, const char *fieldname, + const char *envname, + const char *dft) { + const char *nver = lua_pushfstring(L, "%s%s", envname, LUA_VERSUFFIX); + const char *path = getenv(nver); /* use versioned name */ + if (path == NULL) /* no environment variable? */ + path = getenv(envname); /* try unversioned name */ + if (path == NULL || noenv(L)) /* no environment variable? */ + lua_pushstring(L, dft); /* use default */ + else { + /* replace ";;" by ";AUXMARK;" and then AUXMARK by default path */ + path = luaL_gsub(L, path, LUA_PATH_SEP LUA_PATH_SEP, + LUA_PATH_SEP AUXMARK LUA_PATH_SEP); + luaL_gsub(L, path, AUXMARK, dft); + lua_remove(L, -2); /* remove result from 1st 'gsub' */ + } + setprogdir(L); + lua_setfield(L, -3, fieldname); /* package[fieldname] = path value */ + lua_pop(L, 1); /* pop versioned variable name */ +} + +/* }================================================================== */ + + +/* +** return registry.CLIBS[path] +*/ +static void *checkclib (lua_State *L, const char *path) { + void *plib; + lua_rawgetp(L, LUA_REGISTRYINDEX, &CLIBS); + lua_getfield(L, -1, path); + plib = lua_touserdata(L, -1); /* plib = CLIBS[path] */ + lua_pop(L, 2); /* pop CLIBS table and 'plib' */ + return plib; +} + + +/* +** registry.CLIBS[path] = plib -- for queries +** registry.CLIBS[#CLIBS + 1] = plib -- also keep a list of all libraries +*/ +static void addtoclib (lua_State *L, const char *path, void *plib) { + lua_rawgetp(L, LUA_REGISTRYINDEX, &CLIBS); + lua_pushlightuserdata(L, plib); + lua_pushvalue(L, -1); + lua_setfield(L, -3, path); /* CLIBS[path] = plib */ + lua_rawseti(L, -2, luaL_len(L, -2) + 1); /* CLIBS[#CLIBS + 1] = plib */ + lua_pop(L, 1); /* pop CLIBS table */ +} + + +/* +** __gc tag method for CLIBS table: calls 'lsys_unloadlib' for all lib +** handles in list CLIBS +*/ +static int gctm (lua_State *L) { + lua_Integer n = luaL_len(L, 1); + for (; n >= 1; n--) { /* for each handle, in reverse order */ + lua_rawgeti(L, 1, n); /* get handle CLIBS[n] */ + lsys_unloadlib(lua_touserdata(L, -1)); + lua_pop(L, 1); /* pop handle */ + } + return 0; +} + + + +/* error codes for 'lookforfunc' */ +#define ERRLIB 1 +#define ERRFUNC 2 + +/* +** Look for a C function named 'sym' in a dynamically loaded library +** 'path'. +** First, check whether the library is already loaded; if not, try +** to load it. +** Then, if 'sym' is '*', return true (as library has been loaded). +** Otherwise, look for symbol 'sym' in the library and push a +** C function with that symbol. +** Return 0 and 'true' or a function in the stack; in case of +** errors, return an error code and an error message in the stack. +*/ +static int lookforfunc (lua_State *L, const char *path, const char *sym) { + void *reg = checkclib(L, path); /* check loaded C libraries */ + if (reg == NULL) { /* must load library? */ + reg = lsys_load(L, path, *sym == '*'); /* global symbols if 'sym'=='*' */ + if (reg == NULL) return ERRLIB; /* unable to load library */ + addtoclib(L, path, reg); + } + if (*sym == '*') { /* loading only library (no function)? */ + lua_pushboolean(L, 1); /* return 'true' */ + return 0; /* no errors */ + } + else { + lua_CFunction f = lsys_sym(L, reg, sym); + if (f == NULL) + return ERRFUNC; /* unable to find function */ + lua_pushcfunction(L, f); /* else create new function */ + return 0; /* no errors */ + } +} + + +static int ll_loadlib (lua_State *L) { + const char *path = luaL_checkstring(L, 1); + const char *init = luaL_checkstring(L, 2); + int stat = lookforfunc(L, path, init); + if (stat == 0) /* no errors? */ + return 1; /* return the loaded function */ + else { /* error; error message is on stack top */ + lua_pushnil(L); + lua_insert(L, -2); + lua_pushstring(L, (stat == ERRLIB) ? LIB_FAIL : "init"); + return 3; /* return nil, error message, and where */ + } +} + + + +/* +** {====================================================== +** 'require' function +** ======================================================= +*/ + + +static int readable (const char *filename) { + FILE *f = fopen(filename, "r"); /* try to open file */ + if (f == NULL) return 0; /* open failed */ + fclose(f); + return 1; +} + + +static const char *pushnexttemplate (lua_State *L, const char *path) { + const char *l; + while (*path == *LUA_PATH_SEP) path++; /* skip separators */ + if (*path == '\0') return NULL; /* no more templates */ + l = strchr(path, *LUA_PATH_SEP); /* find next separator */ + if (l == NULL) l = path + strlen(path); + lua_pushlstring(L, path, l - path); /* template */ + return l; +} + + +static const char *searchpath (lua_State *L, const char *name, + const char *path, + const char *sep, + const char *dirsep) { + luaL_Buffer msg; /* to build error message */ + luaL_buffinit(L, &msg); + if (*sep != '\0') /* non-empty separator? */ + name = luaL_gsub(L, name, sep, dirsep); /* replace it by 'dirsep' */ + while ((path = pushnexttemplate(L, path)) != NULL) { + const char *filename = luaL_gsub(L, lua_tostring(L, -1), + LUA_PATH_MARK, name); + lua_remove(L, -2); /* remove path template */ + if (readable(filename)) /* does file exist and is readable? */ + return filename; /* return that file name */ + lua_pushfstring(L, "\n\tno file '%s'", filename); + lua_remove(L, -2); /* remove file name */ + luaL_addvalue(&msg); /* concatenate error msg. entry */ + } + luaL_pushresult(&msg); /* create error message */ + return NULL; /* not found */ +} + + +static int ll_searchpath (lua_State *L) { + const char *f = searchpath(L, luaL_checkstring(L, 1), + luaL_checkstring(L, 2), + luaL_optstring(L, 3, "."), + luaL_optstring(L, 4, LUA_DIRSEP)); + if (f != NULL) return 1; + else { /* error message is on top of the stack */ + lua_pushnil(L); + lua_insert(L, -2); + return 2; /* return nil + error message */ + } +} + + +static const char *findfile (lua_State *L, const char *name, + const char *pname, + const char *dirsep) { + const char *path; + lua_getfield(L, lua_upvalueindex(1), pname); + path = lua_tostring(L, -1); + if (path == NULL) + luaL_error(L, "'package.%s' must be a string", pname); + return searchpath(L, name, path, ".", dirsep); +} + + +static int checkload (lua_State *L, int stat, const char *filename) { + if (stat) { /* module loaded successfully? */ + lua_pushstring(L, filename); /* will be 2nd argument to module */ + return 2; /* return open function and file name */ + } + else + return luaL_error(L, "error loading module '%s' from file '%s':\n\t%s", + lua_tostring(L, 1), filename, lua_tostring(L, -1)); +} + + +static int searcher_Lua (lua_State *L) { + const char *filename; + const char *name = luaL_checkstring(L, 1); + filename = findfile(L, name, "path", LUA_LSUBSEP); + if (filename == NULL) return 1; /* module not found in this path */ + return checkload(L, (luaL_loadfile(L, filename) == LUA_OK), filename); +} + + +/* +** Try to find a load function for module 'modname' at file 'filename'. +** First, change '.' to '_' in 'modname'; then, if 'modname' has +** the form X-Y (that is, it has an "ignore mark"), build a function +** name "luaopen_X" and look for it. (For compatibility, if that +** fails, it also tries "luaopen_Y".) If there is no ignore mark, +** look for a function named "luaopen_modname". +*/ +static int loadfunc (lua_State *L, const char *filename, const char *modname) { + const char *openfunc; + const char *mark; + modname = luaL_gsub(L, modname, ".", LUA_OFSEP); + mark = strchr(modname, *LUA_IGMARK); + if (mark) { + int stat; + openfunc = lua_pushlstring(L, modname, mark - modname); + openfunc = lua_pushfstring(L, LUA_POF"%s", openfunc); + stat = lookforfunc(L, filename, openfunc); + if (stat != ERRFUNC) return stat; + modname = mark + 1; /* else go ahead and try old-style name */ + } + openfunc = lua_pushfstring(L, LUA_POF"%s", modname); + return lookforfunc(L, filename, openfunc); +} + + +static int searcher_C (lua_State *L) { + const char *name = luaL_checkstring(L, 1); + const char *filename = findfile(L, name, "cpath", LUA_CSUBSEP); + if (filename == NULL) return 1; /* module not found in this path */ + return checkload(L, (loadfunc(L, filename, name) == 0), filename); +} + + +static int searcher_Croot (lua_State *L) { + const char *filename; + const char *name = luaL_checkstring(L, 1); + const char *p = strchr(name, '.'); + int stat; + if (p == NULL) return 0; /* is root */ + lua_pushlstring(L, name, p - name); + filename = findfile(L, lua_tostring(L, -1), "cpath", LUA_CSUBSEP); + if (filename == NULL) return 1; /* root not found */ + if ((stat = loadfunc(L, filename, name)) != 0) { + if (stat != ERRFUNC) + return checkload(L, 0, filename); /* real error */ + else { /* open function not found */ + lua_pushfstring(L, "\n\tno module '%s' in file '%s'", name, filename); + return 1; + } + } + lua_pushstring(L, filename); /* will be 2nd argument to module */ + return 2; +} + + +static int searcher_preload (lua_State *L) { + const char *name = luaL_checkstring(L, 1); + lua_getfield(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE); + if (lua_getfield(L, -1, name) == LUA_TNIL) /* not found? */ + lua_pushfstring(L, "\n\tno field package.preload['%s']", name); + return 1; +} + + +static void findloader (lua_State *L, const char *name) { + int i; + luaL_Buffer msg; /* to build error message */ + luaL_buffinit(L, &msg); + /* push 'package.searchers' to index 3 in the stack */ + if (lua_getfield(L, lua_upvalueindex(1), "searchers") != LUA_TTABLE) + luaL_error(L, "'package.searchers' must be a table"); + /* iterate over available searchers to find a loader */ + for (i = 1; ; i++) { + if (lua_rawgeti(L, 3, i) == LUA_TNIL) { /* no more searchers? */ + lua_pop(L, 1); /* remove nil */ + luaL_pushresult(&msg); /* create error message */ + luaL_error(L, "module '%s' not found:%s", name, lua_tostring(L, -1)); + } + lua_pushstring(L, name); + lua_call(L, 1, 2); /* call it */ + if (lua_isfunction(L, -2)) /* did it find a loader? */ + return; /* module loader found */ + else if (lua_isstring(L, -2)) { /* searcher returned error message? */ + lua_pop(L, 1); /* remove extra return */ + luaL_addvalue(&msg); /* concatenate error message */ + } + else + lua_pop(L, 2); /* remove both returns */ + } +} + + +static int ll_require (lua_State *L) { + const char *name = luaL_checkstring(L, 1); + lua_settop(L, 1); /* LOADED table will be at index 2 */ + lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + lua_getfield(L, 2, name); /* LOADED[name] */ + if (lua_toboolean(L, -1)) /* is it there? */ + return 1; /* package is already loaded */ + /* else must load package */ + lua_pop(L, 1); /* remove 'getfield' result */ + findloader(L, name); + lua_pushstring(L, name); /* pass name as argument to module loader */ + lua_insert(L, -2); /* name is 1st argument (before search data) */ + lua_call(L, 2, 1); /* run loader to load module */ + if (!lua_isnil(L, -1)) /* non-nil return? */ + lua_setfield(L, 2, name); /* LOADED[name] = returned value */ + if (lua_getfield(L, 2, name) == LUA_TNIL) { /* module set no value? */ + lua_pushboolean(L, 1); /* use true as result */ + lua_pushvalue(L, -1); /* extra copy to be returned */ + lua_setfield(L, 2, name); /* LOADED[name] = true */ + } + return 1; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** 'module' function +** ======================================================= +*/ +#if defined(LUA_COMPAT_MODULE) + +/* +** changes the environment variable of calling function +*/ +static void set_env (lua_State *L) { + lua_Debug ar; + if (lua_getstack(L, 1, &ar) == 0 || + lua_getinfo(L, "f", &ar) == 0 || /* get calling function */ + lua_iscfunction(L, -1)) + luaL_error(L, "'module' not called from a Lua function"); + lua_pushvalue(L, -2); /* copy new environment table to top */ + lua_setupvalue(L, -2, 1); + lua_pop(L, 1); /* remove function */ +} + + +static void dooptions (lua_State *L, int n) { + int i; + for (i = 2; i <= n; i++) { + if (lua_isfunction(L, i)) { /* avoid 'calling' extra info. */ + lua_pushvalue(L, i); /* get option (a function) */ + lua_pushvalue(L, -2); /* module */ + lua_call(L, 1, 0); + } + } +} + + +static void modinit (lua_State *L, const char *modname) { + const char *dot; + lua_pushvalue(L, -1); + lua_setfield(L, -2, "_M"); /* module._M = module */ + lua_pushstring(L, modname); + lua_setfield(L, -2, "_NAME"); + dot = strrchr(modname, '.'); /* look for last dot in module name */ + if (dot == NULL) dot = modname; + else dot++; + /* set _PACKAGE as package name (full module name minus last part) */ + lua_pushlstring(L, modname, dot - modname); + lua_setfield(L, -2, "_PACKAGE"); +} + + +static int ll_module (lua_State *L) { + const char *modname = luaL_checkstring(L, 1); + int lastarg = lua_gettop(L); /* last parameter */ + luaL_pushmodule(L, modname, 1); /* get/create module table */ + /* check whether table already has a _NAME field */ + if (lua_getfield(L, -1, "_NAME") != LUA_TNIL) + lua_pop(L, 1); /* table is an initialized module */ + else { /* no; initialize it */ + lua_pop(L, 1); + modinit(L, modname); + } + lua_pushvalue(L, -1); + set_env(L); + dooptions(L, lastarg); + return 1; +} + + +static int ll_seeall (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (!lua_getmetatable(L, 1)) { + lua_createtable(L, 0, 1); /* create new metatable */ + lua_pushvalue(L, -1); + lua_setmetatable(L, 1); + } + lua_pushglobaltable(L); + lua_setfield(L, -2, "__index"); /* mt.__index = _G */ + return 0; +} + +#endif +/* }====================================================== */ + + + +static const luaL_Reg pk_funcs[] = { + {"loadlib", ll_loadlib}, + {"searchpath", ll_searchpath}, +#if defined(LUA_COMPAT_MODULE) + {"seeall", ll_seeall}, +#endif + /* placeholders */ + {"preload", NULL}, + {"cpath", NULL}, + {"path", NULL}, + {"searchers", NULL}, + {"loaded", NULL}, + {NULL, NULL} +}; + + +static const luaL_Reg ll_funcs[] = { +#if defined(LUA_COMPAT_MODULE) + {"module", ll_module}, +#endif + {"require", ll_require}, + {NULL, NULL} +}; + + +static void createsearcherstable (lua_State *L) { + static const lua_CFunction searchers[] = + {searcher_preload, searcher_Lua, searcher_C, searcher_Croot, NULL}; + int i; + /* create 'searchers' table */ + lua_createtable(L, sizeof(searchers)/sizeof(searchers[0]) - 1, 0); + /* fill it with predefined searchers */ + for (i=0; searchers[i] != NULL; i++) { + lua_pushvalue(L, -2); /* set 'package' as upvalue for all searchers */ + lua_pushcclosure(L, searchers[i], 1); + lua_rawseti(L, -2, i+1); + } +#if defined(LUA_COMPAT_LOADERS) + lua_pushvalue(L, -1); /* make a copy of 'searchers' table */ + lua_setfield(L, -3, "loaders"); /* put it in field 'loaders' */ +#endif + lua_setfield(L, -2, "searchers"); /* put it in field 'searchers' */ +} + + +/* +** create table CLIBS to keep track of loaded C libraries, +** setting a finalizer to close all libraries when closing state. +*/ +static void createclibstable (lua_State *L) { + lua_newtable(L); /* create CLIBS table */ + lua_createtable(L, 0, 1); /* create metatable for CLIBS */ + lua_pushcfunction(L, gctm); + lua_setfield(L, -2, "__gc"); /* set finalizer for CLIBS table */ + lua_setmetatable(L, -2); + lua_rawsetp(L, LUA_REGISTRYINDEX, &CLIBS); /* set CLIBS table in registry */ +} + + +LUAMOD_API int luaopen_package (lua_State *L) { + createclibstable(L); + luaL_newlib(L, pk_funcs); /* create 'package' table */ + createsearcherstable(L); + /* set paths */ + setpath(L, "path", LUA_PATH_VAR, LUA_PATH_DEFAULT); + setpath(L, "cpath", LUA_CPATH_VAR, LUA_CPATH_DEFAULT); + /* store config information */ + lua_pushliteral(L, LUA_DIRSEP "\n" LUA_PATH_SEP "\n" LUA_PATH_MARK "\n" + LUA_EXEC_DIR "\n" LUA_IGMARK "\n"); + lua_setfield(L, -2, "config"); + /* set field 'loaded' */ + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + lua_setfield(L, -2, "loaded"); + /* set field 'preload' */ + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE); + lua_setfield(L, -2, "preload"); + lua_pushglobaltable(L); + lua_pushvalue(L, -2); /* set 'package' as upvalue for next lib */ + luaL_setfuncs(L, ll_funcs, 1); /* open lib into global table */ + lua_pop(L, 1); /* pop global table */ + return 1; /* return 'package' table */ +} + diff --git a/src/rcheevos/test/lua/src/lobject.c b/src/rcheevos/test/lua/src/lobject.c new file mode 100644 index 000000000..2da76899a --- /dev/null +++ b/src/rcheevos/test/lua/src/lobject.c @@ -0,0 +1,521 @@ +/* +** $Id: lobject.c,v 2.113 2016/12/22 13:08:50 roberto Exp $ +** Some generic functions over Lua objects +** See Copyright Notice in lua.h +*/ + +#define lobject_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lctype.h" +#include "ldebug.h" +#include "ldo.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "lvm.h" + + + +LUAI_DDEF const TValue luaO_nilobject_ = {NILCONSTANT}; + + +/* +** converts an integer to a "floating point byte", represented as +** (eeeeexxx), where the real value is (1xxx) * 2^(eeeee - 1) if +** eeeee != 0 and (xxx) otherwise. +*/ +int luaO_int2fb (unsigned int x) { + int e = 0; /* exponent */ + if (x < 8) return x; + while (x >= (8 << 4)) { /* coarse steps */ + x = (x + 0xf) >> 4; /* x = ceil(x / 16) */ + e += 4; + } + while (x >= (8 << 1)) { /* fine steps */ + x = (x + 1) >> 1; /* x = ceil(x / 2) */ + e++; + } + return ((e+1) << 3) | (cast_int(x) - 8); +} + + +/* converts back */ +int luaO_fb2int (int x) { + return (x < 8) ? x : ((x & 7) + 8) << ((x >> 3) - 1); +} + + +/* +** Computes ceil(log2(x)) +*/ +int luaO_ceillog2 (unsigned int x) { + static const lu_byte log_2[256] = { /* log_2[i] = ceil(log2(i - 1)) */ + 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8 + }; + int l = 0; + x--; + while (x >= 256) { l += 8; x >>= 8; } + return l + log_2[x]; +} + + +static lua_Integer intarith (lua_State *L, int op, lua_Integer v1, + lua_Integer v2) { + switch (op) { + case LUA_OPADD: return intop(+, v1, v2); + case LUA_OPSUB:return intop(-, v1, v2); + case LUA_OPMUL:return intop(*, v1, v2); + case LUA_OPMOD: return luaV_mod(L, v1, v2); + case LUA_OPIDIV: return luaV_div(L, v1, v2); + case LUA_OPBAND: return intop(&, v1, v2); + case LUA_OPBOR: return intop(|, v1, v2); + case LUA_OPBXOR: return intop(^, v1, v2); + case LUA_OPSHL: return luaV_shiftl(v1, v2); + case LUA_OPSHR: return luaV_shiftl(v1, -v2); + case LUA_OPUNM: return intop(-, 0, v1); + case LUA_OPBNOT: return intop(^, ~l_castS2U(0), v1); + default: lua_assert(0); return 0; + } +} + + +static lua_Number numarith (lua_State *L, int op, lua_Number v1, + lua_Number v2) { + switch (op) { + case LUA_OPADD: return luai_numadd(L, v1, v2); + case LUA_OPSUB: return luai_numsub(L, v1, v2); + case LUA_OPMUL: return luai_nummul(L, v1, v2); + case LUA_OPDIV: return luai_numdiv(L, v1, v2); + case LUA_OPPOW: return luai_numpow(L, v1, v2); + case LUA_OPIDIV: return luai_numidiv(L, v1, v2); + case LUA_OPUNM: return luai_numunm(L, v1); + case LUA_OPMOD: { + lua_Number m; + luai_nummod(L, v1, v2, m); + return m; + } + default: lua_assert(0); return 0; + } +} + + +void luaO_arith (lua_State *L, int op, const TValue *p1, const TValue *p2, + TValue *res) { + switch (op) { + case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR: + case LUA_OPSHL: case LUA_OPSHR: + case LUA_OPBNOT: { /* operate only on integers */ + lua_Integer i1; lua_Integer i2; + if (tointeger(p1, &i1) && tointeger(p2, &i2)) { + setivalue(res, intarith(L, op, i1, i2)); + return; + } + else break; /* go to the end */ + } + case LUA_OPDIV: case LUA_OPPOW: { /* operate only on floats */ + lua_Number n1; lua_Number n2; + if (tonumber(p1, &n1) && tonumber(p2, &n2)) { + setfltvalue(res, numarith(L, op, n1, n2)); + return; + } + else break; /* go to the end */ + } + default: { /* other operations */ + lua_Number n1; lua_Number n2; + if (ttisinteger(p1) && ttisinteger(p2)) { + setivalue(res, intarith(L, op, ivalue(p1), ivalue(p2))); + return; + } + else if (tonumber(p1, &n1) && tonumber(p2, &n2)) { + setfltvalue(res, numarith(L, op, n1, n2)); + return; + } + else break; /* go to the end */ + } + } + /* could not perform raw operation; try metamethod */ + lua_assert(L != NULL); /* should not fail when folding (compile time) */ + luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD)); +} + + +int luaO_hexavalue (int c) { + if (lisdigit(c)) return c - '0'; + else return (ltolower(c) - 'a') + 10; +} + + +static int isneg (const char **s) { + if (**s == '-') { (*s)++; return 1; } + else if (**s == '+') (*s)++; + return 0; +} + + + +/* +** {================================================================== +** Lua's implementation for 'lua_strx2number' +** =================================================================== +*/ + +#if !defined(lua_strx2number) + +/* maximum number of significant digits to read (to avoid overflows + even with single floats) */ +#define MAXSIGDIG 30 + +/* +** convert an hexadecimal numeric string to a number, following +** C99 specification for 'strtod' +*/ +static lua_Number lua_strx2number (const char *s, char **endptr) { + int dot = lua_getlocaledecpoint(); + lua_Number r = 0.0; /* result (accumulator) */ + int sigdig = 0; /* number of significant digits */ + int nosigdig = 0; /* number of non-significant digits */ + int e = 0; /* exponent correction */ + int neg; /* 1 if number is negative */ + int hasdot = 0; /* true after seen a dot */ + *endptr = cast(char *, s); /* nothing is valid yet */ + while (lisspace(cast_uchar(*s))) s++; /* skip initial spaces */ + neg = isneg(&s); /* check signal */ + if (!(*s == '0' && (*(s + 1) == 'x' || *(s + 1) == 'X'))) /* check '0x' */ + return 0.0; /* invalid format (no '0x') */ + for (s += 2; ; s++) { /* skip '0x' and read numeral */ + if (*s == dot) { + if (hasdot) break; /* second dot? stop loop */ + else hasdot = 1; + } + else if (lisxdigit(cast_uchar(*s))) { + if (sigdig == 0 && *s == '0') /* non-significant digit (zero)? */ + nosigdig++; + else if (++sigdig <= MAXSIGDIG) /* can read it without overflow? */ + r = (r * cast_num(16.0)) + luaO_hexavalue(*s); + else e++; /* too many digits; ignore, but still count for exponent */ + if (hasdot) e--; /* decimal digit? correct exponent */ + } + else break; /* neither a dot nor a digit */ + } + if (nosigdig + sigdig == 0) /* no digits? */ + return 0.0; /* invalid format */ + *endptr = cast(char *, s); /* valid up to here */ + e *= 4; /* each digit multiplies/divides value by 2^4 */ + if (*s == 'p' || *s == 'P') { /* exponent part? */ + int exp1 = 0; /* exponent value */ + int neg1; /* exponent signal */ + s++; /* skip 'p' */ + neg1 = isneg(&s); /* signal */ + if (!lisdigit(cast_uchar(*s))) + return 0.0; /* invalid; must have at least one digit */ + while (lisdigit(cast_uchar(*s))) /* read exponent */ + exp1 = exp1 * 10 + *(s++) - '0'; + if (neg1) exp1 = -exp1; + e += exp1; + *endptr = cast(char *, s); /* valid up to here */ + } + if (neg) r = -r; + return l_mathop(ldexp)(r, e); +} + +#endif +/* }====================================================== */ + + +/* maximum length of a numeral */ +#if !defined (L_MAXLENNUM) +#define L_MAXLENNUM 200 +#endif + +static const char *l_str2dloc (const char *s, lua_Number *result, int mode) { + char *endptr; + *result = (mode == 'x') ? lua_strx2number(s, &endptr) /* try to convert */ + : lua_str2number(s, &endptr); + if (endptr == s) return NULL; /* nothing recognized? */ + while (lisspace(cast_uchar(*endptr))) endptr++; /* skip trailing spaces */ + return (*endptr == '\0') ? endptr : NULL; /* OK if no trailing characters */ +} + + +/* +** Convert string 's' to a Lua number (put in 'result'). Return NULL +** on fail or the address of the ending '\0' on success. +** 'pmode' points to (and 'mode' contains) special things in the string: +** - 'x'/'X' means an hexadecimal numeral +** - 'n'/'N' means 'inf' or 'nan' (which should be rejected) +** - '.' just optimizes the search for the common case (nothing special) +** This function accepts both the current locale or a dot as the radix +** mark. If the convertion fails, it may mean number has a dot but +** locale accepts something else. In that case, the code copies 's' +** to a buffer (because 's' is read-only), changes the dot to the +** current locale radix mark, and tries to convert again. +*/ +static const char *l_str2d (const char *s, lua_Number *result) { + const char *endptr; + const char *pmode = strpbrk(s, ".xXnN"); + int mode = pmode ? ltolower(cast_uchar(*pmode)) : 0; + if (mode == 'n') /* reject 'inf' and 'nan' */ + return NULL; + endptr = l_str2dloc(s, result, mode); /* try to convert */ + if (endptr == NULL) { /* failed? may be a different locale */ + char buff[L_MAXLENNUM + 1]; + const char *pdot = strchr(s, '.'); + if (strlen(s) > L_MAXLENNUM || pdot == NULL) + return NULL; /* string too long or no dot; fail */ + strcpy(buff, s); /* copy string to buffer */ + buff[pdot - s] = lua_getlocaledecpoint(); /* correct decimal point */ + endptr = l_str2dloc(buff, result, mode); /* try again */ + if (endptr != NULL) + endptr = s + (endptr - buff); /* make relative to 's' */ + } + return endptr; +} + + +#define MAXBY10 cast(lua_Unsigned, LUA_MAXINTEGER / 10) +#define MAXLASTD cast_int(LUA_MAXINTEGER % 10) + +static const char *l_str2int (const char *s, lua_Integer *result) { + lua_Unsigned a = 0; + int empty = 1; + int neg; + while (lisspace(cast_uchar(*s))) s++; /* skip initial spaces */ + neg = isneg(&s); + if (s[0] == '0' && + (s[1] == 'x' || s[1] == 'X')) { /* hex? */ + s += 2; /* skip '0x' */ + for (; lisxdigit(cast_uchar(*s)); s++) { + a = a * 16 + luaO_hexavalue(*s); + empty = 0; + } + } + else { /* decimal */ + for (; lisdigit(cast_uchar(*s)); s++) { + int d = *s - '0'; + if (a >= MAXBY10 && (a > MAXBY10 || d > MAXLASTD + neg)) /* overflow? */ + return NULL; /* do not accept it (as integer) */ + a = a * 10 + d; + empty = 0; + } + } + while (lisspace(cast_uchar(*s))) s++; /* skip trailing spaces */ + if (empty || *s != '\0') return NULL; /* something wrong in the numeral */ + else { + *result = l_castU2S((neg) ? 0u - a : a); + return s; + } +} + + +size_t luaO_str2num (const char *s, TValue *o) { + lua_Integer i; lua_Number n; + const char *e; + if ((e = l_str2int(s, &i)) != NULL) { /* try as an integer */ + setivalue(o, i); + } + else if ((e = l_str2d(s, &n)) != NULL) { /* else try as a float */ + setfltvalue(o, n); + } + else + return 0; /* conversion failed */ + return (e - s) + 1; /* success; return string size */ +} + + +int luaO_utf8esc (char *buff, unsigned long x) { + int n = 1; /* number of bytes put in buffer (backwards) */ + lua_assert(x <= 0x10FFFF); + if (x < 0x80) /* ascii? */ + buff[UTF8BUFFSZ - 1] = cast(char, x); + else { /* need continuation bytes */ + unsigned int mfb = 0x3f; /* maximum that fits in first byte */ + do { /* add continuation bytes */ + buff[UTF8BUFFSZ - (n++)] = cast(char, 0x80 | (x & 0x3f)); + x >>= 6; /* remove added bits */ + mfb >>= 1; /* now there is one less bit available in first byte */ + } while (x > mfb); /* still needs continuation byte? */ + buff[UTF8BUFFSZ - n] = cast(char, (~mfb << 1) | x); /* add first byte */ + } + return n; +} + + +/* maximum length of the conversion of a number to a string */ +#define MAXNUMBER2STR 50 + + +/* +** Convert a number object to a string +*/ +void luaO_tostring (lua_State *L, StkId obj) { + char buff[MAXNUMBER2STR]; + size_t len; + lua_assert(ttisnumber(obj)); + if (ttisinteger(obj)) + len = lua_integer2str(buff, sizeof(buff), ivalue(obj)); + else { + len = lua_number2str(buff, sizeof(buff), fltvalue(obj)); +#if !defined(LUA_COMPAT_FLOATSTRING) + if (buff[strspn(buff, "-0123456789")] == '\0') { /* looks like an int? */ + buff[len++] = lua_getlocaledecpoint(); + buff[len++] = '0'; /* adds '.0' to result */ + } +#endif + } + setsvalue2s(L, obj, luaS_newlstr(L, buff, len)); +} + + +static void pushstr (lua_State *L, const char *str, size_t l) { + setsvalue2s(L, L->top, luaS_newlstr(L, str, l)); + luaD_inctop(L); +} + + +/* +** this function handles only '%d', '%c', '%f', '%p', and '%s' + conventional formats, plus Lua-specific '%I' and '%U' +*/ +const char *luaO_pushvfstring (lua_State *L, const char *fmt, va_list argp) { + int n = 0; + for (;;) { + const char *e = strchr(fmt, '%'); + if (e == NULL) break; + pushstr(L, fmt, e - fmt); + switch (*(e+1)) { + case 's': { /* zero-terminated string */ + const char *s = va_arg(argp, char *); + if (s == NULL) s = "(null)"; + pushstr(L, s, strlen(s)); + break; + } + case 'c': { /* an 'int' as a character */ + char buff = cast(char, va_arg(argp, int)); + if (lisprint(cast_uchar(buff))) + pushstr(L, &buff, 1); + else /* non-printable character; print its code */ + luaO_pushfstring(L, "<\\%d>", cast_uchar(buff)); + break; + } + case 'd': { /* an 'int' */ + setivalue(L->top, va_arg(argp, int)); + goto top2str; + } + case 'I': { /* a 'lua_Integer' */ + setivalue(L->top, cast(lua_Integer, va_arg(argp, l_uacInt))); + goto top2str; + } + case 'f': { /* a 'lua_Number' */ + setfltvalue(L->top, cast_num(va_arg(argp, l_uacNumber))); + top2str: /* convert the top element to a string */ + luaD_inctop(L); + luaO_tostring(L, L->top - 1); + break; + } + case 'p': { /* a pointer */ + char buff[4*sizeof(void *) + 8]; /* should be enough space for a '%p' */ + int l = l_sprintf(buff, sizeof(buff), "%p", va_arg(argp, void *)); + pushstr(L, buff, l); + break; + } + case 'U': { /* an 'int' as a UTF-8 sequence */ + char buff[UTF8BUFFSZ]; + int l = luaO_utf8esc(buff, cast(long, va_arg(argp, long))); + pushstr(L, buff + UTF8BUFFSZ - l, l); + break; + } + case '%': { + pushstr(L, "%", 1); + break; + } + default: { + luaG_runerror(L, "invalid option '%%%c' to 'lua_pushfstring'", + *(e + 1)); + } + } + n += 2; + fmt = e+2; + } + luaD_checkstack(L, 1); + pushstr(L, fmt, strlen(fmt)); + if (n > 0) luaV_concat(L, n + 1); + return svalue(L->top - 1); +} + + +const char *luaO_pushfstring (lua_State *L, const char *fmt, ...) { + const char *msg; + va_list argp; + va_start(argp, fmt); + msg = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + return msg; +} + + +/* number of chars of a literal string without the ending \0 */ +#define LL(x) (sizeof(x)/sizeof(char) - 1) + +#define RETS "..." +#define PRE "[string \"" +#define POS "\"]" + +#define addstr(a,b,l) ( memcpy(a,b,(l) * sizeof(char)), a += (l) ) + +void luaO_chunkid (char *out, const char *source, size_t bufflen) { + size_t l = strlen(source); + if (*source == '=') { /* 'literal' source */ + if (l <= bufflen) /* small enough? */ + memcpy(out, source + 1, l * sizeof(char)); + else { /* truncate it */ + addstr(out, source + 1, bufflen - 1); + *out = '\0'; + } + } + else if (*source == '@') { /* file name */ + if (l <= bufflen) /* small enough? */ + memcpy(out, source + 1, l * sizeof(char)); + else { /* add '...' before rest of name */ + addstr(out, RETS, LL(RETS)); + bufflen -= LL(RETS); + memcpy(out, source + 1 + l - bufflen, bufflen * sizeof(char)); + } + } + else { /* string; format as [string "source"] */ + const char *nl = strchr(source, '\n'); /* find first new line (if any) */ + addstr(out, PRE, LL(PRE)); /* add prefix */ + bufflen -= LL(PRE RETS POS) + 1; /* save space for prefix+suffix+'\0' */ + if (l < bufflen && nl == NULL) { /* small one-line source? */ + addstr(out, source, l); /* keep it */ + } + else { + if (nl != NULL) l = nl - source; /* stop at first newline */ + if (l > bufflen) l = bufflen; + addstr(out, source, l); + addstr(out, RETS, LL(RETS)); + } + memcpy(out, POS, (LL(POS) + 1) * sizeof(char)); + } +} + diff --git a/src/rcheevos/test/lua/src/lobject.h b/src/rcheevos/test/lua/src/lobject.h new file mode 100644 index 000000000..3c0422894 --- /dev/null +++ b/src/rcheevos/test/lua/src/lobject.h @@ -0,0 +1,549 @@ +/* +** $Id: lobject.h,v 2.117 2016/08/01 19:51:24 roberto Exp $ +** Type definitions for Lua objects +** See Copyright Notice in lua.h +*/ + + +#ifndef lobject_h +#define lobject_h + + +#include + + +#include "llimits.h" +#include "lua.h" + + +/* +** Extra tags for non-values +*/ +#define LUA_TPROTO LUA_NUMTAGS /* function prototypes */ +#define LUA_TDEADKEY (LUA_NUMTAGS+1) /* removed keys in tables */ + +/* +** number of all possible tags (including LUA_TNONE but excluding DEADKEY) +*/ +#define LUA_TOTALTAGS (LUA_TPROTO + 2) + + +/* +** tags for Tagged Values have the following use of bits: +** bits 0-3: actual tag (a LUA_T* value) +** bits 4-5: variant bits +** bit 6: whether value is collectable +*/ + + +/* +** LUA_TFUNCTION variants: +** 0 - Lua function +** 1 - light C function +** 2 - regular C function (closure) +*/ + +/* Variant tags for functions */ +#define LUA_TLCL (LUA_TFUNCTION | (0 << 4)) /* Lua closure */ +#define LUA_TLCF (LUA_TFUNCTION | (1 << 4)) /* light C function */ +#define LUA_TCCL (LUA_TFUNCTION | (2 << 4)) /* C closure */ + + +/* Variant tags for strings */ +#define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) /* short strings */ +#define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) /* long strings */ + + +/* Variant tags for numbers */ +#define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) /* float numbers */ +#define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) /* integer numbers */ + + +/* Bit mark for collectable types */ +#define BIT_ISCOLLECTABLE (1 << 6) + +/* mark a tag as collectable */ +#define ctb(t) ((t) | BIT_ISCOLLECTABLE) + + +/* +** Common type for all collectable objects +*/ +typedef struct GCObject GCObject; + + +/* +** Common Header for all collectable objects (in macro form, to be +** included in other objects) +*/ +#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked + + +/* +** Common type has only the common header +*/ +struct GCObject { + CommonHeader; +}; + + + + +/* +** Tagged Values. This is the basic representation of values in Lua, +** an actual value plus a tag with its type. +*/ + +/* +** Union of all Lua values +*/ +typedef union Value { + GCObject *gc; /* collectable objects */ + void *p; /* light userdata */ + int b; /* booleans */ + lua_CFunction f; /* light C functions */ + lua_Integer i; /* integer numbers */ + lua_Number n; /* float numbers */ +} Value; + + +#define TValuefields Value value_; int tt_ + + +typedef struct lua_TValue { + TValuefields; +} TValue; + + + +/* macro defining a nil value */ +#define NILCONSTANT {NULL}, LUA_TNIL + + +#define val_(o) ((o)->value_) + + +/* raw type tag of a TValue */ +#define rttype(o) ((o)->tt_) + +/* tag with no variants (bits 0-3) */ +#define novariant(x) ((x) & 0x0F) + +/* type tag of a TValue (bits 0-3 for tags + variant bits 4-5) */ +#define ttype(o) (rttype(o) & 0x3F) + +/* type tag of a TValue with no variants (bits 0-3) */ +#define ttnov(o) (novariant(rttype(o))) + + +/* Macros to test type */ +#define checktag(o,t) (rttype(o) == (t)) +#define checktype(o,t) (ttnov(o) == (t)) +#define ttisnumber(o) checktype((o), LUA_TNUMBER) +#define ttisfloat(o) checktag((o), LUA_TNUMFLT) +#define ttisinteger(o) checktag((o), LUA_TNUMINT) +#define ttisnil(o) checktag((o), LUA_TNIL) +#define ttisboolean(o) checktag((o), LUA_TBOOLEAN) +#define ttislightuserdata(o) checktag((o), LUA_TLIGHTUSERDATA) +#define ttisstring(o) checktype((o), LUA_TSTRING) +#define ttisshrstring(o) checktag((o), ctb(LUA_TSHRSTR)) +#define ttislngstring(o) checktag((o), ctb(LUA_TLNGSTR)) +#define ttistable(o) checktag((o), ctb(LUA_TTABLE)) +#define ttisfunction(o) checktype(o, LUA_TFUNCTION) +#define ttisclosure(o) ((rttype(o) & 0x1F) == LUA_TFUNCTION) +#define ttisCclosure(o) checktag((o), ctb(LUA_TCCL)) +#define ttisLclosure(o) checktag((o), ctb(LUA_TLCL)) +#define ttislcf(o) checktag((o), LUA_TLCF) +#define ttisfulluserdata(o) checktag((o), ctb(LUA_TUSERDATA)) +#define ttisthread(o) checktag((o), ctb(LUA_TTHREAD)) +#define ttisdeadkey(o) checktag((o), LUA_TDEADKEY) + + +/* Macros to access values */ +#define ivalue(o) check_exp(ttisinteger(o), val_(o).i) +#define fltvalue(o) check_exp(ttisfloat(o), val_(o).n) +#define nvalue(o) check_exp(ttisnumber(o), \ + (ttisinteger(o) ? cast_num(ivalue(o)) : fltvalue(o))) +#define gcvalue(o) check_exp(iscollectable(o), val_(o).gc) +#define pvalue(o) check_exp(ttislightuserdata(o), val_(o).p) +#define tsvalue(o) check_exp(ttisstring(o), gco2ts(val_(o).gc)) +#define uvalue(o) check_exp(ttisfulluserdata(o), gco2u(val_(o).gc)) +#define clvalue(o) check_exp(ttisclosure(o), gco2cl(val_(o).gc)) +#define clLvalue(o) check_exp(ttisLclosure(o), gco2lcl(val_(o).gc)) +#define clCvalue(o) check_exp(ttisCclosure(o), gco2ccl(val_(o).gc)) +#define fvalue(o) check_exp(ttislcf(o), val_(o).f) +#define hvalue(o) check_exp(ttistable(o), gco2t(val_(o).gc)) +#define bvalue(o) check_exp(ttisboolean(o), val_(o).b) +#define thvalue(o) check_exp(ttisthread(o), gco2th(val_(o).gc)) +/* a dead value may get the 'gc' field, but cannot access its contents */ +#define deadvalue(o) check_exp(ttisdeadkey(o), cast(void *, val_(o).gc)) + +#define l_isfalse(o) (ttisnil(o) || (ttisboolean(o) && bvalue(o) == 0)) + + +#define iscollectable(o) (rttype(o) & BIT_ISCOLLECTABLE) + + +/* Macros for internal tests */ +#define righttt(obj) (ttype(obj) == gcvalue(obj)->tt) + +#define checkliveness(L,obj) \ + lua_longassert(!iscollectable(obj) || \ + (righttt(obj) && (L == NULL || !isdead(G(L),gcvalue(obj))))) + + +/* Macros to set values */ +#define settt_(o,t) ((o)->tt_=(t)) + +#define setfltvalue(obj,x) \ + { TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_TNUMFLT); } + +#define chgfltvalue(obj,x) \ + { TValue *io=(obj); lua_assert(ttisfloat(io)); val_(io).n=(x); } + +#define setivalue(obj,x) \ + { TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_TNUMINT); } + +#define chgivalue(obj,x) \ + { TValue *io=(obj); lua_assert(ttisinteger(io)); val_(io).i=(x); } + +#define setnilvalue(obj) settt_(obj, LUA_TNIL) + +#define setfvalue(obj,x) \ + { TValue *io=(obj); val_(io).f=(x); settt_(io, LUA_TLCF); } + +#define setpvalue(obj,x) \ + { TValue *io=(obj); val_(io).p=(x); settt_(io, LUA_TLIGHTUSERDATA); } + +#define setbvalue(obj,x) \ + { TValue *io=(obj); val_(io).b=(x); settt_(io, LUA_TBOOLEAN); } + +#define setgcovalue(L,obj,x) \ + { TValue *io = (obj); GCObject *i_g=(x); \ + val_(io).gc = i_g; settt_(io, ctb(i_g->tt)); } + +#define setsvalue(L,obj,x) \ + { TValue *io = (obj); TString *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(x_->tt)); \ + checkliveness(L,io); } + +#define setuvalue(L,obj,x) \ + { TValue *io = (obj); Udata *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_TUSERDATA)); \ + checkliveness(L,io); } + +#define setthvalue(L,obj,x) \ + { TValue *io = (obj); lua_State *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_TTHREAD)); \ + checkliveness(L,io); } + +#define setclLvalue(L,obj,x) \ + { TValue *io = (obj); LClosure *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_TLCL)); \ + checkliveness(L,io); } + +#define setclCvalue(L,obj,x) \ + { TValue *io = (obj); CClosure *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_TCCL)); \ + checkliveness(L,io); } + +#define sethvalue(L,obj,x) \ + { TValue *io = (obj); Table *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_TTABLE)); \ + checkliveness(L,io); } + +#define setdeadvalue(obj) settt_(obj, LUA_TDEADKEY) + + + +#define setobj(L,obj1,obj2) \ + { TValue *io1=(obj1); *io1 = *(obj2); \ + (void)L; checkliveness(L,io1); } + + +/* +** different types of assignments, according to destination +*/ + +/* from stack to (same) stack */ +#define setobjs2s setobj +/* to stack (not from same stack) */ +#define setobj2s setobj +#define setsvalue2s setsvalue +#define sethvalue2s sethvalue +#define setptvalue2s setptvalue +/* from table to same table */ +#define setobjt2t setobj +/* to new object */ +#define setobj2n setobj +#define setsvalue2n setsvalue + +/* to table (define it as an expression to be used in macros) */ +#define setobj2t(L,o1,o2) ((void)L, *(o1)=*(o2), checkliveness(L,(o1))) + + + + +/* +** {====================================================== +** types and prototypes +** ======================================================= +*/ + + +typedef TValue *StkId; /* index to stack elements */ + + + + +/* +** Header for string value; string bytes follow the end of this structure +** (aligned according to 'UTString'; see next). +*/ +typedef struct TString { + CommonHeader; + lu_byte extra; /* reserved words for short strings; "has hash" for longs */ + lu_byte shrlen; /* length for short strings */ + unsigned int hash; + union { + size_t lnglen; /* length for long strings */ + struct TString *hnext; /* linked list for hash table */ + } u; +} TString; + + +/* +** Ensures that address after this type is always fully aligned. +*/ +typedef union UTString { + L_Umaxalign dummy; /* ensures maximum alignment for strings */ + TString tsv; +} UTString; + + +/* +** Get the actual string (array of bytes) from a 'TString'. +** (Access to 'extra' ensures that value is really a 'TString'.) +*/ +#define getstr(ts) \ + check_exp(sizeof((ts)->extra), cast(char *, (ts)) + sizeof(UTString)) + + +/* get the actual string (array of bytes) from a Lua value */ +#define svalue(o) getstr(tsvalue(o)) + +/* get string length from 'TString *s' */ +#define tsslen(s) ((s)->tt == LUA_TSHRSTR ? (s)->shrlen : (s)->u.lnglen) + +/* get string length from 'TValue *o' */ +#define vslen(o) tsslen(tsvalue(o)) + + +/* +** Header for userdata; memory area follows the end of this structure +** (aligned according to 'UUdata'; see next). +*/ +typedef struct Udata { + CommonHeader; + lu_byte ttuv_; /* user value's tag */ + struct Table *metatable; + size_t len; /* number of bytes */ + union Value user_; /* user value */ +} Udata; + + +/* +** Ensures that address after this type is always fully aligned. +*/ +typedef union UUdata { + L_Umaxalign dummy; /* ensures maximum alignment for 'local' udata */ + Udata uv; +} UUdata; + + +/* +** Get the address of memory block inside 'Udata'. +** (Access to 'ttuv_' ensures that value is really a 'Udata'.) +*/ +#define getudatamem(u) \ + check_exp(sizeof((u)->ttuv_), (cast(char*, (u)) + sizeof(UUdata))) + +#define setuservalue(L,u,o) \ + { const TValue *io=(o); Udata *iu = (u); \ + iu->user_ = io->value_; iu->ttuv_ = rttype(io); \ + checkliveness(L,io); } + + +#define getuservalue(L,u,o) \ + { TValue *io=(o); const Udata *iu = (u); \ + io->value_ = iu->user_; settt_(io, iu->ttuv_); \ + checkliveness(L,io); } + + +/* +** Description of an upvalue for function prototypes +*/ +typedef struct Upvaldesc { + TString *name; /* upvalue name (for debug information) */ + lu_byte instack; /* whether it is in stack (register) */ + lu_byte idx; /* index of upvalue (in stack or in outer function's list) */ +} Upvaldesc; + + +/* +** Description of a local variable for function prototypes +** (used for debug information) +*/ +typedef struct LocVar { + TString *varname; + int startpc; /* first point where variable is active */ + int endpc; /* first point where variable is dead */ +} LocVar; + + +/* +** Function Prototypes +*/ +typedef struct Proto { + CommonHeader; + lu_byte numparams; /* number of fixed parameters */ + lu_byte is_vararg; + lu_byte maxstacksize; /* number of registers needed by this function */ + int sizeupvalues; /* size of 'upvalues' */ + int sizek; /* size of 'k' */ + int sizecode; + int sizelineinfo; + int sizep; /* size of 'p' */ + int sizelocvars; + int linedefined; /* debug information */ + int lastlinedefined; /* debug information */ + TValue *k; /* constants used by the function */ + Instruction *code; /* opcodes */ + struct Proto **p; /* functions defined inside the function */ + int *lineinfo; /* map from opcodes to source lines (debug information) */ + LocVar *locvars; /* information about local variables (debug information) */ + Upvaldesc *upvalues; /* upvalue information */ + struct LClosure *cache; /* last-created closure with this prototype */ + TString *source; /* used for debug information */ + GCObject *gclist; +} Proto; + + + +/* +** Lua Upvalues +*/ +typedef struct UpVal UpVal; + + +/* +** Closures +*/ + +#define ClosureHeader \ + CommonHeader; lu_byte nupvalues; GCObject *gclist + +typedef struct CClosure { + ClosureHeader; + lua_CFunction f; + TValue upvalue[1]; /* list of upvalues */ +} CClosure; + + +typedef struct LClosure { + ClosureHeader; + struct Proto *p; + UpVal *upvals[1]; /* list of upvalues */ +} LClosure; + + +typedef union Closure { + CClosure c; + LClosure l; +} Closure; + + +#define isLfunction(o) ttisLclosure(o) + +#define getproto(o) (clLvalue(o)->p) + + +/* +** Tables +*/ + +typedef union TKey { + struct { + TValuefields; + int next; /* for chaining (offset for next node) */ + } nk; + TValue tvk; +} TKey; + + +/* copy a value into a key without messing up field 'next' */ +#define setnodekey(L,key,obj) \ + { TKey *k_=(key); const TValue *io_=(obj); \ + k_->nk.value_ = io_->value_; k_->nk.tt_ = io_->tt_; \ + (void)L; checkliveness(L,io_); } + + +typedef struct Node { + TValue i_val; + TKey i_key; +} Node; + + +typedef struct Table { + CommonHeader; + lu_byte flags; /* 1<

lsizenode)) + + +/* +** (address of) a fixed nil value +*/ +#define luaO_nilobject (&luaO_nilobject_) + + +LUAI_DDEC const TValue luaO_nilobject_; + +/* size of buffer for 'luaO_utf8esc' function */ +#define UTF8BUFFSZ 8 + +LUAI_FUNC int luaO_int2fb (unsigned int x); +LUAI_FUNC int luaO_fb2int (int x); +LUAI_FUNC int luaO_utf8esc (char *buff, unsigned long x); +LUAI_FUNC int luaO_ceillog2 (unsigned int x); +LUAI_FUNC void luaO_arith (lua_State *L, int op, const TValue *p1, + const TValue *p2, TValue *res); +LUAI_FUNC size_t luaO_str2num (const char *s, TValue *o); +LUAI_FUNC int luaO_hexavalue (int c); +LUAI_FUNC void luaO_tostring (lua_State *L, StkId obj); +LUAI_FUNC const char *luaO_pushvfstring (lua_State *L, const char *fmt, + va_list argp); +LUAI_FUNC const char *luaO_pushfstring (lua_State *L, const char *fmt, ...); +LUAI_FUNC void luaO_chunkid (char *out, const char *source, size_t len); + + +#endif + diff --git a/src/rcheevos/test/lua/src/lopcodes.c b/src/rcheevos/test/lua/src/lopcodes.c new file mode 100644 index 000000000..a1cbef857 --- /dev/null +++ b/src/rcheevos/test/lua/src/lopcodes.c @@ -0,0 +1,124 @@ +/* +** $Id: lopcodes.c,v 1.55 2015/01/05 13:48:33 roberto Exp $ +** Opcodes for Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#define lopcodes_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lopcodes.h" + + +/* ORDER OP */ + +LUAI_DDEF const char *const luaP_opnames[NUM_OPCODES+1] = { + "MOVE", + "LOADK", + "LOADKX", + "LOADBOOL", + "LOADNIL", + "GETUPVAL", + "GETTABUP", + "GETTABLE", + "SETTABUP", + "SETUPVAL", + "SETTABLE", + "NEWTABLE", + "SELF", + "ADD", + "SUB", + "MUL", + "MOD", + "POW", + "DIV", + "IDIV", + "BAND", + "BOR", + "BXOR", + "SHL", + "SHR", + "UNM", + "BNOT", + "NOT", + "LEN", + "CONCAT", + "JMP", + "EQ", + "LT", + "LE", + "TEST", + "TESTSET", + "CALL", + "TAILCALL", + "RETURN", + "FORLOOP", + "FORPREP", + "TFORCALL", + "TFORLOOP", + "SETLIST", + "CLOSURE", + "VARARG", + "EXTRAARG", + NULL +}; + + +#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m)) + +LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { +/* T A B C mode opcode */ + opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE */ + ,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_LOADK */ + ,opmode(0, 1, OpArgN, OpArgN, iABx) /* OP_LOADKX */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_LOADBOOL */ + ,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_LOADNIL */ + ,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_GETUPVAL */ + ,opmode(0, 1, OpArgU, OpArgK, iABC) /* OP_GETTABUP */ + ,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_GETTABLE */ + ,opmode(0, 0, OpArgK, OpArgK, iABC) /* OP_SETTABUP */ + ,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_SETUPVAL */ + ,opmode(0, 0, OpArgK, OpArgK, iABC) /* OP_SETTABLE */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_NEWTABLE */ + ,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_SELF */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_ADD */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SUB */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MUL */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MOD */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_POW */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_DIV */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_IDIV */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_BAND */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_BOR */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_BXOR */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SHL */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SHR */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_UNM */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_BNOT */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_NOT */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LEN */ + ,opmode(0, 1, OpArgR, OpArgR, iABC) /* OP_CONCAT */ + ,opmode(0, 0, OpArgR, OpArgN, iAsBx) /* OP_JMP */ + ,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_EQ */ + ,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LT */ + ,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LE */ + ,opmode(1, 0, OpArgN, OpArgU, iABC) /* OP_TEST */ + ,opmode(1, 1, OpArgR, OpArgU, iABC) /* OP_TESTSET */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_CALL */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_TAILCALL */ + ,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_RETURN */ + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORLOOP */ + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORPREP */ + ,opmode(0, 0, OpArgN, OpArgU, iABC) /* OP_TFORCALL */ + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_TFORLOOP */ + ,opmode(0, 0, OpArgU, OpArgU, iABC) /* OP_SETLIST */ + ,opmode(0, 1, OpArgU, OpArgN, iABx) /* OP_CLOSURE */ + ,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_VARARG */ + ,opmode(0, 0, OpArgU, OpArgU, iAx) /* OP_EXTRAARG */ +}; + diff --git a/src/rcheevos/test/lua/src/lopcodes.h b/src/rcheevos/test/lua/src/lopcodes.h new file mode 100644 index 000000000..bbc4b6196 --- /dev/null +++ b/src/rcheevos/test/lua/src/lopcodes.h @@ -0,0 +1,297 @@ +/* +** $Id: lopcodes.h,v 1.149 2016/07/19 17:12:21 roberto Exp $ +** Opcodes for Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#ifndef lopcodes_h +#define lopcodes_h + +#include "llimits.h" + + +/*=========================================================================== + We assume that instructions are unsigned numbers. + All instructions have an opcode in the first 6 bits. + Instructions can have the following fields: + 'A' : 8 bits + 'B' : 9 bits + 'C' : 9 bits + 'Ax' : 26 bits ('A', 'B', and 'C' together) + 'Bx' : 18 bits ('B' and 'C' together) + 'sBx' : signed Bx + + A signed argument is represented in excess K; that is, the number + value is the unsigned value minus K. K is exactly the maximum value + for that argument (so that -max is represented by 0, and +max is + represented by 2*max), which is half the maximum for the corresponding + unsigned argument. +===========================================================================*/ + + +enum OpMode {iABC, iABx, iAsBx, iAx}; /* basic instruction format */ + + +/* +** size and position of opcode arguments. +*/ +#define SIZE_C 9 +#define SIZE_B 9 +#define SIZE_Bx (SIZE_C + SIZE_B) +#define SIZE_A 8 +#define SIZE_Ax (SIZE_C + SIZE_B + SIZE_A) + +#define SIZE_OP 6 + +#define POS_OP 0 +#define POS_A (POS_OP + SIZE_OP) +#define POS_C (POS_A + SIZE_A) +#define POS_B (POS_C + SIZE_C) +#define POS_Bx POS_C +#define POS_Ax POS_A + + +/* +** limits for opcode arguments. +** we use (signed) int to manipulate most arguments, +** so they must fit in LUAI_BITSINT-1 bits (-1 for sign) +*/ +#if SIZE_Bx < LUAI_BITSINT-1 +#define MAXARG_Bx ((1<>1) /* 'sBx' is signed */ +#else +#define MAXARG_Bx MAX_INT +#define MAXARG_sBx MAX_INT +#endif + +#if SIZE_Ax < LUAI_BITSINT-1 +#define MAXARG_Ax ((1<>POS_OP) & MASK1(SIZE_OP,0))) +#define SET_OPCODE(i,o) ((i) = (((i)&MASK0(SIZE_OP,POS_OP)) | \ + ((cast(Instruction, o)<>pos) & MASK1(size,0))) +#define setarg(i,v,pos,size) ((i) = (((i)&MASK0(size,pos)) | \ + ((cast(Instruction, v)<> RK(C) */ +OP_UNM,/* A B R(A) := -R(B) */ +OP_BNOT,/* A B R(A) := ~R(B) */ +OP_NOT,/* A B R(A) := not R(B) */ +OP_LEN,/* A B R(A) := length of R(B) */ + +OP_CONCAT,/* A B C R(A) := R(B).. ... ..R(C) */ + +OP_JMP,/* A sBx pc+=sBx; if (A) close all upvalues >= R(A - 1) */ +OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */ +OP_LT,/* A B C if ((RK(B) < RK(C)) ~= A) then pc++ */ +OP_LE,/* A B C if ((RK(B) <= RK(C)) ~= A) then pc++ */ + +OP_TEST,/* A C if not (R(A) <=> C) then pc++ */ +OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */ + +OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */ +OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */ +OP_RETURN,/* A B return R(A), ... ,R(A+B-2) (see note) */ + +OP_FORLOOP,/* A sBx R(A)+=R(A+2); + if R(A) > 4) & 3)) +#define getCMode(m) (cast(enum OpArgMask, (luaP_opmodes[m] >> 2) & 3)) +#define testAMode(m) (luaP_opmodes[m] & (1 << 6)) +#define testTMode(m) (luaP_opmodes[m] & (1 << 7)) + + +LUAI_DDEC const char *const luaP_opnames[NUM_OPCODES+1]; /* opcode names */ + + +/* number of list items to accumulate before a SETLIST instruction */ +#define LFIELDS_PER_FLUSH 50 + + +#endif diff --git a/src/rcheevos/test/lua/src/loslib.c b/src/rcheevos/test/lua/src/loslib.c new file mode 100644 index 000000000..5a94eb906 --- /dev/null +++ b/src/rcheevos/test/lua/src/loslib.c @@ -0,0 +1,407 @@ +/* +** $Id: loslib.c,v 1.65 2016/07/18 17:58:58 roberto Exp $ +** Standard Operating System library +** See Copyright Notice in lua.h +*/ + +#define loslib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* +** {================================================================== +** List of valid conversion specifiers for the 'strftime' function; +** options are grouped by length; group of length 2 start with '||'. +** =================================================================== +*/ +#if !defined(LUA_STRFTIMEOPTIONS) /* { */ + +/* options for ANSI C 89 (only 1-char options) */ +#define L_STRFTIMEC89 "aAbBcdHIjmMpSUwWxXyYZ%" + +/* options for ISO C 99 and POSIX */ +#define L_STRFTIMEC99 "aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%" \ + "||" "EcECExEXEyEY" "OdOeOHOIOmOMOSOuOUOVOwOWOy" /* two-char options */ + +/* options for Windows */ +#define L_STRFTIMEWIN "aAbBcdHIjmMpSUwWxXyYzZ%" \ + "||" "#c#x#d#H#I#j#m#M#S#U#w#W#y#Y" /* two-char options */ + +#if defined(LUA_USE_WINDOWS) +#define LUA_STRFTIMEOPTIONS L_STRFTIMEWIN +#elif defined(LUA_USE_C89) +#define LUA_STRFTIMEOPTIONS L_STRFTIMEC89 +#else /* C99 specification */ +#define LUA_STRFTIMEOPTIONS L_STRFTIMEC99 +#endif + +#endif /* } */ +/* }================================================================== */ + + +/* +** {================================================================== +** Configuration for time-related stuff +** =================================================================== +*/ + +#if !defined(l_time_t) /* { */ +/* +** type to represent time_t in Lua +*/ +#define l_timet lua_Integer +#define l_pushtime(L,t) lua_pushinteger(L,(lua_Integer)(t)) + +static time_t l_checktime (lua_State *L, int arg) { + lua_Integer t = luaL_checkinteger(L, arg); + luaL_argcheck(L, (time_t)t == t, arg, "time out-of-bounds"); + return (time_t)t; +} + +#endif /* } */ + + +#if !defined(l_gmtime) /* { */ +/* +** By default, Lua uses gmtime/localtime, except when POSIX is available, +** where it uses gmtime_r/localtime_r +*/ + +#if defined(LUA_USE_POSIX) /* { */ + +#define l_gmtime(t,r) gmtime_r(t,r) +#define l_localtime(t,r) localtime_r(t,r) + +#else /* }{ */ + +/* ISO C definitions */ +#define l_gmtime(t,r) ((void)(r)->tm_sec, gmtime(t)) +#define l_localtime(t,r) ((void)(r)->tm_sec, localtime(t)) + +#endif /* } */ + +#endif /* } */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Configuration for 'tmpnam': +** By default, Lua uses tmpnam except when POSIX is available, where +** it uses mkstemp. +** =================================================================== +*/ +#if !defined(lua_tmpnam) /* { */ + +#if defined(LUA_USE_POSIX) /* { */ + +#include + +#define LUA_TMPNAMBUFSIZE 32 + +#if !defined(LUA_TMPNAMTEMPLATE) +#define LUA_TMPNAMTEMPLATE "/tmp/lua_XXXXXX" +#endif + +#define lua_tmpnam(b,e) { \ + strcpy(b, LUA_TMPNAMTEMPLATE); \ + e = mkstemp(b); \ + if (e != -1) close(e); \ + e = (e == -1); } + +#else /* }{ */ + +/* ISO C definitions */ +#define LUA_TMPNAMBUFSIZE L_tmpnam +#define lua_tmpnam(b,e) { e = (tmpnam(b) == NULL); } + +#endif /* } */ + +#endif /* } */ +/* }================================================================== */ + + + + +static int os_execute (lua_State *L) { + const char *cmd = luaL_optstring(L, 1, NULL); + int stat = system(cmd); + if (cmd != NULL) + return luaL_execresult(L, stat); + else { + lua_pushboolean(L, stat); /* true if there is a shell */ + return 1; + } +} + + +static int os_remove (lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + return luaL_fileresult(L, remove(filename) == 0, filename); +} + + +static int os_rename (lua_State *L) { + const char *fromname = luaL_checkstring(L, 1); + const char *toname = luaL_checkstring(L, 2); + return luaL_fileresult(L, rename(fromname, toname) == 0, NULL); +} + + +static int os_tmpname (lua_State *L) { + char buff[LUA_TMPNAMBUFSIZE]; + int err; + lua_tmpnam(buff, err); + if (err) + return luaL_error(L, "unable to generate a unique filename"); + lua_pushstring(L, buff); + return 1; +} + + +static int os_getenv (lua_State *L) { + lua_pushstring(L, getenv(luaL_checkstring(L, 1))); /* if NULL push nil */ + return 1; +} + + +static int os_clock (lua_State *L) { + lua_pushnumber(L, ((lua_Number)clock())/(lua_Number)CLOCKS_PER_SEC); + return 1; +} + + +/* +** {====================================================== +** Time/Date operations +** { year=%Y, month=%m, day=%d, hour=%H, min=%M, sec=%S, +** wday=%w+1, yday=%j, isdst=? } +** ======================================================= +*/ + +static void setfield (lua_State *L, const char *key, int value) { + lua_pushinteger(L, value); + lua_setfield(L, -2, key); +} + +static void setboolfield (lua_State *L, const char *key, int value) { + if (value < 0) /* undefined? */ + return; /* does not set field */ + lua_pushboolean(L, value); + lua_setfield(L, -2, key); +} + + +/* +** Set all fields from structure 'tm' in the table on top of the stack +*/ +static void setallfields (lua_State *L, struct tm *stm) { + setfield(L, "sec", stm->tm_sec); + setfield(L, "min", stm->tm_min); + setfield(L, "hour", stm->tm_hour); + setfield(L, "day", stm->tm_mday); + setfield(L, "month", stm->tm_mon + 1); + setfield(L, "year", stm->tm_year + 1900); + setfield(L, "wday", stm->tm_wday + 1); + setfield(L, "yday", stm->tm_yday + 1); + setboolfield(L, "isdst", stm->tm_isdst); +} + + +static int getboolfield (lua_State *L, const char *key) { + int res; + res = (lua_getfield(L, -1, key) == LUA_TNIL) ? -1 : lua_toboolean(L, -1); + lua_pop(L, 1); + return res; +} + + +/* maximum value for date fields (to avoid arithmetic overflows with 'int') */ +#if !defined(L_MAXDATEFIELD) +#define L_MAXDATEFIELD (INT_MAX / 2) +#endif + +static int getfield (lua_State *L, const char *key, int d, int delta) { + int isnum; + int t = lua_getfield(L, -1, key); /* get field and its type */ + lua_Integer res = lua_tointegerx(L, -1, &isnum); + if (!isnum) { /* field is not an integer? */ + if (t != LUA_TNIL) /* some other value? */ + return luaL_error(L, "field '%s' is not an integer", key); + else if (d < 0) /* absent field; no default? */ + return luaL_error(L, "field '%s' missing in date table", key); + res = d; + } + else { + if (!(-L_MAXDATEFIELD <= res && res <= L_MAXDATEFIELD)) + return luaL_error(L, "field '%s' is out-of-bound", key); + res -= delta; + } + lua_pop(L, 1); + return (int)res; +} + + +static const char *checkoption (lua_State *L, const char *conv, + ptrdiff_t convlen, char *buff) { + const char *option = LUA_STRFTIMEOPTIONS; + int oplen = 1; /* length of options being checked */ + for (; *option != '\0' && oplen <= convlen; option += oplen) { + if (*option == '|') /* next block? */ + oplen++; /* will check options with next length (+1) */ + else if (memcmp(conv, option, oplen) == 0) { /* match? */ + memcpy(buff, conv, oplen); /* copy valid option to buffer */ + buff[oplen] = '\0'; + return conv + oplen; /* return next item */ + } + } + luaL_argerror(L, 1, + lua_pushfstring(L, "invalid conversion specifier '%%%s'", conv)); + return conv; /* to avoid warnings */ +} + + +/* maximum size for an individual 'strftime' item */ +#define SIZETIMEFMT 250 + + +static int os_date (lua_State *L) { + size_t slen; + const char *s = luaL_optlstring(L, 1, "%c", &slen); + time_t t = luaL_opt(L, l_checktime, 2, time(NULL)); + const char *se = s + slen; /* 's' end */ + struct tm tmr, *stm; + if (*s == '!') { /* UTC? */ + stm = l_gmtime(&t, &tmr); + s++; /* skip '!' */ + } + else + stm = l_localtime(&t, &tmr); + if (stm == NULL) /* invalid date? */ + luaL_error(L, "time result cannot be represented in this installation"); + if (strcmp(s, "*t") == 0) { + lua_createtable(L, 0, 9); /* 9 = number of fields */ + setallfields(L, stm); + } + else { + char cc[4]; /* buffer for individual conversion specifiers */ + luaL_Buffer b; + cc[0] = '%'; + luaL_buffinit(L, &b); + while (s < se) { + if (*s != '%') /* not a conversion specifier? */ + luaL_addchar(&b, *s++); + else { + size_t reslen; + char *buff = luaL_prepbuffsize(&b, SIZETIMEFMT); + s++; /* skip '%' */ + s = checkoption(L, s, se - s, cc + 1); /* copy specifier to 'cc' */ + reslen = strftime(buff, SIZETIMEFMT, cc, stm); + luaL_addsize(&b, reslen); + } + } + luaL_pushresult(&b); + } + return 1; +} + + +static int os_time (lua_State *L) { + time_t t; + if (lua_isnoneornil(L, 1)) /* called without args? */ + t = time(NULL); /* get current time */ + else { + struct tm ts; + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 1); /* make sure table is at the top */ + ts.tm_sec = getfield(L, "sec", 0, 0); + ts.tm_min = getfield(L, "min", 0, 0); + ts.tm_hour = getfield(L, "hour", 12, 0); + ts.tm_mday = getfield(L, "day", -1, 0); + ts.tm_mon = getfield(L, "month", -1, 1); + ts.tm_year = getfield(L, "year", -1, 1900); + ts.tm_isdst = getboolfield(L, "isdst"); + t = mktime(&ts); + setallfields(L, &ts); /* update fields with normalized values */ + } + if (t != (time_t)(l_timet)t || t == (time_t)(-1)) + luaL_error(L, "time result cannot be represented in this installation"); + l_pushtime(L, t); + return 1; +} + + +static int os_difftime (lua_State *L) { + time_t t1 = l_checktime(L, 1); + time_t t2 = l_checktime(L, 2); + lua_pushnumber(L, (lua_Number)difftime(t1, t2)); + return 1; +} + +/* }====================================================== */ + + +static int os_setlocale (lua_State *L) { + static const int cat[] = {LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, + LC_NUMERIC, LC_TIME}; + static const char *const catnames[] = {"all", "collate", "ctype", "monetary", + "numeric", "time", NULL}; + const char *l = luaL_optstring(L, 1, NULL); + int op = luaL_checkoption(L, 2, "all", catnames); + lua_pushstring(L, setlocale(cat[op], l)); + return 1; +} + + +static int os_exit (lua_State *L) { + int status; + if (lua_isboolean(L, 1)) + status = (lua_toboolean(L, 1) ? EXIT_SUCCESS : EXIT_FAILURE); + else + status = (int)luaL_optinteger(L, 1, EXIT_SUCCESS); + if (lua_toboolean(L, 2)) + lua_close(L); + if (L) exit(status); /* 'if' to avoid warnings for unreachable 'return' */ + return 0; +} + + +static const luaL_Reg syslib[] = { + {"clock", os_clock}, + {"date", os_date}, + {"difftime", os_difftime}, + {"execute", os_execute}, + {"exit", os_exit}, + {"getenv", os_getenv}, + {"remove", os_remove}, + {"rename", os_rename}, + {"setlocale", os_setlocale}, + {"time", os_time}, + {"tmpname", os_tmpname}, + {NULL, NULL} +}; + +/* }====================================================== */ + + + +LUAMOD_API int luaopen_os (lua_State *L) { + luaL_newlib(L, syslib); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/lparser.c b/src/rcheevos/test/lua/src/lparser.c new file mode 100644 index 000000000..cd4512d4d --- /dev/null +++ b/src/rcheevos/test/lua/src/lparser.c @@ -0,0 +1,1650 @@ +/* +** $Id: lparser.c,v 2.155 2016/08/01 19:51:24 roberto Exp $ +** Lua Parser +** See Copyright Notice in lua.h +*/ + +#define lparser_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "llex.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" + + + +/* maximum number of local variables per function (must be smaller + than 250, due to the bytecode format) */ +#define MAXVARS 200 + + +#define hasmultret(k) ((k) == VCALL || (k) == VVARARG) + + +/* because all strings are unified by the scanner, the parser + can use pointer equality for string equality */ +#define eqstr(a,b) ((a) == (b)) + + +/* +** nodes for block list (list of active blocks) +*/ +typedef struct BlockCnt { + struct BlockCnt *previous; /* chain */ + int firstlabel; /* index of first label in this block */ + int firstgoto; /* index of first pending goto in this block */ + lu_byte nactvar; /* # active locals outside the block */ + lu_byte upval; /* true if some variable in the block is an upvalue */ + lu_byte isloop; /* true if 'block' is a loop */ +} BlockCnt; + + + +/* +** prototypes for recursive non-terminal functions +*/ +static void statement (LexState *ls); +static void expr (LexState *ls, expdesc *v); + + +/* semantic error */ +static l_noret semerror (LexState *ls, const char *msg) { + ls->t.token = 0; /* remove "near " from final message */ + luaX_syntaxerror(ls, msg); +} + + +static l_noret error_expected (LexState *ls, int token) { + luaX_syntaxerror(ls, + luaO_pushfstring(ls->L, "%s expected", luaX_token2str(ls, token))); +} + + +static l_noret errorlimit (FuncState *fs, int limit, const char *what) { + lua_State *L = fs->ls->L; + const char *msg; + int line = fs->f->linedefined; + const char *where = (line == 0) + ? "main function" + : luaO_pushfstring(L, "function at line %d", line); + msg = luaO_pushfstring(L, "too many %s (limit is %d) in %s", + what, limit, where); + luaX_syntaxerror(fs->ls, msg); +} + + +static void checklimit (FuncState *fs, int v, int l, const char *what) { + if (v > l) errorlimit(fs, l, what); +} + + +static int testnext (LexState *ls, int c) { + if (ls->t.token == c) { + luaX_next(ls); + return 1; + } + else return 0; +} + + +static void check (LexState *ls, int c) { + if (ls->t.token != c) + error_expected(ls, c); +} + + +static void checknext (LexState *ls, int c) { + check(ls, c); + luaX_next(ls); +} + + +#define check_condition(ls,c,msg) { if (!(c)) luaX_syntaxerror(ls, msg); } + + + +static void check_match (LexState *ls, int what, int who, int where) { + if (!testnext(ls, what)) { + if (where == ls->linenumber) + error_expected(ls, what); + else { + luaX_syntaxerror(ls, luaO_pushfstring(ls->L, + "%s expected (to close %s at line %d)", + luaX_token2str(ls, what), luaX_token2str(ls, who), where)); + } + } +} + + +static TString *str_checkname (LexState *ls) { + TString *ts; + check(ls, TK_NAME); + ts = ls->t.seminfo.ts; + luaX_next(ls); + return ts; +} + + +static void init_exp (expdesc *e, expkind k, int i) { + e->f = e->t = NO_JUMP; + e->k = k; + e->u.info = i; +} + + +static void codestring (LexState *ls, expdesc *e, TString *s) { + init_exp(e, VK, luaK_stringK(ls->fs, s)); +} + + +static void checkname (LexState *ls, expdesc *e) { + codestring(ls, e, str_checkname(ls)); +} + + +static int registerlocalvar (LexState *ls, TString *varname) { + FuncState *fs = ls->fs; + Proto *f = fs->f; + int oldsize = f->sizelocvars; + luaM_growvector(ls->L, f->locvars, fs->nlocvars, f->sizelocvars, + LocVar, SHRT_MAX, "local variables"); + while (oldsize < f->sizelocvars) + f->locvars[oldsize++].varname = NULL; + f->locvars[fs->nlocvars].varname = varname; + luaC_objbarrier(ls->L, f, varname); + return fs->nlocvars++; +} + + +static void new_localvar (LexState *ls, TString *name) { + FuncState *fs = ls->fs; + Dyndata *dyd = ls->dyd; + int reg = registerlocalvar(ls, name); + checklimit(fs, dyd->actvar.n + 1 - fs->firstlocal, + MAXVARS, "local variables"); + luaM_growvector(ls->L, dyd->actvar.arr, dyd->actvar.n + 1, + dyd->actvar.size, Vardesc, MAX_INT, "local variables"); + dyd->actvar.arr[dyd->actvar.n++].idx = cast(short, reg); +} + + +static void new_localvarliteral_ (LexState *ls, const char *name, size_t sz) { + new_localvar(ls, luaX_newstring(ls, name, sz)); +} + +#define new_localvarliteral(ls,v) \ + new_localvarliteral_(ls, "" v, (sizeof(v)/sizeof(char))-1) + + +static LocVar *getlocvar (FuncState *fs, int i) { + int idx = fs->ls->dyd->actvar.arr[fs->firstlocal + i].idx; + lua_assert(idx < fs->nlocvars); + return &fs->f->locvars[idx]; +} + + +static void adjustlocalvars (LexState *ls, int nvars) { + FuncState *fs = ls->fs; + fs->nactvar = cast_byte(fs->nactvar + nvars); + for (; nvars; nvars--) { + getlocvar(fs, fs->nactvar - nvars)->startpc = fs->pc; + } +} + + +static void removevars (FuncState *fs, int tolevel) { + fs->ls->dyd->actvar.n -= (fs->nactvar - tolevel); + while (fs->nactvar > tolevel) + getlocvar(fs, --fs->nactvar)->endpc = fs->pc; +} + + +static int searchupvalue (FuncState *fs, TString *name) { + int i; + Upvaldesc *up = fs->f->upvalues; + for (i = 0; i < fs->nups; i++) { + if (eqstr(up[i].name, name)) return i; + } + return -1; /* not found */ +} + + +static int newupvalue (FuncState *fs, TString *name, expdesc *v) { + Proto *f = fs->f; + int oldsize = f->sizeupvalues; + checklimit(fs, fs->nups + 1, MAXUPVAL, "upvalues"); + luaM_growvector(fs->ls->L, f->upvalues, fs->nups, f->sizeupvalues, + Upvaldesc, MAXUPVAL, "upvalues"); + while (oldsize < f->sizeupvalues) + f->upvalues[oldsize++].name = NULL; + f->upvalues[fs->nups].instack = (v->k == VLOCAL); + f->upvalues[fs->nups].idx = cast_byte(v->u.info); + f->upvalues[fs->nups].name = name; + luaC_objbarrier(fs->ls->L, f, name); + return fs->nups++; +} + + +static int searchvar (FuncState *fs, TString *n) { + int i; + for (i = cast_int(fs->nactvar) - 1; i >= 0; i--) { + if (eqstr(n, getlocvar(fs, i)->varname)) + return i; + } + return -1; /* not found */ +} + + +/* + Mark block where variable at given level was defined + (to emit close instructions later). +*/ +static void markupval (FuncState *fs, int level) { + BlockCnt *bl = fs->bl; + while (bl->nactvar > level) + bl = bl->previous; + bl->upval = 1; +} + + +/* + Find variable with given name 'n'. If it is an upvalue, add this + upvalue into all intermediate functions. +*/ +static void singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) { + if (fs == NULL) /* no more levels? */ + init_exp(var, VVOID, 0); /* default is global */ + else { + int v = searchvar(fs, n); /* look up locals at current level */ + if (v >= 0) { /* found? */ + init_exp(var, VLOCAL, v); /* variable is local */ + if (!base) + markupval(fs, v); /* local will be used as an upval */ + } + else { /* not found as local at current level; try upvalues */ + int idx = searchupvalue(fs, n); /* try existing upvalues */ + if (idx < 0) { /* not found? */ + singlevaraux(fs->prev, n, var, 0); /* try upper levels */ + if (var->k == VVOID) /* not found? */ + return; /* it is a global */ + /* else was LOCAL or UPVAL */ + idx = newupvalue(fs, n, var); /* will be a new upvalue */ + } + init_exp(var, VUPVAL, idx); /* new or old upvalue */ + } + } +} + + +static void singlevar (LexState *ls, expdesc *var) { + TString *varname = str_checkname(ls); + FuncState *fs = ls->fs; + singlevaraux(fs, varname, var, 1); + if (var->k == VVOID) { /* global name? */ + expdesc key; + singlevaraux(fs, ls->envn, var, 1); /* get environment variable */ + lua_assert(var->k != VVOID); /* this one must exist */ + codestring(ls, &key, varname); /* key is variable name */ + luaK_indexed(fs, var, &key); /* env[varname] */ + } +} + + +static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) { + FuncState *fs = ls->fs; + int extra = nvars - nexps; + if (hasmultret(e->k)) { + extra++; /* includes call itself */ + if (extra < 0) extra = 0; + luaK_setreturns(fs, e, extra); /* last exp. provides the difference */ + if (extra > 1) luaK_reserveregs(fs, extra-1); + } + else { + if (e->k != VVOID) luaK_exp2nextreg(fs, e); /* close last expression */ + if (extra > 0) { + int reg = fs->freereg; + luaK_reserveregs(fs, extra); + luaK_nil(fs, reg, extra); + } + } + if (nexps > nvars) + ls->fs->freereg -= nexps - nvars; /* remove extra values */ +} + + +static void enterlevel (LexState *ls) { + lua_State *L = ls->L; + ++L->nCcalls; + checklimit(ls->fs, L->nCcalls, LUAI_MAXCCALLS, "C levels"); +} + + +#define leavelevel(ls) ((ls)->L->nCcalls--) + + +static void closegoto (LexState *ls, int g, Labeldesc *label) { + int i; + FuncState *fs = ls->fs; + Labellist *gl = &ls->dyd->gt; + Labeldesc *gt = &gl->arr[g]; + lua_assert(eqstr(gt->name, label->name)); + if (gt->nactvar < label->nactvar) { + TString *vname = getlocvar(fs, gt->nactvar)->varname; + const char *msg = luaO_pushfstring(ls->L, + " at line %d jumps into the scope of local '%s'", + getstr(gt->name), gt->line, getstr(vname)); + semerror(ls, msg); + } + luaK_patchlist(fs, gt->pc, label->pc); + /* remove goto from pending list */ + for (i = g; i < gl->n - 1; i++) + gl->arr[i] = gl->arr[i + 1]; + gl->n--; +} + + +/* +** try to close a goto with existing labels; this solves backward jumps +*/ +static int findlabel (LexState *ls, int g) { + int i; + BlockCnt *bl = ls->fs->bl; + Dyndata *dyd = ls->dyd; + Labeldesc *gt = &dyd->gt.arr[g]; + /* check labels in current block for a match */ + for (i = bl->firstlabel; i < dyd->label.n; i++) { + Labeldesc *lb = &dyd->label.arr[i]; + if (eqstr(lb->name, gt->name)) { /* correct label? */ + if (gt->nactvar > lb->nactvar && + (bl->upval || dyd->label.n > bl->firstlabel)) + luaK_patchclose(ls->fs, gt->pc, lb->nactvar); + closegoto(ls, g, lb); /* close it */ + return 1; + } + } + return 0; /* label not found; cannot close goto */ +} + + +static int newlabelentry (LexState *ls, Labellist *l, TString *name, + int line, int pc) { + int n = l->n; + luaM_growvector(ls->L, l->arr, n, l->size, + Labeldesc, SHRT_MAX, "labels/gotos"); + l->arr[n].name = name; + l->arr[n].line = line; + l->arr[n].nactvar = ls->fs->nactvar; + l->arr[n].pc = pc; + l->n = n + 1; + return n; +} + + +/* +** check whether new label 'lb' matches any pending gotos in current +** block; solves forward jumps +*/ +static void findgotos (LexState *ls, Labeldesc *lb) { + Labellist *gl = &ls->dyd->gt; + int i = ls->fs->bl->firstgoto; + while (i < gl->n) { + if (eqstr(gl->arr[i].name, lb->name)) + closegoto(ls, i, lb); + else + i++; + } +} + + +/* +** export pending gotos to outer level, to check them against +** outer labels; if the block being exited has upvalues, and +** the goto exits the scope of any variable (which can be the +** upvalue), close those variables being exited. +*/ +static void movegotosout (FuncState *fs, BlockCnt *bl) { + int i = bl->firstgoto; + Labellist *gl = &fs->ls->dyd->gt; + /* correct pending gotos to current block and try to close it + with visible labels */ + while (i < gl->n) { + Labeldesc *gt = &gl->arr[i]; + if (gt->nactvar > bl->nactvar) { + if (bl->upval) + luaK_patchclose(fs, gt->pc, bl->nactvar); + gt->nactvar = bl->nactvar; + } + if (!findlabel(fs->ls, i)) + i++; /* move to next one */ + } +} + + +static void enterblock (FuncState *fs, BlockCnt *bl, lu_byte isloop) { + bl->isloop = isloop; + bl->nactvar = fs->nactvar; + bl->firstlabel = fs->ls->dyd->label.n; + bl->firstgoto = fs->ls->dyd->gt.n; + bl->upval = 0; + bl->previous = fs->bl; + fs->bl = bl; + lua_assert(fs->freereg == fs->nactvar); +} + + +/* +** create a label named 'break' to resolve break statements +*/ +static void breaklabel (LexState *ls) { + TString *n = luaS_new(ls->L, "break"); + int l = newlabelentry(ls, &ls->dyd->label, n, 0, ls->fs->pc); + findgotos(ls, &ls->dyd->label.arr[l]); +} + +/* +** generates an error for an undefined 'goto'; choose appropriate +** message when label name is a reserved word (which can only be 'break') +*/ +static l_noret undefgoto (LexState *ls, Labeldesc *gt) { + const char *msg = isreserved(gt->name) + ? "<%s> at line %d not inside a loop" + : "no visible label '%s' for at line %d"; + msg = luaO_pushfstring(ls->L, msg, getstr(gt->name), gt->line); + semerror(ls, msg); +} + + +static void leaveblock (FuncState *fs) { + BlockCnt *bl = fs->bl; + LexState *ls = fs->ls; + if (bl->previous && bl->upval) { + /* create a 'jump to here' to close upvalues */ + int j = luaK_jump(fs); + luaK_patchclose(fs, j, bl->nactvar); + luaK_patchtohere(fs, j); + } + if (bl->isloop) + breaklabel(ls); /* close pending breaks */ + fs->bl = bl->previous; + removevars(fs, bl->nactvar); + lua_assert(bl->nactvar == fs->nactvar); + fs->freereg = fs->nactvar; /* free registers */ + ls->dyd->label.n = bl->firstlabel; /* remove local labels */ + if (bl->previous) /* inner block? */ + movegotosout(fs, bl); /* update pending gotos to outer block */ + else if (bl->firstgoto < ls->dyd->gt.n) /* pending gotos in outer block? */ + undefgoto(ls, &ls->dyd->gt.arr[bl->firstgoto]); /* error */ +} + + +/* +** adds a new prototype into list of prototypes +*/ +static Proto *addprototype (LexState *ls) { + Proto *clp; + lua_State *L = ls->L; + FuncState *fs = ls->fs; + Proto *f = fs->f; /* prototype of current function */ + if (fs->np >= f->sizep) { + int oldsize = f->sizep; + luaM_growvector(L, f->p, fs->np, f->sizep, Proto *, MAXARG_Bx, "functions"); + while (oldsize < f->sizep) + f->p[oldsize++] = NULL; + } + f->p[fs->np++] = clp = luaF_newproto(L); + luaC_objbarrier(L, f, clp); + return clp; +} + + +/* +** codes instruction to create new closure in parent function. +** The OP_CLOSURE instruction must use the last available register, +** so that, if it invokes the GC, the GC knows which registers +** are in use at that time. +*/ +static void codeclosure (LexState *ls, expdesc *v) { + FuncState *fs = ls->fs->prev; + init_exp(v, VRELOCABLE, luaK_codeABx(fs, OP_CLOSURE, 0, fs->np - 1)); + luaK_exp2nextreg(fs, v); /* fix it at the last register */ +} + + +static void open_func (LexState *ls, FuncState *fs, BlockCnt *bl) { + Proto *f; + fs->prev = ls->fs; /* linked list of funcstates */ + fs->ls = ls; + ls->fs = fs; + fs->pc = 0; + fs->lasttarget = 0; + fs->jpc = NO_JUMP; + fs->freereg = 0; + fs->nk = 0; + fs->np = 0; + fs->nups = 0; + fs->nlocvars = 0; + fs->nactvar = 0; + fs->firstlocal = ls->dyd->actvar.n; + fs->bl = NULL; + f = fs->f; + f->source = ls->source; + f->maxstacksize = 2; /* registers 0/1 are always valid */ + enterblock(fs, bl, 0); +} + + +static void close_func (LexState *ls) { + lua_State *L = ls->L; + FuncState *fs = ls->fs; + Proto *f = fs->f; + luaK_ret(fs, 0, 0); /* final return */ + leaveblock(fs); + luaM_reallocvector(L, f->code, f->sizecode, fs->pc, Instruction); + f->sizecode = fs->pc; + luaM_reallocvector(L, f->lineinfo, f->sizelineinfo, fs->pc, int); + f->sizelineinfo = fs->pc; + luaM_reallocvector(L, f->k, f->sizek, fs->nk, TValue); + f->sizek = fs->nk; + luaM_reallocvector(L, f->p, f->sizep, fs->np, Proto *); + f->sizep = fs->np; + luaM_reallocvector(L, f->locvars, f->sizelocvars, fs->nlocvars, LocVar); + f->sizelocvars = fs->nlocvars; + luaM_reallocvector(L, f->upvalues, f->sizeupvalues, fs->nups, Upvaldesc); + f->sizeupvalues = fs->nups; + lua_assert(fs->bl == NULL); + ls->fs = fs->prev; + luaC_checkGC(L); +} + + + +/*============================================================*/ +/* GRAMMAR RULES */ +/*============================================================*/ + + +/* +** check whether current token is in the follow set of a block. +** 'until' closes syntactical blocks, but do not close scope, +** so it is handled in separate. +*/ +static int block_follow (LexState *ls, int withuntil) { + switch (ls->t.token) { + case TK_ELSE: case TK_ELSEIF: + case TK_END: case TK_EOS: + return 1; + case TK_UNTIL: return withuntil; + default: return 0; + } +} + + +static void statlist (LexState *ls) { + /* statlist -> { stat [';'] } */ + while (!block_follow(ls, 1)) { + if (ls->t.token == TK_RETURN) { + statement(ls); + return; /* 'return' must be last statement */ + } + statement(ls); + } +} + + +static void fieldsel (LexState *ls, expdesc *v) { + /* fieldsel -> ['.' | ':'] NAME */ + FuncState *fs = ls->fs; + expdesc key; + luaK_exp2anyregup(fs, v); + luaX_next(ls); /* skip the dot or colon */ + checkname(ls, &key); + luaK_indexed(fs, v, &key); +} + + +static void yindex (LexState *ls, expdesc *v) { + /* index -> '[' expr ']' */ + luaX_next(ls); /* skip the '[' */ + expr(ls, v); + luaK_exp2val(ls->fs, v); + checknext(ls, ']'); +} + + +/* +** {====================================================================== +** Rules for Constructors +** ======================================================================= +*/ + + +struct ConsControl { + expdesc v; /* last list item read */ + expdesc *t; /* table descriptor */ + int nh; /* total number of 'record' elements */ + int na; /* total number of array elements */ + int tostore; /* number of array elements pending to be stored */ +}; + + +static void recfield (LexState *ls, struct ConsControl *cc) { + /* recfield -> (NAME | '['exp1']') = exp1 */ + FuncState *fs = ls->fs; + int reg = ls->fs->freereg; + expdesc key, val; + int rkkey; + if (ls->t.token == TK_NAME) { + checklimit(fs, cc->nh, MAX_INT, "items in a constructor"); + checkname(ls, &key); + } + else /* ls->t.token == '[' */ + yindex(ls, &key); + cc->nh++; + checknext(ls, '='); + rkkey = luaK_exp2RK(fs, &key); + expr(ls, &val); + luaK_codeABC(fs, OP_SETTABLE, cc->t->u.info, rkkey, luaK_exp2RK(fs, &val)); + fs->freereg = reg; /* free registers */ +} + + +static void closelistfield (FuncState *fs, struct ConsControl *cc) { + if (cc->v.k == VVOID) return; /* there is no list item */ + luaK_exp2nextreg(fs, &cc->v); + cc->v.k = VVOID; + if (cc->tostore == LFIELDS_PER_FLUSH) { + luaK_setlist(fs, cc->t->u.info, cc->na, cc->tostore); /* flush */ + cc->tostore = 0; /* no more items pending */ + } +} + + +static void lastlistfield (FuncState *fs, struct ConsControl *cc) { + if (cc->tostore == 0) return; + if (hasmultret(cc->v.k)) { + luaK_setmultret(fs, &cc->v); + luaK_setlist(fs, cc->t->u.info, cc->na, LUA_MULTRET); + cc->na--; /* do not count last expression (unknown number of elements) */ + } + else { + if (cc->v.k != VVOID) + luaK_exp2nextreg(fs, &cc->v); + luaK_setlist(fs, cc->t->u.info, cc->na, cc->tostore); + } +} + + +static void listfield (LexState *ls, struct ConsControl *cc) { + /* listfield -> exp */ + expr(ls, &cc->v); + checklimit(ls->fs, cc->na, MAX_INT, "items in a constructor"); + cc->na++; + cc->tostore++; +} + + +static void field (LexState *ls, struct ConsControl *cc) { + /* field -> listfield | recfield */ + switch(ls->t.token) { + case TK_NAME: { /* may be 'listfield' or 'recfield' */ + if (luaX_lookahead(ls) != '=') /* expression? */ + listfield(ls, cc); + else + recfield(ls, cc); + break; + } + case '[': { + recfield(ls, cc); + break; + } + default: { + listfield(ls, cc); + break; + } + } +} + + +static void constructor (LexState *ls, expdesc *t) { + /* constructor -> '{' [ field { sep field } [sep] ] '}' + sep -> ',' | ';' */ + FuncState *fs = ls->fs; + int line = ls->linenumber; + int pc = luaK_codeABC(fs, OP_NEWTABLE, 0, 0, 0); + struct ConsControl cc; + cc.na = cc.nh = cc.tostore = 0; + cc.t = t; + init_exp(t, VRELOCABLE, pc); + init_exp(&cc.v, VVOID, 0); /* no value (yet) */ + luaK_exp2nextreg(ls->fs, t); /* fix it at stack top */ + checknext(ls, '{'); + do { + lua_assert(cc.v.k == VVOID || cc.tostore > 0); + if (ls->t.token == '}') break; + closelistfield(fs, &cc); + field(ls, &cc); + } while (testnext(ls, ',') || testnext(ls, ';')); + check_match(ls, '}', '{', line); + lastlistfield(fs, &cc); + SETARG_B(fs->f->code[pc], luaO_int2fb(cc.na)); /* set initial array size */ + SETARG_C(fs->f->code[pc], luaO_int2fb(cc.nh)); /* set initial table size */ +} + +/* }====================================================================== */ + + + +static void parlist (LexState *ls) { + /* parlist -> [ param { ',' param } ] */ + FuncState *fs = ls->fs; + Proto *f = fs->f; + int nparams = 0; + f->is_vararg = 0; + if (ls->t.token != ')') { /* is 'parlist' not empty? */ + do { + switch (ls->t.token) { + case TK_NAME: { /* param -> NAME */ + new_localvar(ls, str_checkname(ls)); + nparams++; + break; + } + case TK_DOTS: { /* param -> '...' */ + luaX_next(ls); + f->is_vararg = 1; /* declared vararg */ + break; + } + default: luaX_syntaxerror(ls, " or '...' expected"); + } + } while (!f->is_vararg && testnext(ls, ',')); + } + adjustlocalvars(ls, nparams); + f->numparams = cast_byte(fs->nactvar); + luaK_reserveregs(fs, fs->nactvar); /* reserve register for parameters */ +} + + +static void body (LexState *ls, expdesc *e, int ismethod, int line) { + /* body -> '(' parlist ')' block END */ + FuncState new_fs; + BlockCnt bl; + new_fs.f = addprototype(ls); + new_fs.f->linedefined = line; + open_func(ls, &new_fs, &bl); + checknext(ls, '('); + if (ismethod) { + new_localvarliteral(ls, "self"); /* create 'self' parameter */ + adjustlocalvars(ls, 1); + } + parlist(ls); + checknext(ls, ')'); + statlist(ls); + new_fs.f->lastlinedefined = ls->linenumber; + check_match(ls, TK_END, TK_FUNCTION, line); + codeclosure(ls, e); + close_func(ls); +} + + +static int explist (LexState *ls, expdesc *v) { + /* explist -> expr { ',' expr } */ + int n = 1; /* at least one expression */ + expr(ls, v); + while (testnext(ls, ',')) { + luaK_exp2nextreg(ls->fs, v); + expr(ls, v); + n++; + } + return n; +} + + +static void funcargs (LexState *ls, expdesc *f, int line) { + FuncState *fs = ls->fs; + expdesc args; + int base, nparams; + switch (ls->t.token) { + case '(': { /* funcargs -> '(' [ explist ] ')' */ + luaX_next(ls); + if (ls->t.token == ')') /* arg list is empty? */ + args.k = VVOID; + else { + explist(ls, &args); + luaK_setmultret(fs, &args); + } + check_match(ls, ')', '(', line); + break; + } + case '{': { /* funcargs -> constructor */ + constructor(ls, &args); + break; + } + case TK_STRING: { /* funcargs -> STRING */ + codestring(ls, &args, ls->t.seminfo.ts); + luaX_next(ls); /* must use 'seminfo' before 'next' */ + break; + } + default: { + luaX_syntaxerror(ls, "function arguments expected"); + } + } + lua_assert(f->k == VNONRELOC); + base = f->u.info; /* base register for call */ + if (hasmultret(args.k)) + nparams = LUA_MULTRET; /* open call */ + else { + if (args.k != VVOID) + luaK_exp2nextreg(fs, &args); /* close last argument */ + nparams = fs->freereg - (base+1); + } + init_exp(f, VCALL, luaK_codeABC(fs, OP_CALL, base, nparams+1, 2)); + luaK_fixline(fs, line); + fs->freereg = base+1; /* call remove function and arguments and leaves + (unless changed) one result */ +} + + + + +/* +** {====================================================================== +** Expression parsing +** ======================================================================= +*/ + + +static void primaryexp (LexState *ls, expdesc *v) { + /* primaryexp -> NAME | '(' expr ')' */ + switch (ls->t.token) { + case '(': { + int line = ls->linenumber; + luaX_next(ls); + expr(ls, v); + check_match(ls, ')', '(', line); + luaK_dischargevars(ls->fs, v); + return; + } + case TK_NAME: { + singlevar(ls, v); + return; + } + default: { + luaX_syntaxerror(ls, "unexpected symbol"); + } + } +} + + +static void suffixedexp (LexState *ls, expdesc *v) { + /* suffixedexp -> + primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */ + FuncState *fs = ls->fs; + int line = ls->linenumber; + primaryexp(ls, v); + for (;;) { + switch (ls->t.token) { + case '.': { /* fieldsel */ + fieldsel(ls, v); + break; + } + case '[': { /* '[' exp1 ']' */ + expdesc key; + luaK_exp2anyregup(fs, v); + yindex(ls, &key); + luaK_indexed(fs, v, &key); + break; + } + case ':': { /* ':' NAME funcargs */ + expdesc key; + luaX_next(ls); + checkname(ls, &key); + luaK_self(fs, v, &key); + funcargs(ls, v, line); + break; + } + case '(': case TK_STRING: case '{': { /* funcargs */ + luaK_exp2nextreg(fs, v); + funcargs(ls, v, line); + break; + } + default: return; + } + } +} + + +static void simpleexp (LexState *ls, expdesc *v) { + /* simpleexp -> FLT | INT | STRING | NIL | TRUE | FALSE | ... | + constructor | FUNCTION body | suffixedexp */ + switch (ls->t.token) { + case TK_FLT: { + init_exp(v, VKFLT, 0); + v->u.nval = ls->t.seminfo.r; + break; + } + case TK_INT: { + init_exp(v, VKINT, 0); + v->u.ival = ls->t.seminfo.i; + break; + } + case TK_STRING: { + codestring(ls, v, ls->t.seminfo.ts); + break; + } + case TK_NIL: { + init_exp(v, VNIL, 0); + break; + } + case TK_TRUE: { + init_exp(v, VTRUE, 0); + break; + } + case TK_FALSE: { + init_exp(v, VFALSE, 0); + break; + } + case TK_DOTS: { /* vararg */ + FuncState *fs = ls->fs; + check_condition(ls, fs->f->is_vararg, + "cannot use '...' outside a vararg function"); + init_exp(v, VVARARG, luaK_codeABC(fs, OP_VARARG, 0, 1, 0)); + break; + } + case '{': { /* constructor */ + constructor(ls, v); + return; + } + case TK_FUNCTION: { + luaX_next(ls); + body(ls, v, 0, ls->linenumber); + return; + } + default: { + suffixedexp(ls, v); + return; + } + } + luaX_next(ls); +} + + +static UnOpr getunopr (int op) { + switch (op) { + case TK_NOT: return OPR_NOT; + case '-': return OPR_MINUS; + case '~': return OPR_BNOT; + case '#': return OPR_LEN; + default: return OPR_NOUNOPR; + } +} + + +static BinOpr getbinopr (int op) { + switch (op) { + case '+': return OPR_ADD; + case '-': return OPR_SUB; + case '*': return OPR_MUL; + case '%': return OPR_MOD; + case '^': return OPR_POW; + case '/': return OPR_DIV; + case TK_IDIV: return OPR_IDIV; + case '&': return OPR_BAND; + case '|': return OPR_BOR; + case '~': return OPR_BXOR; + case TK_SHL: return OPR_SHL; + case TK_SHR: return OPR_SHR; + case TK_CONCAT: return OPR_CONCAT; + case TK_NE: return OPR_NE; + case TK_EQ: return OPR_EQ; + case '<': return OPR_LT; + case TK_LE: return OPR_LE; + case '>': return OPR_GT; + case TK_GE: return OPR_GE; + case TK_AND: return OPR_AND; + case TK_OR: return OPR_OR; + default: return OPR_NOBINOPR; + } +} + + +static const struct { + lu_byte left; /* left priority for each binary operator */ + lu_byte right; /* right priority */ +} priority[] = { /* ORDER OPR */ + {10, 10}, {10, 10}, /* '+' '-' */ + {11, 11}, {11, 11}, /* '*' '%' */ + {14, 13}, /* '^' (right associative) */ + {11, 11}, {11, 11}, /* '/' '//' */ + {6, 6}, {4, 4}, {5, 5}, /* '&' '|' '~' */ + {7, 7}, {7, 7}, /* '<<' '>>' */ + {9, 8}, /* '..' (right associative) */ + {3, 3}, {3, 3}, {3, 3}, /* ==, <, <= */ + {3, 3}, {3, 3}, {3, 3}, /* ~=, >, >= */ + {2, 2}, {1, 1} /* and, or */ +}; + +#define UNARY_PRIORITY 12 /* priority for unary operators */ + + +/* +** subexpr -> (simpleexp | unop subexpr) { binop subexpr } +** where 'binop' is any binary operator with a priority higher than 'limit' +*/ +static BinOpr subexpr (LexState *ls, expdesc *v, int limit) { + BinOpr op; + UnOpr uop; + enterlevel(ls); + uop = getunopr(ls->t.token); + if (uop != OPR_NOUNOPR) { + int line = ls->linenumber; + luaX_next(ls); + subexpr(ls, v, UNARY_PRIORITY); + luaK_prefix(ls->fs, uop, v, line); + } + else simpleexp(ls, v); + /* expand while operators have priorities higher than 'limit' */ + op = getbinopr(ls->t.token); + while (op != OPR_NOBINOPR && priority[op].left > limit) { + expdesc v2; + BinOpr nextop; + int line = ls->linenumber; + luaX_next(ls); + luaK_infix(ls->fs, op, v); + /* read sub-expression with higher priority */ + nextop = subexpr(ls, &v2, priority[op].right); + luaK_posfix(ls->fs, op, v, &v2, line); + op = nextop; + } + leavelevel(ls); + return op; /* return first untreated operator */ +} + + +static void expr (LexState *ls, expdesc *v) { + subexpr(ls, v, 0); +} + +/* }==================================================================== */ + + + +/* +** {====================================================================== +** Rules for Statements +** ======================================================================= +*/ + + +static void block (LexState *ls) { + /* block -> statlist */ + FuncState *fs = ls->fs; + BlockCnt bl; + enterblock(fs, &bl, 0); + statlist(ls); + leaveblock(fs); +} + + +/* +** structure to chain all variables in the left-hand side of an +** assignment +*/ +struct LHS_assign { + struct LHS_assign *prev; + expdesc v; /* variable (global, local, upvalue, or indexed) */ +}; + + +/* +** check whether, in an assignment to an upvalue/local variable, the +** upvalue/local variable is begin used in a previous assignment to a +** table. If so, save original upvalue/local value in a safe place and +** use this safe copy in the previous assignment. +*/ +static void check_conflict (LexState *ls, struct LHS_assign *lh, expdesc *v) { + FuncState *fs = ls->fs; + int extra = fs->freereg; /* eventual position to save local variable */ + int conflict = 0; + for (; lh; lh = lh->prev) { /* check all previous assignments */ + if (lh->v.k == VINDEXED) { /* assigning to a table? */ + /* table is the upvalue/local being assigned now? */ + if (lh->v.u.ind.vt == v->k && lh->v.u.ind.t == v->u.info) { + conflict = 1; + lh->v.u.ind.vt = VLOCAL; + lh->v.u.ind.t = extra; /* previous assignment will use safe copy */ + } + /* index is the local being assigned? (index cannot be upvalue) */ + if (v->k == VLOCAL && lh->v.u.ind.idx == v->u.info) { + conflict = 1; + lh->v.u.ind.idx = extra; /* previous assignment will use safe copy */ + } + } + } + if (conflict) { + /* copy upvalue/local value to a temporary (in position 'extra') */ + OpCode op = (v->k == VLOCAL) ? OP_MOVE : OP_GETUPVAL; + luaK_codeABC(fs, op, extra, v->u.info, 0); + luaK_reserveregs(fs, 1); + } +} + + +static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) { + expdesc e; + check_condition(ls, vkisvar(lh->v.k), "syntax error"); + if (testnext(ls, ',')) { /* assignment -> ',' suffixedexp assignment */ + struct LHS_assign nv; + nv.prev = lh; + suffixedexp(ls, &nv.v); + if (nv.v.k != VINDEXED) + check_conflict(ls, lh, &nv.v); + checklimit(ls->fs, nvars + ls->L->nCcalls, LUAI_MAXCCALLS, + "C levels"); + assignment(ls, &nv, nvars+1); + } + else { /* assignment -> '=' explist */ + int nexps; + checknext(ls, '='); + nexps = explist(ls, &e); + if (nexps != nvars) + adjust_assign(ls, nvars, nexps, &e); + else { + luaK_setoneret(ls->fs, &e); /* close last expression */ + luaK_storevar(ls->fs, &lh->v, &e); + return; /* avoid default */ + } + } + init_exp(&e, VNONRELOC, ls->fs->freereg-1); /* default assignment */ + luaK_storevar(ls->fs, &lh->v, &e); +} + + +static int cond (LexState *ls) { + /* cond -> exp */ + expdesc v; + expr(ls, &v); /* read condition */ + if (v.k == VNIL) v.k = VFALSE; /* 'falses' are all equal here */ + luaK_goiftrue(ls->fs, &v); + return v.f; +} + + +static void gotostat (LexState *ls, int pc) { + int line = ls->linenumber; + TString *label; + int g; + if (testnext(ls, TK_GOTO)) + label = str_checkname(ls); + else { + luaX_next(ls); /* skip break */ + label = luaS_new(ls->L, "break"); + } + g = newlabelentry(ls, &ls->dyd->gt, label, line, pc); + findlabel(ls, g); /* close it if label already defined */ +} + + +/* check for repeated labels on the same block */ +static void checkrepeated (FuncState *fs, Labellist *ll, TString *label) { + int i; + for (i = fs->bl->firstlabel; i < ll->n; i++) { + if (eqstr(label, ll->arr[i].name)) { + const char *msg = luaO_pushfstring(fs->ls->L, + "label '%s' already defined on line %d", + getstr(label), ll->arr[i].line); + semerror(fs->ls, msg); + } + } +} + + +/* skip no-op statements */ +static void skipnoopstat (LexState *ls) { + while (ls->t.token == ';' || ls->t.token == TK_DBCOLON) + statement(ls); +} + + +static void labelstat (LexState *ls, TString *label, int line) { + /* label -> '::' NAME '::' */ + FuncState *fs = ls->fs; + Labellist *ll = &ls->dyd->label; + int l; /* index of new label being created */ + checkrepeated(fs, ll, label); /* check for repeated labels */ + checknext(ls, TK_DBCOLON); /* skip double colon */ + /* create new entry for this label */ + l = newlabelentry(ls, ll, label, line, luaK_getlabel(fs)); + skipnoopstat(ls); /* skip other no-op statements */ + if (block_follow(ls, 0)) { /* label is last no-op statement in the block? */ + /* assume that locals are already out of scope */ + ll->arr[l].nactvar = fs->bl->nactvar; + } + findgotos(ls, &ll->arr[l]); +} + + +static void whilestat (LexState *ls, int line) { + /* whilestat -> WHILE cond DO block END */ + FuncState *fs = ls->fs; + int whileinit; + int condexit; + BlockCnt bl; + luaX_next(ls); /* skip WHILE */ + whileinit = luaK_getlabel(fs); + condexit = cond(ls); + enterblock(fs, &bl, 1); + checknext(ls, TK_DO); + block(ls); + luaK_jumpto(fs, whileinit); + check_match(ls, TK_END, TK_WHILE, line); + leaveblock(fs); + luaK_patchtohere(fs, condexit); /* false conditions finish the loop */ +} + + +static void repeatstat (LexState *ls, int line) { + /* repeatstat -> REPEAT block UNTIL cond */ + int condexit; + FuncState *fs = ls->fs; + int repeat_init = luaK_getlabel(fs); + BlockCnt bl1, bl2; + enterblock(fs, &bl1, 1); /* loop block */ + enterblock(fs, &bl2, 0); /* scope block */ + luaX_next(ls); /* skip REPEAT */ + statlist(ls); + check_match(ls, TK_UNTIL, TK_REPEAT, line); + condexit = cond(ls); /* read condition (inside scope block) */ + if (bl2.upval) /* upvalues? */ + luaK_patchclose(fs, condexit, bl2.nactvar); + leaveblock(fs); /* finish scope */ + luaK_patchlist(fs, condexit, repeat_init); /* close the loop */ + leaveblock(fs); /* finish loop */ +} + + +static int exp1 (LexState *ls) { + expdesc e; + int reg; + expr(ls, &e); + luaK_exp2nextreg(ls->fs, &e); + lua_assert(e.k == VNONRELOC); + reg = e.u.info; + return reg; +} + + +static void forbody (LexState *ls, int base, int line, int nvars, int isnum) { + /* forbody -> DO block */ + BlockCnt bl; + FuncState *fs = ls->fs; + int prep, endfor; + adjustlocalvars(ls, 3); /* control variables */ + checknext(ls, TK_DO); + prep = isnum ? luaK_codeAsBx(fs, OP_FORPREP, base, NO_JUMP) : luaK_jump(fs); + enterblock(fs, &bl, 0); /* scope for declared variables */ + adjustlocalvars(ls, nvars); + luaK_reserveregs(fs, nvars); + block(ls); + leaveblock(fs); /* end of scope for declared variables */ + luaK_patchtohere(fs, prep); + if (isnum) /* numeric for? */ + endfor = luaK_codeAsBx(fs, OP_FORLOOP, base, NO_JUMP); + else { /* generic for */ + luaK_codeABC(fs, OP_TFORCALL, base, 0, nvars); + luaK_fixline(fs, line); + endfor = luaK_codeAsBx(fs, OP_TFORLOOP, base + 2, NO_JUMP); + } + luaK_patchlist(fs, endfor, prep + 1); + luaK_fixline(fs, line); +} + + +static void fornum (LexState *ls, TString *varname, int line) { + /* fornum -> NAME = exp1,exp1[,exp1] forbody */ + FuncState *fs = ls->fs; + int base = fs->freereg; + new_localvarliteral(ls, "(for index)"); + new_localvarliteral(ls, "(for limit)"); + new_localvarliteral(ls, "(for step)"); + new_localvar(ls, varname); + checknext(ls, '='); + exp1(ls); /* initial value */ + checknext(ls, ','); + exp1(ls); /* limit */ + if (testnext(ls, ',')) + exp1(ls); /* optional step */ + else { /* default step = 1 */ + luaK_codek(fs, fs->freereg, luaK_intK(fs, 1)); + luaK_reserveregs(fs, 1); + } + forbody(ls, base, line, 1, 1); +} + + +static void forlist (LexState *ls, TString *indexname) { + /* forlist -> NAME {,NAME} IN explist forbody */ + FuncState *fs = ls->fs; + expdesc e; + int nvars = 4; /* gen, state, control, plus at least one declared var */ + int line; + int base = fs->freereg; + /* create control variables */ + new_localvarliteral(ls, "(for generator)"); + new_localvarliteral(ls, "(for state)"); + new_localvarliteral(ls, "(for control)"); + /* create declared variables */ + new_localvar(ls, indexname); + while (testnext(ls, ',')) { + new_localvar(ls, str_checkname(ls)); + nvars++; + } + checknext(ls, TK_IN); + line = ls->linenumber; + adjust_assign(ls, 3, explist(ls, &e), &e); + luaK_checkstack(fs, 3); /* extra space to call generator */ + forbody(ls, base, line, nvars - 3, 0); +} + + +static void forstat (LexState *ls, int line) { + /* forstat -> FOR (fornum | forlist) END */ + FuncState *fs = ls->fs; + TString *varname; + BlockCnt bl; + enterblock(fs, &bl, 1); /* scope for loop and control variables */ + luaX_next(ls); /* skip 'for' */ + varname = str_checkname(ls); /* first variable name */ + switch (ls->t.token) { + case '=': fornum(ls, varname, line); break; + case ',': case TK_IN: forlist(ls, varname); break; + default: luaX_syntaxerror(ls, "'=' or 'in' expected"); + } + check_match(ls, TK_END, TK_FOR, line); + leaveblock(fs); /* loop scope ('break' jumps to this point) */ +} + + +static void test_then_block (LexState *ls, int *escapelist) { + /* test_then_block -> [IF | ELSEIF] cond THEN block */ + BlockCnt bl; + FuncState *fs = ls->fs; + expdesc v; + int jf; /* instruction to skip 'then' code (if condition is false) */ + luaX_next(ls); /* skip IF or ELSEIF */ + expr(ls, &v); /* read condition */ + checknext(ls, TK_THEN); + if (ls->t.token == TK_GOTO || ls->t.token == TK_BREAK) { + luaK_goiffalse(ls->fs, &v); /* will jump to label if condition is true */ + enterblock(fs, &bl, 0); /* must enter block before 'goto' */ + gotostat(ls, v.t); /* handle goto/break */ + skipnoopstat(ls); /* skip other no-op statements */ + if (block_follow(ls, 0)) { /* 'goto' is the entire block? */ + leaveblock(fs); + return; /* and that is it */ + } + else /* must skip over 'then' part if condition is false */ + jf = luaK_jump(fs); + } + else { /* regular case (not goto/break) */ + luaK_goiftrue(ls->fs, &v); /* skip over block if condition is false */ + enterblock(fs, &bl, 0); + jf = v.f; + } + statlist(ls); /* 'then' part */ + leaveblock(fs); + if (ls->t.token == TK_ELSE || + ls->t.token == TK_ELSEIF) /* followed by 'else'/'elseif'? */ + luaK_concat(fs, escapelist, luaK_jump(fs)); /* must jump over it */ + luaK_patchtohere(fs, jf); +} + + +static void ifstat (LexState *ls, int line) { + /* ifstat -> IF cond THEN block {ELSEIF cond THEN block} [ELSE block] END */ + FuncState *fs = ls->fs; + int escapelist = NO_JUMP; /* exit list for finished parts */ + test_then_block(ls, &escapelist); /* IF cond THEN block */ + while (ls->t.token == TK_ELSEIF) + test_then_block(ls, &escapelist); /* ELSEIF cond THEN block */ + if (testnext(ls, TK_ELSE)) + block(ls); /* 'else' part */ + check_match(ls, TK_END, TK_IF, line); + luaK_patchtohere(fs, escapelist); /* patch escape list to 'if' end */ +} + + +static void localfunc (LexState *ls) { + expdesc b; + FuncState *fs = ls->fs; + new_localvar(ls, str_checkname(ls)); /* new local variable */ + adjustlocalvars(ls, 1); /* enter its scope */ + body(ls, &b, 0, ls->linenumber); /* function created in next register */ + /* debug information will only see the variable after this point! */ + getlocvar(fs, b.u.info)->startpc = fs->pc; +} + + +static void localstat (LexState *ls) { + /* stat -> LOCAL NAME {',' NAME} ['=' explist] */ + int nvars = 0; + int nexps; + expdesc e; + do { + new_localvar(ls, str_checkname(ls)); + nvars++; + } while (testnext(ls, ',')); + if (testnext(ls, '=')) + nexps = explist(ls, &e); + else { + e.k = VVOID; + nexps = 0; + } + adjust_assign(ls, nvars, nexps, &e); + adjustlocalvars(ls, nvars); +} + + +static int funcname (LexState *ls, expdesc *v) { + /* funcname -> NAME {fieldsel} [':' NAME] */ + int ismethod = 0; + singlevar(ls, v); + while (ls->t.token == '.') + fieldsel(ls, v); + if (ls->t.token == ':') { + ismethod = 1; + fieldsel(ls, v); + } + return ismethod; +} + + +static void funcstat (LexState *ls, int line) { + /* funcstat -> FUNCTION funcname body */ + int ismethod; + expdesc v, b; + luaX_next(ls); /* skip FUNCTION */ + ismethod = funcname(ls, &v); + body(ls, &b, ismethod, line); + luaK_storevar(ls->fs, &v, &b); + luaK_fixline(ls->fs, line); /* definition "happens" in the first line */ +} + + +static void exprstat (LexState *ls) { + /* stat -> func | assignment */ + FuncState *fs = ls->fs; + struct LHS_assign v; + suffixedexp(ls, &v.v); + if (ls->t.token == '=' || ls->t.token == ',') { /* stat -> assignment ? */ + v.prev = NULL; + assignment(ls, &v, 1); + } + else { /* stat -> func */ + check_condition(ls, v.v.k == VCALL, "syntax error"); + SETARG_C(getinstruction(fs, &v.v), 1); /* call statement uses no results */ + } +} + + +static void retstat (LexState *ls) { + /* stat -> RETURN [explist] [';'] */ + FuncState *fs = ls->fs; + expdesc e; + int first, nret; /* registers with returned values */ + if (block_follow(ls, 1) || ls->t.token == ';') + first = nret = 0; /* return no values */ + else { + nret = explist(ls, &e); /* optional return values */ + if (hasmultret(e.k)) { + luaK_setmultret(fs, &e); + if (e.k == VCALL && nret == 1) { /* tail call? */ + SET_OPCODE(getinstruction(fs,&e), OP_TAILCALL); + lua_assert(GETARG_A(getinstruction(fs,&e)) == fs->nactvar); + } + first = fs->nactvar; + nret = LUA_MULTRET; /* return all values */ + } + else { + if (nret == 1) /* only one single value? */ + first = luaK_exp2anyreg(fs, &e); + else { + luaK_exp2nextreg(fs, &e); /* values must go to the stack */ + first = fs->nactvar; /* return all active values */ + lua_assert(nret == fs->freereg - first); + } + } + } + luaK_ret(fs, first, nret); + testnext(ls, ';'); /* skip optional semicolon */ +} + + +static void statement (LexState *ls) { + int line = ls->linenumber; /* may be needed for error messages */ + enterlevel(ls); + switch (ls->t.token) { + case ';': { /* stat -> ';' (empty statement) */ + luaX_next(ls); /* skip ';' */ + break; + } + case TK_IF: { /* stat -> ifstat */ + ifstat(ls, line); + break; + } + case TK_WHILE: { /* stat -> whilestat */ + whilestat(ls, line); + break; + } + case TK_DO: { /* stat -> DO block END */ + luaX_next(ls); /* skip DO */ + block(ls); + check_match(ls, TK_END, TK_DO, line); + break; + } + case TK_FOR: { /* stat -> forstat */ + forstat(ls, line); + break; + } + case TK_REPEAT: { /* stat -> repeatstat */ + repeatstat(ls, line); + break; + } + case TK_FUNCTION: { /* stat -> funcstat */ + funcstat(ls, line); + break; + } + case TK_LOCAL: { /* stat -> localstat */ + luaX_next(ls); /* skip LOCAL */ + if (testnext(ls, TK_FUNCTION)) /* local function? */ + localfunc(ls); + else + localstat(ls); + break; + } + case TK_DBCOLON: { /* stat -> label */ + luaX_next(ls); /* skip double colon */ + labelstat(ls, str_checkname(ls), line); + break; + } + case TK_RETURN: { /* stat -> retstat */ + luaX_next(ls); /* skip RETURN */ + retstat(ls); + break; + } + case TK_BREAK: /* stat -> breakstat */ + case TK_GOTO: { /* stat -> 'goto' NAME */ + gotostat(ls, luaK_jump(ls->fs)); + break; + } + default: { /* stat -> func | assignment */ + exprstat(ls); + break; + } + } + lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg && + ls->fs->freereg >= ls->fs->nactvar); + ls->fs->freereg = ls->fs->nactvar; /* free registers */ + leavelevel(ls); +} + +/* }====================================================================== */ + + +/* +** compiles the main function, which is a regular vararg function with an +** upvalue named LUA_ENV +*/ +static void mainfunc (LexState *ls, FuncState *fs) { + BlockCnt bl; + expdesc v; + open_func(ls, fs, &bl); + fs->f->is_vararg = 1; /* main function is always declared vararg */ + init_exp(&v, VLOCAL, 0); /* create and... */ + newupvalue(fs, ls->envn, &v); /* ...set environment upvalue */ + luaX_next(ls); /* read first token */ + statlist(ls); /* parse main body */ + check(ls, TK_EOS); + close_func(ls); +} + + +LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, + Dyndata *dyd, const char *name, int firstchar) { + LexState lexstate; + FuncState funcstate; + LClosure *cl = luaF_newLclosure(L, 1); /* create main closure */ + setclLvalue(L, L->top, cl); /* anchor it (to avoid being collected) */ + luaD_inctop(L); + lexstate.h = luaH_new(L); /* create table for scanner */ + sethvalue(L, L->top, lexstate.h); /* anchor it */ + luaD_inctop(L); + funcstate.f = cl->p = luaF_newproto(L); + funcstate.f->source = luaS_new(L, name); /* create and anchor TString */ + lua_assert(iswhite(funcstate.f)); /* do not need barrier here */ + lexstate.buff = buff; + lexstate.dyd = dyd; + dyd->actvar.n = dyd->gt.n = dyd->label.n = 0; + luaX_setinput(L, &lexstate, z, funcstate.f->source, firstchar); + mainfunc(&lexstate, &funcstate); + lua_assert(!funcstate.prev && funcstate.nups == 1 && !lexstate.fs); + /* all scopes should be correctly finished */ + lua_assert(dyd->actvar.n == 0 && dyd->gt.n == 0 && dyd->label.n == 0); + L->top--; /* remove scanner's table */ + return cl; /* closure is on the stack, too */ +} + diff --git a/src/rcheevos/test/lua/src/lparser.h b/src/rcheevos/test/lua/src/lparser.h new file mode 100644 index 000000000..02e9b03ae --- /dev/null +++ b/src/rcheevos/test/lua/src/lparser.h @@ -0,0 +1,133 @@ +/* +** $Id: lparser.h,v 1.76 2015/12/30 18:16:13 roberto Exp $ +** Lua Parser +** See Copyright Notice in lua.h +*/ + +#ifndef lparser_h +#define lparser_h + +#include "llimits.h" +#include "lobject.h" +#include "lzio.h" + + +/* +** Expression and variable descriptor. +** Code generation for variables and expressions can be delayed to allow +** optimizations; An 'expdesc' structure describes a potentially-delayed +** variable/expression. It has a description of its "main" value plus a +** list of conditional jumps that can also produce its value (generated +** by short-circuit operators 'and'/'or'). +*/ + +/* kinds of variables/expressions */ +typedef enum { + VVOID, /* when 'expdesc' describes the last expression a list, + this kind means an empty list (so, no expression) */ + VNIL, /* constant nil */ + VTRUE, /* constant true */ + VFALSE, /* constant false */ + VK, /* constant in 'k'; info = index of constant in 'k' */ + VKFLT, /* floating constant; nval = numerical float value */ + VKINT, /* integer constant; nval = numerical integer value */ + VNONRELOC, /* expression has its value in a fixed register; + info = result register */ + VLOCAL, /* local variable; info = local register */ + VUPVAL, /* upvalue variable; info = index of upvalue in 'upvalues' */ + VINDEXED, /* indexed variable; + ind.vt = whether 't' is register or upvalue; + ind.t = table register or upvalue; + ind.idx = key's R/K index */ + VJMP, /* expression is a test/comparison; + info = pc of corresponding jump instruction */ + VRELOCABLE, /* expression can put result in any register; + info = instruction pc */ + VCALL, /* expression is a function call; info = instruction pc */ + VVARARG /* vararg expression; info = instruction pc */ +} expkind; + + +#define vkisvar(k) (VLOCAL <= (k) && (k) <= VINDEXED) +#define vkisinreg(k) ((k) == VNONRELOC || (k) == VLOCAL) + +typedef struct expdesc { + expkind k; + union { + lua_Integer ival; /* for VKINT */ + lua_Number nval; /* for VKFLT */ + int info; /* for generic use */ + struct { /* for indexed variables (VINDEXED) */ + short idx; /* index (R/K) */ + lu_byte t; /* table (register or upvalue) */ + lu_byte vt; /* whether 't' is register (VLOCAL) or upvalue (VUPVAL) */ + } ind; + } u; + int t; /* patch list of 'exit when true' */ + int f; /* patch list of 'exit when false' */ +} expdesc; + + +/* description of active local variable */ +typedef struct Vardesc { + short idx; /* variable index in stack */ +} Vardesc; + + +/* description of pending goto statements and label statements */ +typedef struct Labeldesc { + TString *name; /* label identifier */ + int pc; /* position in code */ + int line; /* line where it appeared */ + lu_byte nactvar; /* local level where it appears in current block */ +} Labeldesc; + + +/* list of labels or gotos */ +typedef struct Labellist { + Labeldesc *arr; /* array */ + int n; /* number of entries in use */ + int size; /* array size */ +} Labellist; + + +/* dynamic structures used by the parser */ +typedef struct Dyndata { + struct { /* list of active local variables */ + Vardesc *arr; + int n; + int size; + } actvar; + Labellist gt; /* list of pending gotos */ + Labellist label; /* list of active labels */ +} Dyndata; + + +/* control of blocks */ +struct BlockCnt; /* defined in lparser.c */ + + +/* state needed to generate code for a given function */ +typedef struct FuncState { + Proto *f; /* current function header */ + struct FuncState *prev; /* enclosing function */ + struct LexState *ls; /* lexical state */ + struct BlockCnt *bl; /* chain of current blocks */ + int pc; /* next position to code (equivalent to 'ncode') */ + int lasttarget; /* 'label' of last 'jump label' */ + int jpc; /* list of pending jumps to 'pc' */ + int nk; /* number of elements in 'k' */ + int np; /* number of elements in 'p' */ + int firstlocal; /* index of first local var (in Dyndata array) */ + short nlocvars; /* number of elements in 'f->locvars' */ + lu_byte nactvar; /* number of active local variables */ + lu_byte nups; /* number of upvalues */ + lu_byte freereg; /* first free register */ +} FuncState; + + +LUAI_FUNC LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, + Dyndata *dyd, const char *name, int firstchar); + + +#endif diff --git a/src/rcheevos/test/lua/src/lprefix.h b/src/rcheevos/test/lua/src/lprefix.h new file mode 100644 index 000000000..02daa837f --- /dev/null +++ b/src/rcheevos/test/lua/src/lprefix.h @@ -0,0 +1,45 @@ +/* +** $Id: lprefix.h,v 1.2 2014/12/29 16:54:13 roberto Exp $ +** Definitions for Lua code that must come before any other header file +** See Copyright Notice in lua.h +*/ + +#ifndef lprefix_h +#define lprefix_h + + +/* +** Allows POSIX/XSI stuff +*/ +#if !defined(LUA_USE_C89) /* { */ + +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#elif _XOPEN_SOURCE == 0 +#undef _XOPEN_SOURCE /* use -D_XOPEN_SOURCE=0 to undefine it */ +#endif + +/* +** Allows manipulation of large files in gcc and some other compilers +*/ +#if !defined(LUA_32BITS) && !defined(_FILE_OFFSET_BITS) +#define _LARGEFILE_SOURCE 1 +#define _FILE_OFFSET_BITS 64 +#endif + +#endif /* } */ + + +/* +** Windows stuff +*/ +#if defined(_WIN32) /* { */ + +#if !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS /* avoid warnings about ISO C functions */ +#endif + +#endif /* } */ + +#endif + diff --git a/src/rcheevos/test/lua/src/lstate.c b/src/rcheevos/test/lua/src/lstate.c new file mode 100644 index 000000000..9e4c8f061 --- /dev/null +++ b/src/rcheevos/test/lua/src/lstate.c @@ -0,0 +1,347 @@ +/* +** $Id: lstate.c,v 2.133 2015/11/13 12:16:51 roberto Exp $ +** Global State +** See Copyright Notice in lua.h +*/ + +#define lstate_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "llex.h" +#include "lmem.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + +#if !defined(LUAI_GCPAUSE) +#define LUAI_GCPAUSE 200 /* 200% */ +#endif + +#if !defined(LUAI_GCMUL) +#define LUAI_GCMUL 200 /* GC runs 'twice the speed' of memory allocation */ +#endif + + +/* +** a macro to help the creation of a unique random seed when a state is +** created; the seed is used to randomize hashes. +*/ +#if !defined(luai_makeseed) +#include +#define luai_makeseed() cast(unsigned int, time(NULL)) +#endif + + + +/* +** thread state + extra space +*/ +typedef struct LX { + lu_byte extrc_[LUA_EXTRASPACE]; + lua_State l; +} LX; + + +/* +** Main thread combines a thread state and the global state +*/ +typedef struct LG { + LX l; + global_State g; +} LG; + + + +#define fromstate(L) (cast(LX *, cast(lu_byte *, (L)) - offsetof(LX, l))) + + +/* +** Compute an initial seed as random as possible. Rely on Address Space +** Layout Randomization (if present) to increase randomness.. +*/ +#define addbuff(b,p,e) \ + { size_t t = cast(size_t, e); \ + memcpy(b + p, &t, sizeof(t)); p += sizeof(t); } + +static unsigned int makeseed (lua_State *L) { + char buff[4 * sizeof(size_t)]; + unsigned int h = luai_makeseed(); + int p = 0; + addbuff(buff, p, L); /* heap variable */ + addbuff(buff, p, &h); /* local variable */ + addbuff(buff, p, luaO_nilobject); /* global variable */ + addbuff(buff, p, &lua_newstate); /* public function */ + lua_assert(p == sizeof(buff)); + return luaS_hash(buff, p, h); +} + + +/* +** set GCdebt to a new value keeping the value (totalbytes + GCdebt) +** invariant (and avoiding underflows in 'totalbytes') +*/ +void luaE_setdebt (global_State *g, l_mem debt) { + l_mem tb = gettotalbytes(g); + lua_assert(tb > 0); + if (debt < tb - MAX_LMEM) + debt = tb - MAX_LMEM; /* will make 'totalbytes == MAX_LMEM' */ + g->totalbytes = tb - debt; + g->GCdebt = debt; +} + + +CallInfo *luaE_extendCI (lua_State *L) { + CallInfo *ci = luaM_new(L, CallInfo); + lua_assert(L->ci->next == NULL); + L->ci->next = ci; + ci->previous = L->ci; + ci->next = NULL; + L->nci++; + return ci; +} + + +/* +** free all CallInfo structures not in use by a thread +*/ +void luaE_freeCI (lua_State *L) { + CallInfo *ci = L->ci; + CallInfo *next = ci->next; + ci->next = NULL; + while ((ci = next) != NULL) { + next = ci->next; + luaM_free(L, ci); + L->nci--; + } +} + + +/* +** free half of the CallInfo structures not in use by a thread +*/ +void luaE_shrinkCI (lua_State *L) { + CallInfo *ci = L->ci; + CallInfo *next2; /* next's next */ + /* while there are two nexts */ + while (ci->next != NULL && (next2 = ci->next->next) != NULL) { + luaM_free(L, ci->next); /* free next */ + L->nci--; + ci->next = next2; /* remove 'next' from the list */ + next2->previous = ci; + ci = next2; /* keep next's next */ + } +} + + +static void stack_init (lua_State *L1, lua_State *L) { + int i; CallInfo *ci; + /* initialize stack array */ + L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, TValue); + L1->stacksize = BASIC_STACK_SIZE; + for (i = 0; i < BASIC_STACK_SIZE; i++) + setnilvalue(L1->stack + i); /* erase new stack */ + L1->top = L1->stack; + L1->stack_last = L1->stack + L1->stacksize - EXTRC_STACK; + /* initialize first ci */ + ci = &L1->base_ci; + ci->next = ci->previous = NULL; + ci->callstatus = 0; + ci->func = L1->top; + setnilvalue(L1->top++); /* 'function' entry for this 'ci' */ + ci->top = L1->top + LUA_MINSTACK; + L1->ci = ci; +} + + +static void freestack (lua_State *L) { + if (L->stack == NULL) + return; /* stack not completely built yet */ + L->ci = &L->base_ci; /* free the entire 'ci' list */ + luaE_freeCI(L); + lua_assert(L->nci == 0); + luaM_freearray(L, L->stack, L->stacksize); /* free stack array */ +} + + +/* +** Create registry table and its predefined values +*/ +static void init_registry (lua_State *L, global_State *g) { + TValue temp; + /* create registry */ + Table *registry = luaH_new(L); + sethvalue(L, &g->l_registry, registry); + luaH_resize(L, registry, LUA_RIDX_LAST, 0); + /* registry[LUA_RIDX_MAINTHREAD] = L */ + setthvalue(L, &temp, L); /* temp = L */ + luaH_setint(L, registry, LUA_RIDX_MAINTHREAD, &temp); + /* registry[LUA_RIDX_GLOBALS] = table of globals */ + sethvalue(L, &temp, luaH_new(L)); /* temp = new table (global table) */ + luaH_setint(L, registry, LUA_RIDX_GLOBALS, &temp); +} + + +/* +** open parts of the state that may cause memory-allocation errors. +** ('g->version' != NULL flags that the state was completely build) +*/ +static void f_luaopen (lua_State *L, void *ud) { + global_State *g = G(L); + UNUSED(ud); + stack_init(L, L); /* init stack */ + init_registry(L, g); + luaS_init(L); + luaT_init(L); + luaX_init(L); + g->gcrunning = 1; /* allow gc */ + g->version = lua_version(NULL); + luai_userstateopen(L); +} + + +/* +** preinitialize a thread with consistent values without allocating +** any memory (to avoid errors) +*/ +static void preinit_thread (lua_State *L, global_State *g) { + G(L) = g; + L->stack = NULL; + L->ci = NULL; + L->nci = 0; + L->stacksize = 0; + L->twups = L; /* thread has no upvalues */ + L->errorJmp = NULL; + L->nCcalls = 0; + L->hook = NULL; + L->hookmask = 0; + L->basehookcount = 0; + L->allowhook = 1; + resethookcount(L); + L->openupval = NULL; + L->nny = 1; + L->status = LUA_OK; + L->errfunc = 0; +} + + +static void close_state (lua_State *L) { + global_State *g = G(L); + luaF_close(L, L->stack); /* close all upvalues for this thread */ + luaC_freeallobjects(L); /* collect all objects */ + if (g->version) /* closing a fully built state? */ + luai_userstateclose(L); + luaM_freearray(L, G(L)->strt.hash, G(L)->strt.size); + freestack(L); + lua_assert(gettotalbytes(g) == sizeof(LG)); + (*g->frealloc)(g->ud, fromstate(L), sizeof(LG), 0); /* free main block */ +} + + +LUA_API lua_State *lua_newthread (lua_State *L) { + global_State *g = G(L); + lua_State *L1; + lua_lock(L); + luaC_checkGC(L); + /* create new thread */ + L1 = &cast(LX *, luaM_newobject(L, LUA_TTHREAD, sizeof(LX)))->l; + L1->marked = luaC_white(g); + L1->tt = LUA_TTHREAD; + /* link it on list 'allgc' */ + L1->next = g->allgc; + g->allgc = obj2gco(L1); + /* anchor it on L stack */ + setthvalue(L, L->top, L1); + api_incr_top(L); + preinit_thread(L1, g); + L1->hookmask = L->hookmask; + L1->basehookcount = L->basehookcount; + L1->hook = L->hook; + resethookcount(L1); + /* initialize L1 extra space */ + memcpy(lua_getextraspace(L1), lua_getextraspace(g->mainthread), + LUA_EXTRASPACE); + luai_userstatethread(L, L1); + stack_init(L1, L); /* init stack */ + lua_unlock(L); + return L1; +} + + +void luaE_freethread (lua_State *L, lua_State *L1) { + LX *l = fromstate(L1); + luaF_close(L1, L1->stack); /* close all upvalues for this thread */ + lua_assert(L1->openupval == NULL); + luai_userstatefree(L, L1); + freestack(L1); + luaM_free(L, l); +} + + +LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { + int i; + lua_State *L; + global_State *g; + LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG))); + if (l == NULL) return NULL; + L = &l->l.l; + g = &l->g; + L->next = NULL; + L->tt = LUA_TTHREAD; + g->currentwhite = bitmask(WHITE0BIT); + L->marked = luaC_white(g); + preinit_thread(L, g); + g->frealloc = f; + g->ud = ud; + g->mainthread = L; + g->seed = makeseed(L); + g->gcrunning = 0; /* no GC while building state */ + g->GCestimate = 0; + g->strt.size = g->strt.nuse = 0; + g->strt.hash = NULL; + setnilvalue(&g->l_registry); + g->panic = NULL; + g->version = NULL; + g->gcstate = GCSpause; + g->gckind = KGC_NORMAL; + g->allgc = g->finobj = g->tobefnz = g->fixedgc = NULL; + g->sweepgc = NULL; + g->gray = g->grayagain = NULL; + g->weak = g->ephemeron = g->allweak = NULL; + g->twups = NULL; + g->totalbytes = sizeof(LG); + g->GCdebt = 0; + g->gcfinnum = 0; + g->gcpause = LUAI_GCPAUSE; + g->gcstepmul = LUAI_GCMUL; + for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL; + if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) { + /* memory allocation error: free partial state */ + close_state(L); + L = NULL; + } + return L; +} + + +LUA_API void lua_close (lua_State *L) { + L = G(L)->mainthread; /* only the main thread can be closed */ + lua_lock(L); + close_state(L); +} + + diff --git a/src/rcheevos/test/lua/src/lstate.h b/src/rcheevos/test/lua/src/lstate.h new file mode 100644 index 000000000..ed621c4df --- /dev/null +++ b/src/rcheevos/test/lua/src/lstate.h @@ -0,0 +1,235 @@ +/* +** $Id: lstate.h,v 2.133 2016/12/22 13:08:50 roberto Exp $ +** Global State +** See Copyright Notice in lua.h +*/ + +#ifndef lstate_h +#define lstate_h + +#include "lua.h" + +#include "lobject.h" +#include "ltm.h" +#include "lzio.h" + + +/* + +** Some notes about garbage-collected objects: All objects in Lua must +** be kept somehow accessible until being freed, so all objects always +** belong to one (and only one) of these lists, using field 'next' of +** the 'CommonHeader' for the link: +** +** 'allgc': all objects not marked for finalization; +** 'finobj': all objects marked for finalization; +** 'tobefnz': all objects ready to be finalized; +** 'fixedgc': all objects that are not to be collected (currently +** only small strings, such as reserved words). + +*/ + + +struct lua_longjmp; /* defined in ldo.c */ + + +/* +** Atomic type (relative to signals) to better ensure that 'lua_sethook' +** is thread safe +*/ +#if !defined(l_signalT) +#include +#define l_signalT sig_atomic_t +#endif + + +/* extra stack space to handle TM calls and some other extras */ +#define EXTRC_STACK 5 + + +#define BASIC_STACK_SIZE (2*LUA_MINSTACK) + + +/* kinds of Garbage Collection */ +#define KGC_NORMAL 0 +#define KGC_EMERGENCY 1 /* gc was forced by an allocation failure */ + + +typedef struct stringtable { + TString **hash; + int nuse; /* number of elements */ + int size; +} stringtable; + + +/* +** Information about a call. +** When a thread yields, 'func' is adjusted to pretend that the +** top function has only the yielded values in its stack; in that +** case, the actual 'func' value is saved in field 'extra'. +** When a function calls another with a continuation, 'extra' keeps +** the function index so that, in case of errors, the continuation +** function can be called with the correct top. +*/ +typedef struct CallInfo { + StkId func; /* function index in the stack */ + StkId top; /* top for this function */ + struct CallInfo *previous, *next; /* dynamic call link */ + union { + struct { /* only for Lua functions */ + StkId base; /* base for this function */ + const Instruction *savedpc; + } l; + struct { /* only for C functions */ + lua_KFunction k; /* continuation in case of yields */ + ptrdiff_t old_errfunc; + lua_KContext ctx; /* context info. in case of yields */ + } c; + } u; + ptrdiff_t extra; + short nresults; /* expected number of results from this function */ + unsigned short callstatus; +} CallInfo; + + +/* +** Bits in CallInfo status +*/ +#define CIST_OAH (1<<0) /* original value of 'allowhook' */ +#define CIST_LUA (1<<1) /* call is running a Lua function */ +#define CIST_HOOKED (1<<2) /* call is running a debug hook */ +#define CIST_FRESH (1<<3) /* call is running on a fresh invocation + of luaV_execute */ +#define CIST_YPCALL (1<<4) /* call is a yieldable protected call */ +#define CIST_TAIL (1<<5) /* call was tail called */ +#define CIST_HOOKYIELD (1<<6) /* last hook called yielded */ +#define CIST_LEQ (1<<7) /* using __lt for __le */ +#define CIST_FIN (1<<8) /* call is running a finalizer */ + +#define isLua(ci) ((ci)->callstatus & CIST_LUA) + +/* assume that CIST_OAH has offset 0 and that 'v' is strictly 0/1 */ +#define setoah(st,v) ((st) = ((st) & ~CIST_OAH) | (v)) +#define getoah(st) ((st) & CIST_OAH) + + +/* +** 'global state', shared by all threads of this state +*/ +typedef struct global_State { + lua_Alloc frealloc; /* function to reallocate memory */ + void *ud; /* auxiliary data to 'frealloc' */ + l_mem totalbytes; /* number of bytes currently allocated - GCdebt */ + l_mem GCdebt; /* bytes allocated not yet compensated by the collector */ + lu_mem GCmemtrav; /* memory traversed by the GC */ + lu_mem GCestimate; /* an estimate of the non-garbage memory in use */ + stringtable strt; /* hash table for strings */ + TValue l_registry; + unsigned int seed; /* randomized seed for hashes */ + lu_byte currentwhite; + lu_byte gcstate; /* state of garbage collector */ + lu_byte gckind; /* kind of GC running */ + lu_byte gcrunning; /* true if GC is running */ + GCObject *allgc; /* list of all collectable objects */ + GCObject **sweepgc; /* current position of sweep in list */ + GCObject *finobj; /* list of collectable objects with finalizers */ + GCObject *gray; /* list of gray objects */ + GCObject *grayagain; /* list of objects to be traversed atomically */ + GCObject *weak; /* list of tables with weak values */ + GCObject *ephemeron; /* list of ephemeron tables (weak keys) */ + GCObject *allweak; /* list of all-weak tables */ + GCObject *tobefnz; /* list of userdata to be GC */ + GCObject *fixedgc; /* list of objects not to be collected */ + struct lua_State *twups; /* list of threads with open upvalues */ + unsigned int gcfinnum; /* number of finalizers to call in each GC step */ + int gcpause; /* size of pause between successive GCs */ + int gcstepmul; /* GC 'granularity' */ + lua_CFunction panic; /* to be called in unprotected errors */ + struct lua_State *mainthread; + const lua_Number *version; /* pointer to version number */ + TString *memerrmsg; /* memory-error message */ + TString *tmname[TM_N]; /* array with tag-method names */ + struct Table *mt[LUA_NUMTAGS]; /* metatables for basic types */ + TString *strcache[STRCACHE_N][STRCACHE_M]; /* cache for strings in API */ +} global_State; + + +/* +** 'per thread' state +*/ +struct lua_State { + CommonHeader; + unsigned short nci; /* number of items in 'ci' list */ + lu_byte status; + StkId top; /* first free slot in the stack */ + global_State *l_G; + CallInfo *ci; /* call info for current function */ + const Instruction *oldpc; /* last pc traced */ + StkId stack_last; /* last free slot in the stack */ + StkId stack; /* stack base */ + UpVal *openupval; /* list of open upvalues in this stack */ + GCObject *gclist; + struct lua_State *twups; /* list of threads with open upvalues */ + struct lua_longjmp *errorJmp; /* current error recover point */ + CallInfo base_ci; /* CallInfo for first level (C calling Lua) */ + volatile lua_Hook hook; + ptrdiff_t errfunc; /* current error handling function (stack index) */ + int stacksize; + int basehookcount; + int hookcount; + unsigned short nny; /* number of non-yieldable calls in stack */ + unsigned short nCcalls; /* number of nested C calls */ + l_signalT hookmask; + lu_byte allowhook; +}; + + +#define G(L) (L->l_G) + + +/* +** Union of all collectable objects (only for conversions) +*/ +union GCUnion { + GCObject gc; /* common header */ + struct TString ts; + struct Udata u; + union Closure cl; + struct Table h; + struct Proto p; + struct lua_State th; /* thread */ +}; + + +#define cast_u(o) cast(union GCUnion *, (o)) + +/* macros to convert a GCObject into a specific value */ +#define gco2ts(o) \ + check_exp(novariant((o)->tt) == LUA_TSTRING, &((cast_u(o))->ts)) +#define gco2u(o) check_exp((o)->tt == LUA_TUSERDATA, &((cast_u(o))->u)) +#define gco2lcl(o) check_exp((o)->tt == LUA_TLCL, &((cast_u(o))->cl.l)) +#define gco2ccl(o) check_exp((o)->tt == LUA_TCCL, &((cast_u(o))->cl.c)) +#define gco2cl(o) \ + check_exp(novariant((o)->tt) == LUA_TFUNCTION, &((cast_u(o))->cl)) +#define gco2t(o) check_exp((o)->tt == LUA_TTABLE, &((cast_u(o))->h)) +#define gco2p(o) check_exp((o)->tt == LUA_TPROTO, &((cast_u(o))->p)) +#define gco2th(o) check_exp((o)->tt == LUA_TTHREAD, &((cast_u(o))->th)) + + +/* macro to convert a Lua object into a GCObject */ +#define obj2gco(v) \ + check_exp(novariant((v)->tt) < LUA_TDEADKEY, (&(cast_u(v)->gc))) + + +/* actual number of total bytes allocated */ +#define gettotalbytes(g) cast(lu_mem, (g)->totalbytes + (g)->GCdebt) + +LUAI_FUNC void luaE_setdebt (global_State *g, l_mem debt); +LUAI_FUNC void luaE_freethread (lua_State *L, lua_State *L1); +LUAI_FUNC CallInfo *luaE_extendCI (lua_State *L); +LUAI_FUNC void luaE_freeCI (lua_State *L); +LUAI_FUNC void luaE_shrinkCI (lua_State *L); + + +#endif + diff --git a/src/rcheevos/test/lua/src/lstring.c b/src/rcheevos/test/lua/src/lstring.c new file mode 100644 index 000000000..9351766fd --- /dev/null +++ b/src/rcheevos/test/lua/src/lstring.c @@ -0,0 +1,248 @@ +/* +** $Id: lstring.c,v 2.56 2015/11/23 11:32:51 roberto Exp $ +** String table (keeps all strings handled by Lua) +** See Copyright Notice in lua.h +*/ + +#define lstring_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" + + +#define MEMERRMSG "not enough memory" + + +/* +** Lua will use at most ~(2^LUAI_HASHLIMIT) bytes from a string to +** compute its hash +*/ +#if !defined(LUAI_HASHLIMIT) +#define LUAI_HASHLIMIT 5 +#endif + + +/* +** equality for long strings +*/ +int luaS_eqlngstr (TString *a, TString *b) { + size_t len = a->u.lnglen; + lua_assert(a->tt == LUA_TLNGSTR && b->tt == LUA_TLNGSTR); + return (a == b) || /* same instance or... */ + ((len == b->u.lnglen) && /* equal length and ... */ + (memcmp(getstr(a), getstr(b), len) == 0)); /* equal contents */ +} + + +unsigned int luaS_hash (const char *str, size_t l, unsigned int seed) { + unsigned int h = seed ^ cast(unsigned int, l); + size_t step = (l >> LUAI_HASHLIMIT) + 1; + for (; l >= step; l -= step) + h ^= ((h<<5) + (h>>2) + cast_byte(str[l - 1])); + return h; +} + + +unsigned int luaS_hashlongstr (TString *ts) { + lua_assert(ts->tt == LUA_TLNGSTR); + if (ts->extra == 0) { /* no hash? */ + ts->hash = luaS_hash(getstr(ts), ts->u.lnglen, ts->hash); + ts->extra = 1; /* now it has its hash */ + } + return ts->hash; +} + + +/* +** resizes the string table +*/ +void luaS_resize (lua_State *L, int newsize) { + int i; + stringtable *tb = &G(L)->strt; + if (newsize > tb->size) { /* grow table if needed */ + luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *); + for (i = tb->size; i < newsize; i++) + tb->hash[i] = NULL; + } + for (i = 0; i < tb->size; i++) { /* rehash */ + TString *p = tb->hash[i]; + tb->hash[i] = NULL; + while (p) { /* for each node in the list */ + TString *hnext = p->u.hnext; /* save next */ + unsigned int h = lmod(p->hash, newsize); /* new position */ + p->u.hnext = tb->hash[h]; /* chain it */ + tb->hash[h] = p; + p = hnext; + } + } + if (newsize < tb->size) { /* shrink table if needed */ + /* vanishing slice should be empty */ + lua_assert(tb->hash[newsize] == NULL && tb->hash[tb->size - 1] == NULL); + luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *); + } + tb->size = newsize; +} + + +/* +** Clear API string cache. (Entries cannot be empty, so fill them with +** a non-collectable string.) +*/ +void luaS_clearcache (global_State *g) { + int i, j; + for (i = 0; i < STRCACHE_N; i++) + for (j = 0; j < STRCACHE_M; j++) { + if (iswhite(g->strcache[i][j])) /* will entry be collected? */ + g->strcache[i][j] = g->memerrmsg; /* replace it with something fixed */ + } +} + + +/* +** Initialize the string table and the string cache +*/ +void luaS_init (lua_State *L) { + global_State *g = G(L); + int i, j; + luaS_resize(L, MINSTRTABSIZE); /* initial size of string table */ + /* pre-create memory-error message */ + g->memerrmsg = luaS_newliteral(L, MEMERRMSG); + luaC_fix(L, obj2gco(g->memerrmsg)); /* it should never be collected */ + for (i = 0; i < STRCACHE_N; i++) /* fill cache with valid strings */ + for (j = 0; j < STRCACHE_M; j++) + g->strcache[i][j] = g->memerrmsg; +} + + + +/* +** creates a new string object +*/ +static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) { + TString *ts; + GCObject *o; + size_t totalsize; /* total size of TString object */ + totalsize = sizelstring(l); + o = luaC_newobj(L, tag, totalsize); + ts = gco2ts(o); + ts->hash = h; + ts->extra = 0; + getstr(ts)[l] = '\0'; /* ending 0 */ + return ts; +} + + +TString *luaS_createlngstrobj (lua_State *L, size_t l) { + TString *ts = createstrobj(L, l, LUA_TLNGSTR, G(L)->seed); + ts->u.lnglen = l; + return ts; +} + + +void luaS_remove (lua_State *L, TString *ts) { + stringtable *tb = &G(L)->strt; + TString **p = &tb->hash[lmod(ts->hash, tb->size)]; + while (*p != ts) /* find previous element */ + p = &(*p)->u.hnext; + *p = (*p)->u.hnext; /* remove element from its list */ + tb->nuse--; +} + + +/* +** checks whether short string exists and reuses it or creates a new one +*/ +static TString *internshrstr (lua_State *L, const char *str, size_t l) { + TString *ts; + global_State *g = G(L); + unsigned int h = luaS_hash(str, l, g->seed); + TString **list = &g->strt.hash[lmod(h, g->strt.size)]; + lua_assert(str != NULL); /* otherwise 'memcmp'/'memcpy' are undefined */ + for (ts = *list; ts != NULL; ts = ts->u.hnext) { + if (l == ts->shrlen && + (memcmp(str, getstr(ts), l * sizeof(char)) == 0)) { + /* found! */ + if (isdead(g, ts)) /* dead (but not collected yet)? */ + changewhite(ts); /* resurrect it */ + return ts; + } + } + if (g->strt.nuse >= g->strt.size && g->strt.size <= MAX_INT/2) { + luaS_resize(L, g->strt.size * 2); + list = &g->strt.hash[lmod(h, g->strt.size)]; /* recompute with new size */ + } + ts = createstrobj(L, l, LUA_TSHRSTR, h); + memcpy(getstr(ts), str, l * sizeof(char)); + ts->shrlen = cast_byte(l); + ts->u.hnext = *list; + *list = ts; + g->strt.nuse++; + return ts; +} + + +/* +** new string (with explicit length) +*/ +TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { + if (l <= LUAI_MAXSHORTLEN) /* short string? */ + return internshrstr(L, str, l); + else { + TString *ts; + if (l >= (MAX_SIZE - sizeof(TString))/sizeof(char)) + luaM_toobig(L); + ts = luaS_createlngstrobj(L, l); + memcpy(getstr(ts), str, l * sizeof(char)); + return ts; + } +} + + +/* +** Create or reuse a zero-terminated string, first checking in the +** cache (using the string address as a key). The cache can contain +** only zero-terminated strings, so it is safe to use 'strcmp' to +** check hits. +*/ +TString *luaS_new (lua_State *L, const char *str) { + unsigned int i = point2uint(str) % STRCACHE_N; /* hash */ + int j; + TString **p = G(L)->strcache[i]; + for (j = 0; j < STRCACHE_M; j++) { + if (strcmp(str, getstr(p[j])) == 0) /* hit? */ + return p[j]; /* that is it */ + } + /* normal route */ + for (j = STRCACHE_M - 1; j > 0; j--) + p[j] = p[j - 1]; /* move out last element */ + /* new element is first in the list */ + p[0] = luaS_newlstr(L, str, strlen(str)); + return p[0]; +} + + +Udata *luaS_newudata (lua_State *L, size_t s) { + Udata *u; + GCObject *o; + if (s > MAX_SIZE - sizeof(Udata)) + luaM_toobig(L); + o = luaC_newobj(L, LUA_TUSERDATA, sizeludata(s)); + u = gco2u(o); + u->len = s; + u->metatable = NULL; + setuservalue(L, u, luaO_nilobject); + return u; +} + diff --git a/src/rcheevos/test/lua/src/lstring.h b/src/rcheevos/test/lua/src/lstring.h new file mode 100644 index 000000000..27efd2077 --- /dev/null +++ b/src/rcheevos/test/lua/src/lstring.h @@ -0,0 +1,49 @@ +/* +** $Id: lstring.h,v 1.61 2015/11/03 15:36:01 roberto Exp $ +** String table (keep all strings handled by Lua) +** See Copyright Notice in lua.h +*/ + +#ifndef lstring_h +#define lstring_h + +#include "lgc.h" +#include "lobject.h" +#include "lstate.h" + + +#define sizelstring(l) (sizeof(union UTString) + ((l) + 1) * sizeof(char)) + +#define sizeludata(l) (sizeof(union UUdata) + (l)) +#define sizeudata(u) sizeludata((u)->len) + +#define luaS_newliteral(L, s) (luaS_newlstr(L, "" s, \ + (sizeof(s)/sizeof(char))-1)) + + +/* +** test whether a string is a reserved word +*/ +#define isreserved(s) ((s)->tt == LUA_TSHRSTR && (s)->extra > 0) + + +/* +** equality for short strings, which are always internalized +*/ +#define eqshrstr(a,b) check_exp((a)->tt == LUA_TSHRSTR, (a) == (b)) + + +LUAI_FUNC unsigned int luaS_hash (const char *str, size_t l, unsigned int seed); +LUAI_FUNC unsigned int luaS_hashlongstr (TString *ts); +LUAI_FUNC int luaS_eqlngstr (TString *a, TString *b); +LUAI_FUNC void luaS_resize (lua_State *L, int newsize); +LUAI_FUNC void luaS_clearcache (global_State *g); +LUAI_FUNC void luaS_init (lua_State *L); +LUAI_FUNC void luaS_remove (lua_State *L, TString *ts); +LUAI_FUNC Udata *luaS_newudata (lua_State *L, size_t s); +LUAI_FUNC TString *luaS_newlstr (lua_State *L, const char *str, size_t l); +LUAI_FUNC TString *luaS_new (lua_State *L, const char *str); +LUAI_FUNC TString *luaS_createlngstrobj (lua_State *L, size_t l); + + +#endif diff --git a/src/rcheevos/test/lua/src/lstrlib.c b/src/rcheevos/test/lua/src/lstrlib.c new file mode 100644 index 000000000..c7aa755fa --- /dev/null +++ b/src/rcheevos/test/lua/src/lstrlib.c @@ -0,0 +1,1584 @@ +/* +** $Id: lstrlib.c,v 1.254 2016/12/22 13:08:50 roberto Exp $ +** Standard library for string operations and pattern-matching +** See Copyright Notice in lua.h +*/ + +#define lstrlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* +** maximum number of captures that a pattern can do during +** pattern-matching. This limit is arbitrary, but must fit in +** an unsigned char. +*/ +#if !defined(LUA_MAXCAPTURES) +#define LUA_MAXCAPTURES 32 +#endif + + +/* macro to 'unsign' a character */ +#define uchar(c) ((unsigned char)(c)) + + +/* +** Some sizes are better limited to fit in 'int', but must also fit in +** 'size_t'. (We assume that 'lua_Integer' cannot be smaller than 'int'.) +*/ +#define MAX_SIZET ((size_t)(~(size_t)0)) + +#define MAXSIZE \ + (sizeof(size_t) < sizeof(int) ? MAX_SIZET : (size_t)(INT_MAX)) + + + + +static int str_len (lua_State *L) { + size_t l; + luaL_checklstring(L, 1, &l); + lua_pushinteger(L, (lua_Integer)l); + return 1; +} + + +/* translate a relative string position: negative means back from end */ +static lua_Integer posrelat (lua_Integer pos, size_t len) { + if (pos >= 0) return pos; + else if (0u - (size_t)pos > len) return 0; + else return (lua_Integer)len + pos + 1; +} + + +static int str_sub (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + lua_Integer start = posrelat(luaL_checkinteger(L, 2), l); + lua_Integer end = posrelat(luaL_optinteger(L, 3, -1), l); + if (start < 1) start = 1; + if (end > (lua_Integer)l) end = l; + if (start <= end) + lua_pushlstring(L, s + start - 1, (size_t)(end - start) + 1); + else lua_pushliteral(L, ""); + return 1; +} + + +static int str_reverse (lua_State *L) { + size_t l, i; + luaL_Buffer b; + const char *s = luaL_checklstring(L, 1, &l); + char *p = luaL_buffinitsize(L, &b, l); + for (i = 0; i < l; i++) + p[i] = s[l - i - 1]; + luaL_pushresultsize(&b, l); + return 1; +} + + +static int str_lower (lua_State *L) { + size_t l; + size_t i; + luaL_Buffer b; + const char *s = luaL_checklstring(L, 1, &l); + char *p = luaL_buffinitsize(L, &b, l); + for (i=0; i MAXSIZE / n) /* may overflow? */ + return luaL_error(L, "resulting string too large"); + else { + size_t totallen = (size_t)n * l + (size_t)(n - 1) * lsep; + luaL_Buffer b; + char *p = luaL_buffinitsize(L, &b, totallen); + while (n-- > 1) { /* first n-1 copies (followed by separator) */ + memcpy(p, s, l * sizeof(char)); p += l; + if (lsep > 0) { /* empty 'memcpy' is not that cheap */ + memcpy(p, sep, lsep * sizeof(char)); + p += lsep; + } + } + memcpy(p, s, l * sizeof(char)); /* last copy (not followed by separator) */ + luaL_pushresultsize(&b, totallen); + } + return 1; +} + + +static int str_byte (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + lua_Integer posi = posrelat(luaL_optinteger(L, 2, 1), l); + lua_Integer pose = posrelat(luaL_optinteger(L, 3, posi), l); + int n, i; + if (posi < 1) posi = 1; + if (pose > (lua_Integer)l) pose = l; + if (posi > pose) return 0; /* empty interval; return no values */ + if (pose - posi >= INT_MAX) /* arithmetic overflow? */ + return luaL_error(L, "string slice too long"); + n = (int)(pose - posi) + 1; + luaL_checkstack(L, n, "string slice too long"); + for (i=0; i= ms->level || ms->capture[l].len == CAP_UNFINISHED) + return luaL_error(ms->L, "invalid capture index %%%d", l + 1); + return l; +} + + +static int capture_to_close (MatchState *ms) { + int level = ms->level; + for (level--; level>=0; level--) + if (ms->capture[level].len == CAP_UNFINISHED) return level; + return luaL_error(ms->L, "invalid pattern capture"); +} + + +static const char *classend (MatchState *ms, const char *p) { + switch (*p++) { + case L_ESC: { + if (p == ms->p_end) + luaL_error(ms->L, "malformed pattern (ends with '%%')"); + return p+1; + } + case '[': { + if (*p == '^') p++; + do { /* look for a ']' */ + if (p == ms->p_end) + luaL_error(ms->L, "malformed pattern (missing ']')"); + if (*(p++) == L_ESC && p < ms->p_end) + p++; /* skip escapes (e.g. '%]') */ + } while (*p != ']'); + return p+1; + } + default: { + return p; + } + } +} + + +static int match_class (int c, int cl) { + int res; + switch (tolower(cl)) { + case 'a' : res = isalpha(c); break; + case 'c' : res = iscntrl(c); break; + case 'd' : res = isdigit(c); break; + case 'g' : res = isgraph(c); break; + case 'l' : res = islower(c); break; + case 'p' : res = ispunct(c); break; + case 's' : res = isspace(c); break; + case 'u' : res = isupper(c); break; + case 'w' : res = isalnum(c); break; + case 'x' : res = isxdigit(c); break; + case 'z' : res = (c == 0); break; /* deprecated option */ + default: return (cl == c); + } + return (islower(cl) ? res : !res); +} + + +static int matchbracketclass (int c, const char *p, const char *ec) { + int sig = 1; + if (*(p+1) == '^') { + sig = 0; + p++; /* skip the '^' */ + } + while (++p < ec) { + if (*p == L_ESC) { + p++; + if (match_class(c, uchar(*p))) + return sig; + } + else if ((*(p+1) == '-') && (p+2 < ec)) { + p+=2; + if (uchar(*(p-2)) <= c && c <= uchar(*p)) + return sig; + } + else if (uchar(*p) == c) return sig; + } + return !sig; +} + + +static int singlematch (MatchState *ms, const char *s, const char *p, + const char *ep) { + if (s >= ms->src_end) + return 0; + else { + int c = uchar(*s); + switch (*p) { + case '.': return 1; /* matches any char */ + case L_ESC: return match_class(c, uchar(*(p+1))); + case '[': return matchbracketclass(c, p, ep-1); + default: return (uchar(*p) == c); + } + } +} + + +static const char *matchbalance (MatchState *ms, const char *s, + const char *p) { + if (p >= ms->p_end - 1) + luaL_error(ms->L, "malformed pattern (missing arguments to '%%b')"); + if (*s != *p) return NULL; + else { + int b = *p; + int e = *(p+1); + int cont = 1; + while (++s < ms->src_end) { + if (*s == e) { + if (--cont == 0) return s+1; + } + else if (*s == b) cont++; + } + } + return NULL; /* string ends out of balance */ +} + + +static const char *max_expand (MatchState *ms, const char *s, + const char *p, const char *ep) { + ptrdiff_t i = 0; /* counts maximum expand for item */ + while (singlematch(ms, s + i, p, ep)) + i++; + /* keeps trying to match with the maximum repetitions */ + while (i>=0) { + const char *res = match(ms, (s+i), ep+1); + if (res) return res; + i--; /* else didn't match; reduce 1 repetition to try again */ + } + return NULL; +} + + +static const char *min_expand (MatchState *ms, const char *s, + const char *p, const char *ep) { + for (;;) { + const char *res = match(ms, s, ep+1); + if (res != NULL) + return res; + else if (singlematch(ms, s, p, ep)) + s++; /* try with one more repetition */ + else return NULL; + } +} + + +static const char *start_capture (MatchState *ms, const char *s, + const char *p, int what) { + const char *res; + int level = ms->level; + if (level >= LUA_MAXCAPTURES) luaL_error(ms->L, "too many captures"); + ms->capture[level].init = s; + ms->capture[level].len = what; + ms->level = level+1; + if ((res=match(ms, s, p)) == NULL) /* match failed? */ + ms->level--; /* undo capture */ + return res; +} + + +static const char *end_capture (MatchState *ms, const char *s, + const char *p) { + int l = capture_to_close(ms); + const char *res; + ms->capture[l].len = s - ms->capture[l].init; /* close capture */ + if ((res = match(ms, s, p)) == NULL) /* match failed? */ + ms->capture[l].len = CAP_UNFINISHED; /* undo capture */ + return res; +} + + +static const char *match_capture (MatchState *ms, const char *s, int l) { + size_t len; + l = check_capture(ms, l); + len = ms->capture[l].len; + if ((size_t)(ms->src_end-s) >= len && + memcmp(ms->capture[l].init, s, len) == 0) + return s+len; + else return NULL; +} + + +static const char *match (MatchState *ms, const char *s, const char *p) { + if (ms->matchdepth-- == 0) + luaL_error(ms->L, "pattern too complex"); + init: /* using goto's to optimize tail recursion */ + if (p != ms->p_end) { /* end of pattern? */ + switch (*p) { + case '(': { /* start capture */ + if (*(p + 1) == ')') /* position capture? */ + s = start_capture(ms, s, p + 2, CAP_POSITION); + else + s = start_capture(ms, s, p + 1, CAP_UNFINISHED); + break; + } + case ')': { /* end capture */ + s = end_capture(ms, s, p + 1); + break; + } + case '$': { + if ((p + 1) != ms->p_end) /* is the '$' the last char in pattern? */ + goto dflt; /* no; go to default */ + s = (s == ms->src_end) ? s : NULL; /* check end of string */ + break; + } + case L_ESC: { /* escaped sequences not in the format class[*+?-]? */ + switch (*(p + 1)) { + case 'b': { /* balanced string? */ + s = matchbalance(ms, s, p + 2); + if (s != NULL) { + p += 4; goto init; /* return match(ms, s, p + 4); */ + } /* else fail (s == NULL) */ + break; + } + case 'f': { /* frontier? */ + const char *ep; char previous; + p += 2; + if (*p != '[') + luaL_error(ms->L, "missing '[' after '%%f' in pattern"); + ep = classend(ms, p); /* points to what is next */ + previous = (s == ms->src_init) ? '\0' : *(s - 1); + if (!matchbracketclass(uchar(previous), p, ep - 1) && + matchbracketclass(uchar(*s), p, ep - 1)) { + p = ep; goto init; /* return match(ms, s, ep); */ + } + s = NULL; /* match failed */ + break; + } + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': + case '8': case '9': { /* capture results (%0-%9)? */ + s = match_capture(ms, s, uchar(*(p + 1))); + if (s != NULL) { + p += 2; goto init; /* return match(ms, s, p + 2) */ + } + break; + } + default: goto dflt; + } + break; + } + default: dflt: { /* pattern class plus optional suffix */ + const char *ep = classend(ms, p); /* points to optional suffix */ + /* does not match at least once? */ + if (!singlematch(ms, s, p, ep)) { + if (*ep == '*' || *ep == '?' || *ep == '-') { /* accept empty? */ + p = ep + 1; goto init; /* return match(ms, s, ep + 1); */ + } + else /* '+' or no suffix */ + s = NULL; /* fail */ + } + else { /* matched once */ + switch (*ep) { /* handle optional suffix */ + case '?': { /* optional */ + const char *res; + if ((res = match(ms, s + 1, ep + 1)) != NULL) + s = res; + else { + p = ep + 1; goto init; /* else return match(ms, s, ep + 1); */ + } + break; + } + case '+': /* 1 or more repetitions */ + s++; /* 1 match already done */ + /* FALLTHROUGH */ + case '*': /* 0 or more repetitions */ + s = max_expand(ms, s, p, ep); + break; + case '-': /* 0 or more repetitions (minimum) */ + s = min_expand(ms, s, p, ep); + break; + default: /* no suffix */ + s++; p = ep; goto init; /* return match(ms, s + 1, ep); */ + } + } + break; + } + } + } + ms->matchdepth++; + return s; +} + + + +static const char *lmemfind (const char *s1, size_t l1, + const char *s2, size_t l2) { + if (l2 == 0) return s1; /* empty strings are everywhere */ + else if (l2 > l1) return NULL; /* avoids a negative 'l1' */ + else { + const char *init; /* to search for a '*s2' inside 's1' */ + l2--; /* 1st char will be checked by 'memchr' */ + l1 = l1-l2; /* 's2' cannot be found after that */ + while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) { + init++; /* 1st char is already checked */ + if (memcmp(init, s2+1, l2) == 0) + return init-1; + else { /* correct 'l1' and 's1' to try again */ + l1 -= init-s1; + s1 = init; + } + } + return NULL; /* not found */ + } +} + + +static void push_onecapture (MatchState *ms, int i, const char *s, + const char *e) { + if (i >= ms->level) { + if (i == 0) /* ms->level == 0, too */ + lua_pushlstring(ms->L, s, e - s); /* add whole match */ + else + luaL_error(ms->L, "invalid capture index %%%d", i + 1); + } + else { + ptrdiff_t l = ms->capture[i].len; + if (l == CAP_UNFINISHED) luaL_error(ms->L, "unfinished capture"); + if (l == CAP_POSITION) + lua_pushinteger(ms->L, (ms->capture[i].init - ms->src_init) + 1); + else + lua_pushlstring(ms->L, ms->capture[i].init, l); + } +} + + +static int push_captures (MatchState *ms, const char *s, const char *e) { + int i; + int nlevels = (ms->level == 0 && s) ? 1 : ms->level; + luaL_checkstack(ms->L, nlevels, "too many captures"); + for (i = 0; i < nlevels; i++) + push_onecapture(ms, i, s, e); + return nlevels; /* number of strings pushed */ +} + + +/* check whether pattern has no special characters */ +static int nospecials (const char *p, size_t l) { + size_t upto = 0; + do { + if (strpbrk(p + upto, SPECIALS)) + return 0; /* pattern has a special character */ + upto += strlen(p + upto) + 1; /* may have more after \0 */ + } while (upto <= l); + return 1; /* no special chars found */ +} + + +static void prepstate (MatchState *ms, lua_State *L, + const char *s, size_t ls, const char *p, size_t lp) { + ms->L = L; + ms->matchdepth = MAXCCALLS; + ms->src_init = s; + ms->src_end = s + ls; + ms->p_end = p + lp; +} + + +static void reprepstate (MatchState *ms) { + ms->level = 0; + lua_assert(ms->matchdepth == MAXCCALLS); +} + + +static int str_find_aux (lua_State *L, int find) { + size_t ls, lp; + const char *s = luaL_checklstring(L, 1, &ls); + const char *p = luaL_checklstring(L, 2, &lp); + lua_Integer init = posrelat(luaL_optinteger(L, 3, 1), ls); + if (init < 1) init = 1; + else if (init > (lua_Integer)ls + 1) { /* start after string's end? */ + lua_pushnil(L); /* cannot find anything */ + return 1; + } + /* explicit request or no special characters? */ + if (find && (lua_toboolean(L, 4) || nospecials(p, lp))) { + /* do a plain search */ + const char *s2 = lmemfind(s + init - 1, ls - (size_t)init + 1, p, lp); + if (s2) { + lua_pushinteger(L, (s2 - s) + 1); + lua_pushinteger(L, (s2 - s) + lp); + return 2; + } + } + else { + MatchState ms; + const char *s1 = s + init - 1; + int anchor = (*p == '^'); + if (anchor) { + p++; lp--; /* skip anchor character */ + } + prepstate(&ms, L, s, ls, p, lp); + do { + const char *res; + reprepstate(&ms); + if ((res=match(&ms, s1, p)) != NULL) { + if (find) { + lua_pushinteger(L, (s1 - s) + 1); /* start */ + lua_pushinteger(L, res - s); /* end */ + return push_captures(&ms, NULL, 0) + 2; + } + else + return push_captures(&ms, s1, res); + } + } while (s1++ < ms.src_end && !anchor); + } + lua_pushnil(L); /* not found */ + return 1; +} + + +static int str_find (lua_State *L) { + return str_find_aux(L, 1); +} + + +static int str_match (lua_State *L) { + return str_find_aux(L, 0); +} + + +/* state for 'gmatch' */ +typedef struct GMatchState { + const char *src; /* current position */ + const char *p; /* pattern */ + const char *lastmatch; /* end of last match */ + MatchState ms; /* match state */ +} GMatchState; + + +static int gmatch_aux (lua_State *L) { + GMatchState *gm = (GMatchState *)lua_touserdata(L, lua_upvalueindex(3)); + const char *src; + gm->ms.L = L; + for (src = gm->src; src <= gm->ms.src_end; src++) { + const char *e; + reprepstate(&gm->ms); + if ((e = match(&gm->ms, src, gm->p)) != NULL && e != gm->lastmatch) { + gm->src = gm->lastmatch = e; + return push_captures(&gm->ms, src, e); + } + } + return 0; /* not found */ +} + + +static int gmatch (lua_State *L) { + size_t ls, lp; + const char *s = luaL_checklstring(L, 1, &ls); + const char *p = luaL_checklstring(L, 2, &lp); + GMatchState *gm; + lua_settop(L, 2); /* keep them on closure to avoid being collected */ + gm = (GMatchState *)lua_newuserdata(L, sizeof(GMatchState)); + prepstate(&gm->ms, L, s, ls, p, lp); + gm->src = s; gm->p = p; gm->lastmatch = NULL; + lua_pushcclosure(L, gmatch_aux, 3); + return 1; +} + + +static void add_s (MatchState *ms, luaL_Buffer *b, const char *s, + const char *e) { + size_t l, i; + lua_State *L = ms->L; + const char *news = lua_tolstring(L, 3, &l); + for (i = 0; i < l; i++) { + if (news[i] != L_ESC) + luaL_addchar(b, news[i]); + else { + i++; /* skip ESC */ + if (!isdigit(uchar(news[i]))) { + if (news[i] != L_ESC) + luaL_error(L, "invalid use of '%c' in replacement string", L_ESC); + luaL_addchar(b, news[i]); + } + else if (news[i] == '0') + luaL_addlstring(b, s, e - s); + else { + push_onecapture(ms, news[i] - '1', s, e); + luaL_tolstring(L, -1, NULL); /* if number, convert it to string */ + lua_remove(L, -2); /* remove original value */ + luaL_addvalue(b); /* add capture to accumulated result */ + } + } + } +} + + +static void add_value (MatchState *ms, luaL_Buffer *b, const char *s, + const char *e, int tr) { + lua_State *L = ms->L; + switch (tr) { + case LUA_TFUNCTION: { + int n; + lua_pushvalue(L, 3); + n = push_captures(ms, s, e); + lua_call(L, n, 1); + break; + } + case LUA_TTABLE: { + push_onecapture(ms, 0, s, e); + lua_gettable(L, 3); + break; + } + default: { /* LUA_TNUMBER or LUA_TSTRING */ + add_s(ms, b, s, e); + return; + } + } + if (!lua_toboolean(L, -1)) { /* nil or false? */ + lua_pop(L, 1); + lua_pushlstring(L, s, e - s); /* keep original text */ + } + else if (!lua_isstring(L, -1)) + luaL_error(L, "invalid replacement value (a %s)", luaL_typename(L, -1)); + luaL_addvalue(b); /* add result to accumulator */ +} + + +static int str_gsub (lua_State *L) { + size_t srcl, lp; + const char *src = luaL_checklstring(L, 1, &srcl); /* subject */ + const char *p = luaL_checklstring(L, 2, &lp); /* pattern */ + const char *lastmatch = NULL; /* end of last match */ + int tr = lua_type(L, 3); /* replacement type */ + lua_Integer max_s = luaL_optinteger(L, 4, srcl + 1); /* max replacements */ + int anchor = (*p == '^'); + lua_Integer n = 0; /* replacement count */ + MatchState ms; + luaL_Buffer b; + luaL_argcheck(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || + tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, + "string/function/table expected"); + luaL_buffinit(L, &b); + if (anchor) { + p++; lp--; /* skip anchor character */ + } + prepstate(&ms, L, src, srcl, p, lp); + while (n < max_s) { + const char *e; + reprepstate(&ms); /* (re)prepare state for new match */ + if ((e = match(&ms, src, p)) != NULL && e != lastmatch) { /* match? */ + n++; + add_value(&ms, &b, src, e, tr); /* add replacement to buffer */ + src = lastmatch = e; + } + else if (src < ms.src_end) /* otherwise, skip one character */ + luaL_addchar(&b, *src++); + else break; /* end of subject */ + if (anchor) break; + } + luaL_addlstring(&b, src, ms.src_end-src); + luaL_pushresult(&b); + lua_pushinteger(L, n); /* number of substitutions */ + return 2; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** STRING FORMAT +** ======================================================= +*/ + +#if !defined(lua_number2strx) /* { */ + +/* +** Hexadecimal floating-point formatter +*/ + +#include + +#define SIZELENMOD (sizeof(LUA_NUMBER_FRMLEN)/sizeof(char)) + + +/* +** Number of bits that goes into the first digit. It can be any value +** between 1 and 4; the following definition tries to align the number +** to nibble boundaries by making what is left after that first digit a +** multiple of 4. +*/ +#define L_NBFD ((l_mathlim(MANT_DIG) - 1)%4 + 1) + + +/* +** Add integer part of 'x' to buffer and return new 'x' +*/ +static lua_Number adddigit (char *buff, int n, lua_Number x) { + lua_Number dd = l_mathop(floor)(x); /* get integer part from 'x' */ + int d = (int)dd; + buff[n] = (d < 10 ? d + '0' : d - 10 + 'a'); /* add to buffer */ + return x - dd; /* return what is left */ +} + + +static int num2straux (char *buff, int sz, lua_Number x) { + /* if 'inf' or 'NaN', format it like '%g' */ + if (x != x || x == (lua_Number)HUGE_VAL || x == -(lua_Number)HUGE_VAL) + return l_sprintf(buff, sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)x); + else if (x == 0) { /* can be -0... */ + /* create "0" or "-0" followed by exponent */ + return l_sprintf(buff, sz, LUA_NUMBER_FMT "x0p+0", (LUAI_UACNUMBER)x); + } + else { + int e; + lua_Number m = l_mathop(frexp)(x, &e); /* 'x' fraction and exponent */ + int n = 0; /* character count */ + if (m < 0) { /* is number negative? */ + buff[n++] = '-'; /* add signal */ + m = -m; /* make it positive */ + } + buff[n++] = '0'; buff[n++] = 'x'; /* add "0x" */ + m = adddigit(buff, n++, m * (1 << L_NBFD)); /* add first digit */ + e -= L_NBFD; /* this digit goes before the radix point */ + if (m > 0) { /* more digits? */ + buff[n++] = lua_getlocaledecpoint(); /* add radix point */ + do { /* add as many digits as needed */ + m = adddigit(buff, n++, m * 16); + } while (m > 0); + } + n += l_sprintf(buff + n, sz - n, "p%+d", e); /* add exponent */ + lua_assert(n < sz); + return n; + } +} + + +static int lua_number2strx (lua_State *L, char *buff, int sz, + const char *fmt, lua_Number x) { + int n = num2straux(buff, sz, x); + if (fmt[SIZELENMOD] == 'A') { + int i; + for (i = 0; i < n; i++) + buff[i] = toupper(uchar(buff[i])); + } + else if (fmt[SIZELENMOD] != 'a') + luaL_error(L, "modifiers for format '%%a'/'%%A' not implemented"); + return n; +} + +#endif /* } */ + + +/* +** Maximum size of each formatted item. This maximum size is produced +** by format('%.99f', -maxfloat), and is equal to 99 + 3 ('-', '.', +** and '\0') + number of decimal digits to represent maxfloat (which +** is maximum exponent + 1). (99+3+1 then rounded to 120 for "extra +** expenses", such as locale-dependent stuff) +*/ +#define MAX_ITEM (120 + l_mathlim(MAX_10_EXP)) + + +/* valid flags in a format specification */ +#define FLAGS "-+ #0" + +/* +** maximum size of each format specification (such as "%-099.99d") +*/ +#define MAX_FORMAT 32 + + +static void addquoted (luaL_Buffer *b, const char *s, size_t len) { + luaL_addchar(b, '"'); + while (len--) { + if (*s == '"' || *s == '\\' || *s == '\n') { + luaL_addchar(b, '\\'); + luaL_addchar(b, *s); + } + else if (iscntrl(uchar(*s))) { + char buff[10]; + if (!isdigit(uchar(*(s+1)))) + l_sprintf(buff, sizeof(buff), "\\%d", (int)uchar(*s)); + else + l_sprintf(buff, sizeof(buff), "\\%03d", (int)uchar(*s)); + luaL_addstring(b, buff); + } + else + luaL_addchar(b, *s); + s++; + } + luaL_addchar(b, '"'); +} + + +/* +** Ensures the 'buff' string uses a dot as the radix character. +*/ +static void checkdp (char *buff, int nb) { + if (memchr(buff, '.', nb) == NULL) { /* no dot? */ + char point = lua_getlocaledecpoint(); /* try locale point */ + char *ppoint = (char *)memchr(buff, point, nb); + if (ppoint) *ppoint = '.'; /* change it to a dot */ + } +} + + +static void addliteral (lua_State *L, luaL_Buffer *b, int arg) { + switch (lua_type(L, arg)) { + case LUA_TSTRING: { + size_t len; + const char *s = lua_tolstring(L, arg, &len); + addquoted(b, s, len); + break; + } + case LUA_TNUMBER: { + char *buff = luaL_prepbuffsize(b, MAX_ITEM); + int nb; + if (!lua_isinteger(L, arg)) { /* float? */ + lua_Number n = lua_tonumber(L, arg); /* write as hexa ('%a') */ + nb = lua_number2strx(L, buff, MAX_ITEM, "%" LUA_NUMBER_FRMLEN "a", n); + checkdp(buff, nb); /* ensure it uses a dot */ + } + else { /* integers */ + lua_Integer n = lua_tointeger(L, arg); + const char *format = (n == LUA_MININTEGER) /* corner case? */ + ? "0x%" LUA_INTEGER_FRMLEN "x" /* use hexa */ + : LUA_INTEGER_FMT; /* else use default format */ + nb = l_sprintf(buff, MAX_ITEM, format, (LUAI_UACINT)n); + } + luaL_addsize(b, nb); + break; + } + case LUA_TNIL: case LUA_TBOOLEAN: { + luaL_tolstring(L, arg, NULL); + luaL_addvalue(b); + break; + } + default: { + luaL_argerror(L, arg, "value has no literal form"); + } + } +} + + +static const char *scanformat (lua_State *L, const char *strfrmt, char *form) { + const char *p = strfrmt; + while (*p != '\0' && strchr(FLAGS, *p) != NULL) p++; /* skip flags */ + if ((size_t)(p - strfrmt) >= sizeof(FLAGS)/sizeof(char)) + luaL_error(L, "invalid format (repeated flags)"); + if (isdigit(uchar(*p))) p++; /* skip width */ + if (isdigit(uchar(*p))) p++; /* (2 digits at most) */ + if (*p == '.') { + p++; + if (isdigit(uchar(*p))) p++; /* skip precision */ + if (isdigit(uchar(*p))) p++; /* (2 digits at most) */ + } + if (isdigit(uchar(*p))) + luaL_error(L, "invalid format (width or precision too long)"); + *(form++) = '%'; + memcpy(form, strfrmt, ((p - strfrmt) + 1) * sizeof(char)); + form += (p - strfrmt) + 1; + *form = '\0'; + return p; +} + + +/* +** add length modifier into formats +*/ +static void addlenmod (char *form, const char *lenmod) { + size_t l = strlen(form); + size_t lm = strlen(lenmod); + char spec = form[l - 1]; + strcpy(form + l - 1, lenmod); + form[l + lm - 1] = spec; + form[l + lm] = '\0'; +} + + +static int str_format (lua_State *L) { + int top = lua_gettop(L); + int arg = 1; + size_t sfl; + const char *strfrmt = luaL_checklstring(L, arg, &sfl); + const char *strfrmt_end = strfrmt+sfl; + luaL_Buffer b; + luaL_buffinit(L, &b); + while (strfrmt < strfrmt_end) { + if (*strfrmt != L_ESC) + luaL_addchar(&b, *strfrmt++); + else if (*++strfrmt == L_ESC) + luaL_addchar(&b, *strfrmt++); /* %% */ + else { /* format item */ + char form[MAX_FORMAT]; /* to store the format ('%...') */ + char *buff = luaL_prepbuffsize(&b, MAX_ITEM); /* to put formatted item */ + int nb = 0; /* number of bytes in added item */ + if (++arg > top) + luaL_argerror(L, arg, "no value"); + strfrmt = scanformat(L, strfrmt, form); + switch (*strfrmt++) { + case 'c': { + nb = l_sprintf(buff, MAX_ITEM, form, (int)luaL_checkinteger(L, arg)); + break; + } + case 'd': case 'i': + case 'o': case 'u': case 'x': case 'X': { + lua_Integer n = luaL_checkinteger(L, arg); + addlenmod(form, LUA_INTEGER_FRMLEN); + nb = l_sprintf(buff, MAX_ITEM, form, (LUAI_UACINT)n); + break; + } + case 'a': case 'A': + addlenmod(form, LUA_NUMBER_FRMLEN); + nb = lua_number2strx(L, buff, MAX_ITEM, form, + luaL_checknumber(L, arg)); + break; + case 'e': case 'E': case 'f': + case 'g': case 'G': { + lua_Number n = luaL_checknumber(L, arg); + addlenmod(form, LUA_NUMBER_FRMLEN); + nb = l_sprintf(buff, MAX_ITEM, form, (LUAI_UACNUMBER)n); + break; + } + case 'q': { + addliteral(L, &b, arg); + break; + } + case 's': { + size_t l; + const char *s = luaL_tolstring(L, arg, &l); + if (form[2] == '\0') /* no modifiers? */ + luaL_addvalue(&b); /* keep entire string */ + else { + luaL_argcheck(L, l == strlen(s), arg, "string contains zeros"); + if (!strchr(form, '.') && l >= 100) { + /* no precision and string is too long to be formatted */ + luaL_addvalue(&b); /* keep entire string */ + } + else { /* format the string into 'buff' */ + nb = l_sprintf(buff, MAX_ITEM, form, s); + lua_pop(L, 1); /* remove result from 'luaL_tolstring' */ + } + } + break; + } + default: { /* also treat cases 'pnLlh' */ + return luaL_error(L, "invalid option '%%%c' to 'format'", + *(strfrmt - 1)); + } + } + lua_assert(nb < MAX_ITEM); + luaL_addsize(&b, nb); + } + } + luaL_pushresult(&b); + return 1; +} + +/* }====================================================== */ + + +/* +** {====================================================== +** PACK/UNPACK +** ======================================================= +*/ + + +/* value used for padding */ +#if !defined(LUAL_PACKPADBYTE) +#define LUAL_PACKPADBYTE 0x00 +#endif + +/* maximum size for the binary representation of an integer */ +#define MAXINTSIZE 16 + +/* number of bits in a character */ +#define NB CHAR_BIT + +/* mask for one character (NB 1's) */ +#define MC ((1 << NB) - 1) + +/* size of a lua_Integer */ +#define SZINT ((int)sizeof(lua_Integer)) + + +/* dummy union to get native endianness */ +static const union { + int dummy; + char little; /* true iff machine is little endian */ +} nativeendian = {1}; + + +/* dummy structure to get native alignment requirements */ +struct cD { + char c; + union { double d; void *p; lua_Integer i; lua_Number n; } u; +}; + +#define MAXALIGN (offsetof(struct cD, u)) + + +/* +** Union for serializing floats +*/ +typedef union Ftypes { + float f; + double d; + lua_Number n; + char buff[5 * sizeof(lua_Number)]; /* enough for any float type */ +} Ftypes; + + +/* +** information to pack/unpack stuff +*/ +typedef struct Header { + lua_State *L; + int islittle; + int maxalign; +} Header; + + +/* +** options for pack/unpack +*/ +typedef enum KOption { + Kint, /* signed integers */ + Kuint, /* unsigned integers */ + Kfloat, /* floating-point numbers */ + Kchar, /* fixed-length strings */ + Kstring, /* strings with prefixed length */ + Kzstr, /* zero-terminated strings */ + Kpadding, /* padding */ + Kpaddalign, /* padding for alignment */ + Knop /* no-op (configuration or spaces) */ +} KOption; + + +/* +** Read an integer numeral from string 'fmt' or return 'df' if +** there is no numeral +*/ +static int digit (int c) { return '0' <= c && c <= '9'; } + +static int getnum (const char **fmt, int df) { + if (!digit(**fmt)) /* no number? */ + return df; /* return default value */ + else { + int a = 0; + do { + a = a*10 + (*((*fmt)++) - '0'); + } while (digit(**fmt) && a <= ((int)MAXSIZE - 9)/10); + return a; + } +} + + +/* +** Read an integer numeral and raises an error if it is larger +** than the maximum size for integers. +*/ +static int getnumlimit (Header *h, const char **fmt, int df) { + int sz = getnum(fmt, df); + if (sz > MAXINTSIZE || sz <= 0) + luaL_error(h->L, "integral size (%d) out of limits [1,%d]", + sz, MAXINTSIZE); + return sz; +} + + +/* +** Initialize Header +*/ +static void initheader (lua_State *L, Header *h) { + h->L = L; + h->islittle = nativeendian.little; + h->maxalign = 1; +} + + +/* +** Read and classify next option. 'size' is filled with option's size. +*/ +static KOption getoption (Header *h, const char **fmt, int *size) { + int opt = *((*fmt)++); + *size = 0; /* default */ + switch (opt) { + case 'b': *size = sizeof(char); return Kint; + case 'B': *size = sizeof(char); return Kuint; + case 'h': *size = sizeof(short); return Kint; + case 'H': *size = sizeof(short); return Kuint; + case 'l': *size = sizeof(long); return Kint; + case 'L': *size = sizeof(long); return Kuint; + case 'j': *size = sizeof(lua_Integer); return Kint; + case 'J': *size = sizeof(lua_Integer); return Kuint; + case 'T': *size = sizeof(size_t); return Kuint; + case 'f': *size = sizeof(float); return Kfloat; + case 'd': *size = sizeof(double); return Kfloat; + case 'n': *size = sizeof(lua_Number); return Kfloat; + case 'i': *size = getnumlimit(h, fmt, sizeof(int)); return Kint; + case 'I': *size = getnumlimit(h, fmt, sizeof(int)); return Kuint; + case 's': *size = getnumlimit(h, fmt, sizeof(size_t)); return Kstring; + case 'c': + *size = getnum(fmt, -1); + if (*size == -1) + luaL_error(h->L, "missing size for format option 'c'"); + return Kchar; + case 'z': return Kzstr; + case 'x': *size = 1; return Kpadding; + case 'X': return Kpaddalign; + case ' ': break; + case '<': h->islittle = 1; break; + case '>': h->islittle = 0; break; + case '=': h->islittle = nativeendian.little; break; + case '!': h->maxalign = getnumlimit(h, fmt, MAXALIGN); break; + default: luaL_error(h->L, "invalid format option '%c'", opt); + } + return Knop; +} + + +/* +** Read, classify, and fill other details about the next option. +** 'psize' is filled with option's size, 'notoalign' with its +** alignment requirements. +** Local variable 'size' gets the size to be aligned. (Kpadal option +** always gets its full alignment, other options are limited by +** the maximum alignment ('maxalign'). Kchar option needs no alignment +** despite its size. +*/ +static KOption getdetails (Header *h, size_t totalsize, + const char **fmt, int *psize, int *ntoalign) { + KOption opt = getoption(h, fmt, psize); + int align = *psize; /* usually, alignment follows size */ + if (opt == Kpaddalign) { /* 'X' gets alignment from following option */ + if (**fmt == '\0' || getoption(h, fmt, &align) == Kchar || align == 0) + luaL_argerror(h->L, 1, "invalid next option for option 'X'"); + } + if (align <= 1 || opt == Kchar) /* need no alignment? */ + *ntoalign = 0; + else { + if (align > h->maxalign) /* enforce maximum alignment */ + align = h->maxalign; + if ((align & (align - 1)) != 0) /* is 'align' not a power of 2? */ + luaL_argerror(h->L, 1, "format asks for alignment not power of 2"); + *ntoalign = (align - (int)(totalsize & (align - 1))) & (align - 1); + } + return opt; +} + + +/* +** Pack integer 'n' with 'size' bytes and 'islittle' endianness. +** The final 'if' handles the case when 'size' is larger than +** the size of a Lua integer, correcting the extra sign-extension +** bytes if necessary (by default they would be zeros). +*/ +static void packint (luaL_Buffer *b, lua_Unsigned n, + int islittle, int size, int neg) { + char *buff = luaL_prepbuffsize(b, size); + int i; + buff[islittle ? 0 : size - 1] = (char)(n & MC); /* first byte */ + for (i = 1; i < size; i++) { + n >>= NB; + buff[islittle ? i : size - 1 - i] = (char)(n & MC); + } + if (neg && size > SZINT) { /* negative number need sign extension? */ + for (i = SZINT; i < size; i++) /* correct extra bytes */ + buff[islittle ? i : size - 1 - i] = (char)MC; + } + luaL_addsize(b, size); /* add result to buffer */ +} + + +/* +** Copy 'size' bytes from 'src' to 'dest', correcting endianness if +** given 'islittle' is different from native endianness. +*/ +static void copywithendian (volatile char *dest, volatile const char *src, + int size, int islittle) { + if (islittle == nativeendian.little) { + while (size-- != 0) + *(dest++) = *(src++); + } + else { + dest += size - 1; + while (size-- != 0) + *(dest--) = *(src++); + } +} + + +static int str_pack (lua_State *L) { + luaL_Buffer b; + Header h; + const char *fmt = luaL_checkstring(L, 1); /* format string */ + int arg = 1; /* current argument to pack */ + size_t totalsize = 0; /* accumulate total size of result */ + initheader(L, &h); + lua_pushnil(L); /* mark to separate arguments from string buffer */ + luaL_buffinit(L, &b); + while (*fmt != '\0') { + int size, ntoalign; + KOption opt = getdetails(&h, totalsize, &fmt, &size, &ntoalign); + totalsize += ntoalign + size; + while (ntoalign-- > 0) + luaL_addchar(&b, LUAL_PACKPADBYTE); /* fill alignment */ + arg++; + switch (opt) { + case Kint: { /* signed integers */ + lua_Integer n = luaL_checkinteger(L, arg); + if (size < SZINT) { /* need overflow check? */ + lua_Integer lim = (lua_Integer)1 << ((size * NB) - 1); + luaL_argcheck(L, -lim <= n && n < lim, arg, "integer overflow"); + } + packint(&b, (lua_Unsigned)n, h.islittle, size, (n < 0)); + break; + } + case Kuint: { /* unsigned integers */ + lua_Integer n = luaL_checkinteger(L, arg); + if (size < SZINT) /* need overflow check? */ + luaL_argcheck(L, (lua_Unsigned)n < ((lua_Unsigned)1 << (size * NB)), + arg, "unsigned overflow"); + packint(&b, (lua_Unsigned)n, h.islittle, size, 0); + break; + } + case Kfloat: { /* floating-point options */ + volatile Ftypes u; + char *buff = luaL_prepbuffsize(&b, size); + lua_Number n = luaL_checknumber(L, arg); /* get argument */ + if (size == sizeof(u.f)) u.f = (float)n; /* copy it into 'u' */ + else if (size == sizeof(u.d)) u.d = (double)n; + else u.n = n; + /* move 'u' to final result, correcting endianness if needed */ + copywithendian(buff, u.buff, size, h.islittle); + luaL_addsize(&b, size); + break; + } + case Kchar: { /* fixed-size string */ + size_t len; + const char *s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, len <= (size_t)size, arg, + "string longer than given size"); + luaL_addlstring(&b, s, len); /* add string */ + while (len++ < (size_t)size) /* pad extra space */ + luaL_addchar(&b, LUAL_PACKPADBYTE); + break; + } + case Kstring: { /* strings with length count */ + size_t len; + const char *s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, size >= (int)sizeof(size_t) || + len < ((size_t)1 << (size * NB)), + arg, "string length does not fit in given size"); + packint(&b, (lua_Unsigned)len, h.islittle, size, 0); /* pack length */ + luaL_addlstring(&b, s, len); + totalsize += len; + break; + } + case Kzstr: { /* zero-terminated string */ + size_t len; + const char *s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, strlen(s) == len, arg, "string contains zeros"); + luaL_addlstring(&b, s, len); + luaL_addchar(&b, '\0'); /* add zero at the end */ + totalsize += len + 1; + break; + } + case Kpadding: luaL_addchar(&b, LUAL_PACKPADBYTE); /* FALLTHROUGH */ + case Kpaddalign: case Knop: + arg--; /* undo increment */ + break; + } + } + luaL_pushresult(&b); + return 1; +} + + +static int str_packsize (lua_State *L) { + Header h; + const char *fmt = luaL_checkstring(L, 1); /* format string */ + size_t totalsize = 0; /* accumulate total size of result */ + initheader(L, &h); + while (*fmt != '\0') { + int size, ntoalign; + KOption opt = getdetails(&h, totalsize, &fmt, &size, &ntoalign); + size += ntoalign; /* total space used by option */ + luaL_argcheck(L, totalsize <= MAXSIZE - size, 1, + "format result too large"); + totalsize += size; + switch (opt) { + case Kstring: /* strings with length count */ + case Kzstr: /* zero-terminated string */ + luaL_argerror(L, 1, "variable-length format"); + /* call never return, but to avoid warnings: *//* FALLTHROUGH */ + default: break; + } + } + lua_pushinteger(L, (lua_Integer)totalsize); + return 1; +} + + +/* +** Unpack an integer with 'size' bytes and 'islittle' endianness. +** If size is smaller than the size of a Lua integer and integer +** is signed, must do sign extension (propagating the sign to the +** higher bits); if size is larger than the size of a Lua integer, +** it must check the unread bytes to see whether they do not cause an +** overflow. +*/ +static lua_Integer unpackint (lua_State *L, const char *str, + int islittle, int size, int issigned) { + lua_Unsigned res = 0; + int i; + int limit = (size <= SZINT) ? size : SZINT; + for (i = limit - 1; i >= 0; i--) { + res <<= NB; + res |= (lua_Unsigned)(unsigned char)str[islittle ? i : size - 1 - i]; + } + if (size < SZINT) { /* real size smaller than lua_Integer? */ + if (issigned) { /* needs sign extension? */ + lua_Unsigned mask = (lua_Unsigned)1 << (size*NB - 1); + res = ((res ^ mask) - mask); /* do sign extension */ + } + } + else if (size > SZINT) { /* must check unread bytes */ + int mask = (!issigned || (lua_Integer)res >= 0) ? 0 : MC; + for (i = limit; i < size; i++) { + if ((unsigned char)str[islittle ? i : size - 1 - i] != mask) + luaL_error(L, "%d-byte integer does not fit into Lua Integer", size); + } + } + return (lua_Integer)res; +} + + +static int str_unpack (lua_State *L) { + Header h; + const char *fmt = luaL_checkstring(L, 1); + size_t ld; + const char *data = luaL_checklstring(L, 2, &ld); + size_t pos = (size_t)posrelat(luaL_optinteger(L, 3, 1), ld) - 1; + int n = 0; /* number of results */ + luaL_argcheck(L, pos <= ld, 3, "initial position out of string"); + initheader(L, &h); + while (*fmt != '\0') { + int size, ntoalign; + KOption opt = getdetails(&h, pos, &fmt, &size, &ntoalign); + if ((size_t)ntoalign + size > ~pos || pos + ntoalign + size > ld) + luaL_argerror(L, 2, "data string too short"); + pos += ntoalign; /* skip alignment */ + /* stack space for item + next position */ + luaL_checkstack(L, 2, "too many results"); + n++; + switch (opt) { + case Kint: + case Kuint: { + lua_Integer res = unpackint(L, data + pos, h.islittle, size, + (opt == Kint)); + lua_pushinteger(L, res); + break; + } + case Kfloat: { + volatile Ftypes u; + lua_Number num; + copywithendian(u.buff, data + pos, size, h.islittle); + if (size == sizeof(u.f)) num = (lua_Number)u.f; + else if (size == sizeof(u.d)) num = (lua_Number)u.d; + else num = u.n; + lua_pushnumber(L, num); + break; + } + case Kchar: { + lua_pushlstring(L, data + pos, size); + break; + } + case Kstring: { + size_t len = (size_t)unpackint(L, data + pos, h.islittle, size, 0); + luaL_argcheck(L, pos + len + size <= ld, 2, "data string too short"); + lua_pushlstring(L, data + pos + size, len); + pos += len; /* skip string */ + break; + } + case Kzstr: { + size_t len = (int)strlen(data + pos); + lua_pushlstring(L, data + pos, len); + pos += len + 1; /* skip string plus final '\0' */ + break; + } + case Kpaddalign: case Kpadding: case Knop: + n--; /* undo increment */ + break; + } + pos += size; + } + lua_pushinteger(L, pos + 1); /* next position */ + return n + 1; +} + +/* }====================================================== */ + + +static const luaL_Reg strlib[] = { + {"byte", str_byte}, + {"char", str_char}, + {"dump", str_dump}, + {"find", str_find}, + {"format", str_format}, + {"gmatch", gmatch}, + {"gsub", str_gsub}, + {"len", str_len}, + {"lower", str_lower}, + {"match", str_match}, + {"rep", str_rep}, + {"reverse", str_reverse}, + {"sub", str_sub}, + {"upper", str_upper}, + {"pack", str_pack}, + {"packsize", str_packsize}, + {"unpack", str_unpack}, + {NULL, NULL} +}; + + +static void createmetatable (lua_State *L) { + lua_createtable(L, 0, 1); /* table to be metatable for strings */ + lua_pushliteral(L, ""); /* dummy string */ + lua_pushvalue(L, -2); /* copy table */ + lua_setmetatable(L, -2); /* set table as metatable for strings */ + lua_pop(L, 1); /* pop dummy string */ + lua_pushvalue(L, -2); /* get string library */ + lua_setfield(L, -2, "__index"); /* metatable.__index = string */ + lua_pop(L, 1); /* pop metatable */ +} + + +/* +** Open string library +*/ +LUAMOD_API int luaopen_string (lua_State *L) { + luaL_newlib(L, strlib); + createmetatable(L); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/ltable.c b/src/rcheevos/test/lua/src/ltable.c new file mode 100644 index 000000000..d080189f2 --- /dev/null +++ b/src/rcheevos/test/lua/src/ltable.c @@ -0,0 +1,669 @@ +/* +** $Id: ltable.c,v 2.118 2016/11/07 12:38:35 roberto Exp $ +** Lua tables (hash) +** See Copyright Notice in lua.h +*/ + +#define ltable_c +#define LUA_CORE + +#include "lprefix.h" + + +/* +** Implementation of tables (aka arrays, objects, or hash tables). +** Tables keep its elements in two parts: an array part and a hash part. +** Non-negative integer keys are all candidates to be kept in the array +** part. The actual size of the array is the largest 'n' such that +** more than half the slots between 1 and n are in use. +** Hash uses a mix of chained scatter table with Brent's variation. +** A main invariant of these tables is that, if an element is not +** in its main position (i.e. the 'original' position that its hash gives +** to it), then the colliding element is in its own main position. +** Hence even when the load factor reaches 100%, performance remains good. +*/ + +#include +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lvm.h" + + +/* +** Maximum size of array part (MAXASIZE) is 2^MAXABITS. MAXABITS is +** the largest integer such that MAXASIZE fits in an unsigned int. +*/ +#define MAXABITS cast_int(sizeof(int) * CHAR_BIT - 1) +#define MAXASIZE (1u << MAXABITS) + +/* +** Maximum size of hash part is 2^MAXHBITS. MAXHBITS is the largest +** integer such that 2^MAXHBITS fits in a signed int. (Note that the +** maximum number of elements in a table, 2^MAXABITS + 2^MAXHBITS, still +** fits comfortably in an unsigned int.) +*/ +#define MAXHBITS (MAXABITS - 1) + + +#define hashpow2(t,n) (gnode(t, lmod((n), sizenode(t)))) + +#define hashstr(t,str) hashpow2(t, (str)->hash) +#define hashboolean(t,p) hashpow2(t, p) +#define hashint(t,i) hashpow2(t, i) + + +/* +** for some types, it is better to avoid modulus by power of 2, as +** they tend to have many 2 factors. +*/ +#define hashmod(t,n) (gnode(t, ((n) % ((sizenode(t)-1)|1)))) + + +#define hashpointer(t,p) hashmod(t, point2uint(p)) + + +#define dummynode (&dummynode_) + +static const Node dummynode_ = { + {NILCONSTANT}, /* value */ + {{NILCONSTANT, 0}} /* key */ +}; + + +/* +** Hash for floating-point numbers. +** The main computation should be just +** n = frexp(n, &i); return (n * INT_MAX) + i +** but there are some numerical subtleties. +** In a two-complement representation, INT_MAX does not has an exact +** representation as a float, but INT_MIN does; because the absolute +** value of 'frexp' is smaller than 1 (unless 'n' is inf/NaN), the +** absolute value of the product 'frexp * -INT_MIN' is smaller or equal +** to INT_MAX. Next, the use of 'unsigned int' avoids overflows when +** adding 'i'; the use of '~u' (instead of '-u') avoids problems with +** INT_MIN. +*/ +#if !defined(l_hashfloat) +static int l_hashfloat (lua_Number n) { + int i; + lua_Integer ni; + n = l_mathop(frexp)(n, &i) * -cast_num(INT_MIN); + if (!lua_numbertointeger(n, &ni)) { /* is 'n' inf/-inf/NaN? */ + lua_assert(luai_numisnan(n) || l_mathop(fabs)(n) == cast_num(HUGE_VAL)); + return 0; + } + else { /* normal case */ + unsigned int u = cast(unsigned int, i) + cast(unsigned int, ni); + return cast_int(u <= cast(unsigned int, INT_MAX) ? u : ~u); + } +} +#endif + + +/* +** returns the 'main' position of an element in a table (that is, the index +** of its hash value) +*/ +static Node *mainposition (const Table *t, const TValue *key) { + switch (ttype(key)) { + case LUA_TNUMINT: + return hashint(t, ivalue(key)); + case LUA_TNUMFLT: + return hashmod(t, l_hashfloat(fltvalue(key))); + case LUA_TSHRSTR: + return hashstr(t, tsvalue(key)); + case LUA_TLNGSTR: + return hashpow2(t, luaS_hashlongstr(tsvalue(key))); + case LUA_TBOOLEAN: + return hashboolean(t, bvalue(key)); + case LUA_TLIGHTUSERDATA: + return hashpointer(t, pvalue(key)); + case LUA_TLCF: + return hashpointer(t, fvalue(key)); + default: + lua_assert(!ttisdeadkey(key)); + return hashpointer(t, gcvalue(key)); + } +} + + +/* +** returns the index for 'key' if 'key' is an appropriate key to live in +** the array part of the table, 0 otherwise. +*/ +static unsigned int arrayindex (const TValue *key) { + if (ttisinteger(key)) { + lua_Integer k = ivalue(key); + if (0 < k && (lua_Unsigned)k <= MAXASIZE) + return cast(unsigned int, k); /* 'key' is an appropriate array index */ + } + return 0; /* 'key' did not match some condition */ +} + + +/* +** returns the index of a 'key' for table traversals. First goes all +** elements in the array part, then elements in the hash part. The +** beginning of a traversal is signaled by 0. +*/ +static unsigned int findindex (lua_State *L, Table *t, StkId key) { + unsigned int i; + if (ttisnil(key)) return 0; /* first iteration */ + i = arrayindex(key); + if (i != 0 && i <= t->sizearray) /* is 'key' inside array part? */ + return i; /* yes; that's the index */ + else { + int nx; + Node *n = mainposition(t, key); + for (;;) { /* check whether 'key' is somewhere in the chain */ + /* key may be dead already, but it is ok to use it in 'next' */ + if (luaV_rawequalobj(gkey(n), key) || + (ttisdeadkey(gkey(n)) && iscollectable(key) && + deadvalue(gkey(n)) == gcvalue(key))) { + i = cast_int(n - gnode(t, 0)); /* key index in hash table */ + /* hash elements are numbered after array ones */ + return (i + 1) + t->sizearray; + } + nx = gnext(n); + if (nx == 0) + luaG_runerror(L, "invalid key to 'next'"); /* key not found */ + else n += nx; + } + } +} + + +int luaH_next (lua_State *L, Table *t, StkId key) { + unsigned int i = findindex(L, t, key); /* find original element */ + for (; i < t->sizearray; i++) { /* try first array part */ + if (!ttisnil(&t->array[i])) { /* a non-nil value? */ + setivalue(key, i + 1); + setobj2s(L, key+1, &t->array[i]); + return 1; + } + } + for (i -= t->sizearray; cast_int(i) < sizenode(t); i++) { /* hash part */ + if (!ttisnil(gval(gnode(t, i)))) { /* a non-nil value? */ + setobj2s(L, key, gkey(gnode(t, i))); + setobj2s(L, key+1, gval(gnode(t, i))); + return 1; + } + } + return 0; /* no more elements */ +} + + +/* +** {============================================================= +** Rehash +** ============================================================== +*/ + +/* +** Compute the optimal size for the array part of table 't'. 'nums' is a +** "count array" where 'nums[i]' is the number of integers in the table +** between 2^(i - 1) + 1 and 2^i. 'pna' enters with the total number of +** integer keys in the table and leaves with the number of keys that +** will go to the array part; return the optimal size. +*/ +static unsigned int computesizes (unsigned int nums[], unsigned int *pna) { + int i; + unsigned int twotoi; /* 2^i (candidate for optimal size) */ + unsigned int a = 0; /* number of elements smaller than 2^i */ + unsigned int na = 0; /* number of elements to go to array part */ + unsigned int optimal = 0; /* optimal size for array part */ + /* loop while keys can fill more than half of total size */ + for (i = 0, twotoi = 1; *pna > twotoi / 2; i++, twotoi *= 2) { + if (nums[i] > 0) { + a += nums[i]; + if (a > twotoi/2) { /* more than half elements present? */ + optimal = twotoi; /* optimal size (till now) */ + na = a; /* all elements up to 'optimal' will go to array part */ + } + } + } + lua_assert((optimal == 0 || optimal / 2 < na) && na <= optimal); + *pna = na; + return optimal; +} + + +static int countint (const TValue *key, unsigned int *nums) { + unsigned int k = arrayindex(key); + if (k != 0) { /* is 'key' an appropriate array index? */ + nums[luaO_ceillog2(k)]++; /* count as such */ + return 1; + } + else + return 0; +} + + +/* +** Count keys in array part of table 't': Fill 'nums[i]' with +** number of keys that will go into corresponding slice and return +** total number of non-nil keys. +*/ +static unsigned int numusearray (const Table *t, unsigned int *nums) { + int lg; + unsigned int ttlg; /* 2^lg */ + unsigned int ause = 0; /* summation of 'nums' */ + unsigned int i = 1; /* count to traverse all array keys */ + /* traverse each slice */ + for (lg = 0, ttlg = 1; lg <= MAXABITS; lg++, ttlg *= 2) { + unsigned int lc = 0; /* counter */ + unsigned int lim = ttlg; + if (lim > t->sizearray) { + lim = t->sizearray; /* adjust upper limit */ + if (i > lim) + break; /* no more elements to count */ + } + /* count elements in range (2^(lg - 1), 2^lg] */ + for (; i <= lim; i++) { + if (!ttisnil(&t->array[i-1])) + lc++; + } + nums[lg] += lc; + ause += lc; + } + return ause; +} + + +static int numusehash (const Table *t, unsigned int *nums, unsigned int *pna) { + int totaluse = 0; /* total number of elements */ + int ause = 0; /* elements added to 'nums' (can go to array part) */ + int i = sizenode(t); + while (i--) { + Node *n = &t->node[i]; + if (!ttisnil(gval(n))) { + ause += countint(gkey(n), nums); + totaluse++; + } + } + *pna += ause; + return totaluse; +} + + +static void setarrayvector (lua_State *L, Table *t, unsigned int size) { + unsigned int i; + luaM_reallocvector(L, t->array, t->sizearray, size, TValue); + for (i=t->sizearray; iarray[i]); + t->sizearray = size; +} + + +static void setnodevector (lua_State *L, Table *t, unsigned int size) { + if (size == 0) { /* no elements to hash part? */ + t->node = cast(Node *, dummynode); /* use common 'dummynode' */ + t->lsizenode = 0; + t->lastfree = NULL; /* signal that it is using dummy node */ + } + else { + int i; + int lsize = luaO_ceillog2(size); + if (lsize > MAXHBITS) + luaG_runerror(L, "table overflow"); + size = twoto(lsize); + t->node = luaM_newvector(L, size, Node); + for (i = 0; i < (int)size; i++) { + Node *n = gnode(t, i); + gnext(n) = 0; + setnilvalue(wgkey(n)); + setnilvalue(gval(n)); + } + t->lsizenode = cast_byte(lsize); + t->lastfree = gnode(t, size); /* all positions are free */ + } +} + + +void luaH_resize (lua_State *L, Table *t, unsigned int nasize, + unsigned int nhsize) { + unsigned int i; + int j; + unsigned int oldasize = t->sizearray; + int oldhsize = allocsizenode(t); + Node *nold = t->node; /* save old hash ... */ + if (nasize > oldasize) /* array part must grow? */ + setarrayvector(L, t, nasize); + /* create new hash part with appropriate size */ + setnodevector(L, t, nhsize); + if (nasize < oldasize) { /* array part must shrink? */ + t->sizearray = nasize; + /* re-insert elements from vanishing slice */ + for (i=nasize; iarray[i])) + luaH_setint(L, t, i + 1, &t->array[i]); + } + /* shrink array */ + luaM_reallocvector(L, t->array, oldasize, nasize, TValue); + } + /* re-insert elements from hash part */ + for (j = oldhsize - 1; j >= 0; j--) { + Node *old = nold + j; + if (!ttisnil(gval(old))) { + /* doesn't need barrier/invalidate cache, as entry was + already present in the table */ + setobjt2t(L, luaH_set(L, t, gkey(old)), gval(old)); + } + } + if (oldhsize > 0) /* not the dummy node? */ + luaM_freearray(L, nold, cast(size_t, oldhsize)); /* free old hash */ +} + + +void luaH_resizearray (lua_State *L, Table *t, unsigned int nasize) { + int nsize = allocsizenode(t); + luaH_resize(L, t, nasize, nsize); +} + +/* +** nums[i] = number of keys 'k' where 2^(i - 1) < k <= 2^i +*/ +static void rehash (lua_State *L, Table *t, const TValue *ek) { + unsigned int asize; /* optimal size for array part */ + unsigned int na; /* number of keys in the array part */ + unsigned int nums[MAXABITS + 1]; + int i; + int totaluse; + for (i = 0; i <= MAXABITS; i++) nums[i] = 0; /* reset counts */ + na = numusearray(t, nums); /* count keys in array part */ + totaluse = na; /* all those keys are integer keys */ + totaluse += numusehash(t, nums, &na); /* count keys in hash part */ + /* count extra key */ + na += countint(ek, nums); + totaluse++; + /* compute new size for array part */ + asize = computesizes(nums, &na); + /* resize the table to new computed sizes */ + luaH_resize(L, t, asize, totaluse - na); +} + + + +/* +** }============================================================= +*/ + + +Table *luaH_new (lua_State *L) { + GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table)); + Table *t = gco2t(o); + t->metatable = NULL; + t->flags = cast_byte(~0); + t->array = NULL; + t->sizearray = 0; + setnodevector(L, t, 0); + return t; +} + + +void luaH_free (lua_State *L, Table *t) { + if (!isdummy(t)) + luaM_freearray(L, t->node, cast(size_t, sizenode(t))); + luaM_freearray(L, t->array, t->sizearray); + luaM_free(L, t); +} + + +static Node *getfreepos (Table *t) { + if (!isdummy(t)) { + while (t->lastfree > t->node) { + t->lastfree--; + if (ttisnil(gkey(t->lastfree))) + return t->lastfree; + } + } + return NULL; /* could not find a free place */ +} + + + +/* +** inserts a new key into a hash table; first, check whether key's main +** position is free. If not, check whether colliding node is in its main +** position or not: if it is not, move colliding node to an empty place and +** put new key in its main position; otherwise (colliding node is in its main +** position), new key goes to an empty position. +*/ +TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) { + Node *mp; + TValue aux; + if (ttisnil(key)) luaG_runerror(L, "table index is nil"); + else if (ttisfloat(key)) { + lua_Integer k; + if (luaV_tointeger(key, &k, 0)) { /* does index fit in an integer? */ + setivalue(&aux, k); + key = &aux; /* insert it as an integer */ + } + else if (luai_numisnan(fltvalue(key))) + luaG_runerror(L, "table index is NaN"); + } + mp = mainposition(t, key); + if (!ttisnil(gval(mp)) || isdummy(t)) { /* main position is taken? */ + Node *othern; + Node *f = getfreepos(t); /* get a free place */ + if (f == NULL) { /* cannot find a free place? */ + rehash(L, t, key); /* grow table */ + /* whatever called 'newkey' takes care of TM cache */ + return luaH_set(L, t, key); /* insert key into grown table */ + } + lua_assert(!isdummy(t)); + othern = mainposition(t, gkey(mp)); + if (othern != mp) { /* is colliding node out of its main position? */ + /* yes; move colliding node into free position */ + while (othern + gnext(othern) != mp) /* find previous */ + othern += gnext(othern); + gnext(othern) = cast_int(f - othern); /* rechain to point to 'f' */ + *f = *mp; /* copy colliding node into free pos. (mp->next also goes) */ + if (gnext(mp) != 0) { + gnext(f) += cast_int(mp - f); /* correct 'next' */ + gnext(mp) = 0; /* now 'mp' is free */ + } + setnilvalue(gval(mp)); + } + else { /* colliding node is in its own main position */ + /* new node will go into free position */ + if (gnext(mp) != 0) + gnext(f) = cast_int((mp + gnext(mp)) - f); /* chain new position */ + else lua_assert(gnext(f) == 0); + gnext(mp) = cast_int(f - mp); + mp = f; + } + } + setnodekey(L, &mp->i_key, key); + luaC_barrierback(L, t, key); + lua_assert(ttisnil(gval(mp))); + return gval(mp); +} + + +/* +** search function for integers +*/ +const TValue *luaH_getint (Table *t, lua_Integer key) { + /* (1 <= key && key <= t->sizearray) */ + if (l_castS2U(key) - 1 < t->sizearray) + return &t->array[key - 1]; + else { + Node *n = hashint(t, key); + for (;;) { /* check whether 'key' is somewhere in the chain */ + if (ttisinteger(gkey(n)) && ivalue(gkey(n)) == key) + return gval(n); /* that's it */ + else { + int nx = gnext(n); + if (nx == 0) break; + n += nx; + } + } + return luaO_nilobject; + } +} + + +/* +** search function for short strings +*/ +const TValue *luaH_getshortstr (Table *t, TString *key) { + Node *n = hashstr(t, key); + lua_assert(key->tt == LUA_TSHRSTR); + for (;;) { /* check whether 'key' is somewhere in the chain */ + const TValue *k = gkey(n); + if (ttisshrstring(k) && eqshrstr(tsvalue(k), key)) + return gval(n); /* that's it */ + else { + int nx = gnext(n); + if (nx == 0) + return luaO_nilobject; /* not found */ + n += nx; + } + } +} + + +/* +** "Generic" get version. (Not that generic: not valid for integers, +** which may be in array part, nor for floats with integral values.) +*/ +static const TValue *getgeneric (Table *t, const TValue *key) { + Node *n = mainposition(t, key); + for (;;) { /* check whether 'key' is somewhere in the chain */ + if (luaV_rawequalobj(gkey(n), key)) + return gval(n); /* that's it */ + else { + int nx = gnext(n); + if (nx == 0) + return luaO_nilobject; /* not found */ + n += nx; + } + } +} + + +const TValue *luaH_getstr (Table *t, TString *key) { + if (key->tt == LUA_TSHRSTR) + return luaH_getshortstr(t, key); + else { /* for long strings, use generic case */ + TValue ko; + setsvalue(cast(lua_State *, NULL), &ko, key); + return getgeneric(t, &ko); + } +} + + +/* +** main search function +*/ +const TValue *luaH_get (Table *t, const TValue *key) { + switch (ttype(key)) { + case LUA_TSHRSTR: return luaH_getshortstr(t, tsvalue(key)); + case LUA_TNUMINT: return luaH_getint(t, ivalue(key)); + case LUA_TNIL: return luaO_nilobject; + case LUA_TNUMFLT: { + lua_Integer k; + if (luaV_tointeger(key, &k, 0)) /* index is int? */ + return luaH_getint(t, k); /* use specialized version */ + /* else... */ + } /* FALLTHROUGH */ + default: + return getgeneric(t, key); + } +} + + +/* +** beware: when using this function you probably need to check a GC +** barrier and invalidate the TM cache. +*/ +TValue *luaH_set (lua_State *L, Table *t, const TValue *key) { + const TValue *p = luaH_get(t, key); + if (p != luaO_nilobject) + return cast(TValue *, p); + else return luaH_newkey(L, t, key); +} + + +void luaH_setint (lua_State *L, Table *t, lua_Integer key, TValue *value) { + const TValue *p = luaH_getint(t, key); + TValue *cell; + if (p != luaO_nilobject) + cell = cast(TValue *, p); + else { + TValue k; + setivalue(&k, key); + cell = luaH_newkey(L, t, &k); + } + setobj2t(L, cell, value); +} + + +static int unbound_search (Table *t, unsigned int j) { + unsigned int i = j; /* i is zero or a present index */ + j++; + /* find 'i' and 'j' such that i is present and j is not */ + while (!ttisnil(luaH_getint(t, j))) { + i = j; + if (j > cast(unsigned int, MAX_INT)/2) { /* overflow? */ + /* table was built with bad purposes: resort to linear search */ + i = 1; + while (!ttisnil(luaH_getint(t, i))) i++; + return i - 1; + } + j *= 2; + } + /* now do a binary search between them */ + while (j - i > 1) { + unsigned int m = (i+j)/2; + if (ttisnil(luaH_getint(t, m))) j = m; + else i = m; + } + return i; +} + + +/* +** Try to find a boundary in table 't'. A 'boundary' is an integer index +** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil). +*/ +int luaH_getn (Table *t) { + unsigned int j = t->sizearray; + if (j > 0 && ttisnil(&t->array[j - 1])) { + /* there is a boundary in the array part: (binary) search for it */ + unsigned int i = 0; + while (j - i > 1) { + unsigned int m = (i+j)/2; + if (ttisnil(&t->array[m - 1])) j = m; + else i = m; + } + return i; + } + /* else must find a boundary in hash part */ + else if (isdummy(t)) /* hash part is empty? */ + return j; /* that is easy... */ + else return unbound_search(t, j); +} + + + +#if defined(LUA_DEBUG) + +Node *luaH_mainposition (const Table *t, const TValue *key) { + return mainposition(t, key); +} + +int luaH_isdummy (const Table *t) { return isdummy(t); } + +#endif diff --git a/src/rcheevos/test/lua/src/ltable.h b/src/rcheevos/test/lua/src/ltable.h new file mode 100644 index 000000000..6da9024fe --- /dev/null +++ b/src/rcheevos/test/lua/src/ltable.h @@ -0,0 +1,66 @@ +/* +** $Id: ltable.h,v 2.23 2016/12/22 13:08:50 roberto Exp $ +** Lua tables (hash) +** See Copyright Notice in lua.h +*/ + +#ifndef ltable_h +#define ltable_h + +#include "lobject.h" + + +#define gnode(t,i) (&(t)->node[i]) +#define gval(n) (&(n)->i_val) +#define gnext(n) ((n)->i_key.nk.next) + + +/* 'const' to avoid wrong writings that can mess up field 'next' */ +#define gkey(n) cast(const TValue*, (&(n)->i_key.tvk)) + +/* +** writable version of 'gkey'; allows updates to individual fields, +** but not to the whole (which has incompatible type) +*/ +#define wgkey(n) (&(n)->i_key.nk) + +#define invalidateTMcache(t) ((t)->flags = 0) + + +/* true when 't' is using 'dummynode' as its hash part */ +#define isdummy(t) ((t)->lastfree == NULL) + + +/* allocated size for hash nodes */ +#define allocsizenode(t) (isdummy(t) ? 0 : sizenode(t)) + + +/* returns the key, given the value of a table entry */ +#define keyfromval(v) \ + (gkey(cast(Node *, cast(char *, (v)) - offsetof(Node, i_val)))) + + +LUAI_FUNC const TValue *luaH_getint (Table *t, lua_Integer key); +LUAI_FUNC void luaH_setint (lua_State *L, Table *t, lua_Integer key, + TValue *value); +LUAI_FUNC const TValue *luaH_getshortstr (Table *t, TString *key); +LUAI_FUNC const TValue *luaH_getstr (Table *t, TString *key); +LUAI_FUNC const TValue *luaH_get (Table *t, const TValue *key); +LUAI_FUNC TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key); +LUAI_FUNC TValue *luaH_set (lua_State *L, Table *t, const TValue *key); +LUAI_FUNC Table *luaH_new (lua_State *L); +LUAI_FUNC void luaH_resize (lua_State *L, Table *t, unsigned int nasize, + unsigned int nhsize); +LUAI_FUNC void luaH_resizearray (lua_State *L, Table *t, unsigned int nasize); +LUAI_FUNC void luaH_free (lua_State *L, Table *t); +LUAI_FUNC int luaH_next (lua_State *L, Table *t, StkId key); +LUAI_FUNC int luaH_getn (Table *t); + + +#if defined(LUA_DEBUG) +LUAI_FUNC Node *luaH_mainposition (const Table *t, const TValue *key); +LUAI_FUNC int luaH_isdummy (const Table *t); +#endif + + +#endif diff --git a/src/rcheevos/test/lua/src/ltablib.c b/src/rcheevos/test/lua/src/ltablib.c new file mode 100644 index 000000000..98b2f8713 --- /dev/null +++ b/src/rcheevos/test/lua/src/ltablib.c @@ -0,0 +1,450 @@ +/* +** $Id: ltablib.c,v 1.93 2016/02/25 19:41:54 roberto Exp $ +** Library for Table Manipulation +** See Copyright Notice in lua.h +*/ + +#define ltablib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* +** Operations that an object must define to mimic a table +** (some functions only need some of them) +*/ +#define TAB_R 1 /* read */ +#define TAB_W 2 /* write */ +#define TAB_L 4 /* length */ +#define TAB_RW (TAB_R | TAB_W) /* read/write */ + + +#define aux_getn(L,n,w) (checktab(L, n, (w) | TAB_L), luaL_len(L, n)) + + +static int checkfield (lua_State *L, const char *key, int n) { + lua_pushstring(L, key); + return (lua_rawget(L, -n) != LUA_TNIL); +} + + +/* +** Check that 'arg' either is a table or can behave like one (that is, +** has a metatable with the required metamethods) +*/ +static void checktab (lua_State *L, int arg, int what) { + if (lua_type(L, arg) != LUA_TTABLE) { /* is it not a table? */ + int n = 1; /* number of elements to pop */ + if (lua_getmetatable(L, arg) && /* must have metatable */ + (!(what & TAB_R) || checkfield(L, "__index", ++n)) && + (!(what & TAB_W) || checkfield(L, "__newindex", ++n)) && + (!(what & TAB_L) || checkfield(L, "__len", ++n))) { + lua_pop(L, n); /* pop metatable and tested metamethods */ + } + else + luaL_checktype(L, arg, LUA_TTABLE); /* force an error */ + } +} + + +#if defined(LUA_COMPAT_MAXN) +static int maxn (lua_State *L) { + lua_Number max = 0; + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushnil(L); /* first key */ + while (lua_next(L, 1)) { + lua_pop(L, 1); /* remove value */ + if (lua_type(L, -1) == LUA_TNUMBER) { + lua_Number v = lua_tonumber(L, -1); + if (v > max) max = v; + } + } + lua_pushnumber(L, max); + return 1; +} +#endif + + +static int tinsert (lua_State *L) { + lua_Integer e = aux_getn(L, 1, TAB_RW) + 1; /* first empty element */ + lua_Integer pos; /* where to insert new element */ + switch (lua_gettop(L)) { + case 2: { /* called with only 2 arguments */ + pos = e; /* insert new element at the end */ + break; + } + case 3: { + lua_Integer i; + pos = luaL_checkinteger(L, 2); /* 2nd argument is the position */ + luaL_argcheck(L, 1 <= pos && pos <= e, 2, "position out of bounds"); + for (i = e; i > pos; i--) { /* move up elements */ + lua_geti(L, 1, i - 1); + lua_seti(L, 1, i); /* t[i] = t[i - 1] */ + } + break; + } + default: { + return luaL_error(L, "wrong number of arguments to 'insert'"); + } + } + lua_seti(L, 1, pos); /* t[pos] = v */ + return 0; +} + + +static int tremove (lua_State *L) { + lua_Integer size = aux_getn(L, 1, TAB_RW); + lua_Integer pos = luaL_optinteger(L, 2, size); + if (pos != size) /* validate 'pos' if given */ + luaL_argcheck(L, 1 <= pos && pos <= size + 1, 1, "position out of bounds"); + lua_geti(L, 1, pos); /* result = t[pos] */ + for ( ; pos < size; pos++) { + lua_geti(L, 1, pos + 1); + lua_seti(L, 1, pos); /* t[pos] = t[pos + 1] */ + } + lua_pushnil(L); + lua_seti(L, 1, pos); /* t[pos] = nil */ + return 1; +} + + +/* +** Copy elements (1[f], ..., 1[e]) into (tt[t], tt[t+1], ...). Whenever +** possible, copy in increasing order, which is better for rehashing. +** "possible" means destination after original range, or smaller +** than origin, or copying to another table. +*/ +static int tmove (lua_State *L) { + lua_Integer f = luaL_checkinteger(L, 2); + lua_Integer e = luaL_checkinteger(L, 3); + lua_Integer t = luaL_checkinteger(L, 4); + int tt = !lua_isnoneornil(L, 5) ? 5 : 1; /* destination table */ + checktab(L, 1, TAB_R); + checktab(L, tt, TAB_W); + if (e >= f) { /* otherwise, nothing to move */ + lua_Integer n, i; + luaL_argcheck(L, f > 0 || e < LUA_MAXINTEGER + f, 3, + "too many elements to move"); + n = e - f + 1; /* number of elements to move */ + luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, + "destination wrap around"); + if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + for (i = 0; i < n; i++) { + lua_geti(L, 1, f + i); + lua_seti(L, tt, t + i); + } + } + else { + for (i = n - 1; i >= 0; i--) { + lua_geti(L, 1, f + i); + lua_seti(L, tt, t + i); + } + } + } + lua_pushvalue(L, tt); /* return destination table */ + return 1; +} + + +static void addfield (lua_State *L, luaL_Buffer *b, lua_Integer i) { + lua_geti(L, 1, i); + if (!lua_isstring(L, -1)) + luaL_error(L, "invalid value (%s) at index %d in table for 'concat'", + luaL_typename(L, -1), i); + luaL_addvalue(b); +} + + +static int tconcat (lua_State *L) { + luaL_Buffer b; + lua_Integer last = aux_getn(L, 1, TAB_R); + size_t lsep; + const char *sep = luaL_optlstring(L, 2, "", &lsep); + lua_Integer i = luaL_optinteger(L, 3, 1); + last = luaL_optinteger(L, 4, last); + luaL_buffinit(L, &b); + for (; i < last; i++) { + addfield(L, &b, i); + luaL_addlstring(&b, sep, lsep); + } + if (i == last) /* add last value (if interval was not empty) */ + addfield(L, &b, i); + luaL_pushresult(&b); + return 1; +} + + +/* +** {====================================================== +** Pack/unpack +** ======================================================= +*/ + +static int pack (lua_State *L) { + int i; + int n = lua_gettop(L); /* number of elements to pack */ + lua_createtable(L, n, 1); /* create result table */ + lua_insert(L, 1); /* put it at index 1 */ + for (i = n; i >= 1; i--) /* assign elements */ + lua_seti(L, 1, i); + lua_pushinteger(L, n); + lua_setfield(L, 1, "n"); /* t.n = number of elements */ + return 1; /* return table */ +} + + +static int unpack (lua_State *L) { + lua_Unsigned n; + lua_Integer i = luaL_optinteger(L, 2, 1); + lua_Integer e = luaL_opt(L, luaL_checkinteger, 3, luaL_len(L, 1)); + if (i > e) return 0; /* empty range */ + n = (lua_Unsigned)e - i; /* number of elements minus 1 (avoid overflows) */ + if (n >= (unsigned int)INT_MAX || !lua_checkstack(L, (int)(++n))) + return luaL_error(L, "too many results to unpack"); + for (; i < e; i++) { /* push arg[i..e - 1] (to avoid overflows) */ + lua_geti(L, 1, i); + } + lua_geti(L, 1, e); /* push last element */ + return (int)n; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** Quicksort +** (based on 'Algorithms in MODULA-3', Robert Sedgewick; +** Addison-Wesley, 1993.) +** ======================================================= +*/ + + +/* type for array indices */ +typedef unsigned int IdxT; + + +/* +** Produce a "random" 'unsigned int' to randomize pivot choice. This +** macro is used only when 'sort' detects a big imbalance in the result +** of a partition. (If you don't want/need this "randomness", ~0 is a +** good choice.) +*/ +#if !defined(l_randomizePivot) /* { */ + +#include + +/* size of 'e' measured in number of 'unsigned int's */ +#define sof(e) (sizeof(e) / sizeof(unsigned int)) + +/* +** Use 'time' and 'clock' as sources of "randomness". Because we don't +** know the types 'clock_t' and 'time_t', we cannot cast them to +** anything without risking overflows. A safe way to use their values +** is to copy them to an array of a known type and use the array values. +*/ +static unsigned int l_randomizePivot (void) { + clock_t c = clock(); + time_t t = time(NULL); + unsigned int buff[sof(c) + sof(t)]; + unsigned int i, rnd = 0; + memcpy(buff, &c, sof(c) * sizeof(unsigned int)); + memcpy(buff + sof(c), &t, sof(t) * sizeof(unsigned int)); + for (i = 0; i < sof(buff); i++) + rnd += buff[i]; + return rnd; +} + +#endif /* } */ + + +/* arrays larger than 'RANLIMIT' may use randomized pivots */ +#define RANLIMIT 100u + + +static void set2 (lua_State *L, IdxT i, IdxT j) { + lua_seti(L, 1, i); + lua_seti(L, 1, j); +} + + +/* +** Return true iff value at stack index 'a' is less than the value at +** index 'b' (according to the order of the sort). +*/ +static int sort_comp (lua_State *L, int a, int b) { + if (lua_isnil(L, 2)) /* no function? */ + return lua_compare(L, a, b, LUA_OPLT); /* a < b */ + else { /* function */ + int res; + lua_pushvalue(L, 2); /* push function */ + lua_pushvalue(L, a-1); /* -1 to compensate function */ + lua_pushvalue(L, b-2); /* -2 to compensate function and 'a' */ + lua_call(L, 2, 1); /* call function */ + res = lua_toboolean(L, -1); /* get result */ + lua_pop(L, 1); /* pop result */ + return res; + } +} + + +/* +** Does the partition: Pivot P is at the top of the stack. +** precondition: a[lo] <= P == a[up-1] <= a[up], +** so it only needs to do the partition from lo + 1 to up - 2. +** Pos-condition: a[lo .. i - 1] <= a[i] == P <= a[i + 1 .. up] +** returns 'i'. +*/ +static IdxT partition (lua_State *L, IdxT lo, IdxT up) { + IdxT i = lo; /* will be incremented before first use */ + IdxT j = up - 1; /* will be decremented before first use */ + /* loop invariant: a[lo .. i] <= P <= a[j .. up] */ + for (;;) { + /* next loop: repeat ++i while a[i] < P */ + while (lua_geti(L, 1, ++i), sort_comp(L, -1, -2)) { + if (i == up - 1) /* a[i] < P but a[up - 1] == P ?? */ + luaL_error(L, "invalid order function for sorting"); + lua_pop(L, 1); /* remove a[i] */ + } + /* after the loop, a[i] >= P and a[lo .. i - 1] < P */ + /* next loop: repeat --j while P < a[j] */ + while (lua_geti(L, 1, --j), sort_comp(L, -3, -1)) { + if (j < i) /* j < i but a[j] > P ?? */ + luaL_error(L, "invalid order function for sorting"); + lua_pop(L, 1); /* remove a[j] */ + } + /* after the loop, a[j] <= P and a[j + 1 .. up] >= P */ + if (j < i) { /* no elements out of place? */ + /* a[lo .. i - 1] <= P <= a[j + 1 .. i .. up] */ + lua_pop(L, 1); /* pop a[j] */ + /* swap pivot (a[up - 1]) with a[i] to satisfy pos-condition */ + set2(L, up - 1, i); + return i; + } + /* otherwise, swap a[i] - a[j] to restore invariant and repeat */ + set2(L, i, j); + } +} + + +/* +** Choose an element in the middle (2nd-3th quarters) of [lo,up] +** "randomized" by 'rnd' +*/ +static IdxT choosePivot (IdxT lo, IdxT up, unsigned int rnd) { + IdxT r4 = (up - lo) / 4; /* range/4 */ + IdxT p = rnd % (r4 * 2) + (lo + r4); + lua_assert(lo + r4 <= p && p <= up - r4); + return p; +} + + +/* +** QuickSort algorithm (recursive function) +*/ +static void auxsort (lua_State *L, IdxT lo, IdxT up, + unsigned int rnd) { + while (lo < up) { /* loop for tail recursion */ + IdxT p; /* Pivot index */ + IdxT n; /* to be used later */ + /* sort elements 'lo', 'p', and 'up' */ + lua_geti(L, 1, lo); + lua_geti(L, 1, up); + if (sort_comp(L, -1, -2)) /* a[up] < a[lo]? */ + set2(L, lo, up); /* swap a[lo] - a[up] */ + else + lua_pop(L, 2); /* remove both values */ + if (up - lo == 1) /* only 2 elements? */ + return; /* already sorted */ + if (up - lo < RANLIMIT || rnd == 0) /* small interval or no randomize? */ + p = (lo + up)/2; /* middle element is a good pivot */ + else /* for larger intervals, it is worth a random pivot */ + p = choosePivot(lo, up, rnd); + lua_geti(L, 1, p); + lua_geti(L, 1, lo); + if (sort_comp(L, -2, -1)) /* a[p] < a[lo]? */ + set2(L, p, lo); /* swap a[p] - a[lo] */ + else { + lua_pop(L, 1); /* remove a[lo] */ + lua_geti(L, 1, up); + if (sort_comp(L, -1, -2)) /* a[up] < a[p]? */ + set2(L, p, up); /* swap a[up] - a[p] */ + else + lua_pop(L, 2); + } + if (up - lo == 2) /* only 3 elements? */ + return; /* already sorted */ + lua_geti(L, 1, p); /* get middle element (Pivot) */ + lua_pushvalue(L, -1); /* push Pivot */ + lua_geti(L, 1, up - 1); /* push a[up - 1] */ + set2(L, p, up - 1); /* swap Pivot (a[p]) with a[up - 1] */ + p = partition(L, lo, up); + /* a[lo .. p - 1] <= a[p] == P <= a[p + 1 .. up] */ + if (p - lo < up - p) { /* lower interval is smaller? */ + auxsort(L, lo, p - 1, rnd); /* call recursively for lower interval */ + n = p - lo; /* size of smaller interval */ + lo = p + 1; /* tail call for [p + 1 .. up] (upper interval) */ + } + else { + auxsort(L, p + 1, up, rnd); /* call recursively for upper interval */ + n = up - p; /* size of smaller interval */ + up = p - 1; /* tail call for [lo .. p - 1] (lower interval) */ + } + if ((up - lo) / 128 > n) /* partition too imbalanced? */ + rnd = l_randomizePivot(); /* try a new randomization */ + } /* tail call auxsort(L, lo, up, rnd) */ +} + + +static int sort (lua_State *L) { + lua_Integer n = aux_getn(L, 1, TAB_RW); + if (n > 1) { /* non-trivial interval? */ + luaL_argcheck(L, n < INT_MAX, 1, "array too big"); + if (!lua_isnoneornil(L, 2)) /* is there a 2nd argument? */ + luaL_checktype(L, 2, LUA_TFUNCTION); /* must be a function */ + lua_settop(L, 2); /* make sure there are two arguments */ + auxsort(L, 1, (IdxT)n, 0); + } + return 0; +} + +/* }====================================================== */ + + +static const luaL_Reg tab_funcs[] = { + {"concat", tconcat}, +#if defined(LUA_COMPAT_MAXN) + {"maxn", maxn}, +#endif + {"insert", tinsert}, + {"pack", pack}, + {"unpack", unpack}, + {"remove", tremove}, + {"move", tmove}, + {"sort", sort}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_table (lua_State *L) { + luaL_newlib(L, tab_funcs); +#if defined(LUA_COMPAT_UNPACK) + /* _G.unpack = table.unpack */ + lua_getfield(L, -1, "unpack"); + lua_setglobal(L, "unpack"); +#endif + return 1; +} + diff --git a/src/rcheevos/test/lua/src/ltm.c b/src/rcheevos/test/lua/src/ltm.c new file mode 100644 index 000000000..bc8f64eb3 --- /dev/null +++ b/src/rcheevos/test/lua/src/ltm.c @@ -0,0 +1,165 @@ +/* +** $Id: ltm.c,v 2.38 2016/12/22 13:08:50 roberto Exp $ +** Tag methods +** See Copyright Notice in lua.h +*/ + +#define ltm_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + +static const char udatatypename[] = "userdata"; + +LUAI_DDEF const char *const luaT_typenames_[LUA_TOTALTAGS] = { + "no value", + "nil", "boolean", udatatypename, "number", + "string", "table", "function", udatatypename, "thread", + "proto" /* this last case is used for tests only */ +}; + + +void luaT_init (lua_State *L) { + static const char *const luaT_eventname[] = { /* ORDER TM */ + "__index", "__newindex", + "__gc", "__mode", "__len", "__eq", + "__add", "__sub", "__mul", "__mod", "__pow", + "__div", "__idiv", + "__band", "__bor", "__bxor", "__shl", "__shr", + "__unm", "__bnot", "__lt", "__le", + "__concat", "__call" + }; + int i; + for (i=0; itmname[i] = luaS_new(L, luaT_eventname[i]); + luaC_fix(L, obj2gco(G(L)->tmname[i])); /* never collect these names */ + } +} + + +/* +** function to be used with macro "fasttm": optimized for absence of +** tag methods +*/ +const TValue *luaT_gettm (Table *events, TMS event, TString *ename) { + const TValue *tm = luaH_getshortstr(events, ename); + lua_assert(event <= TM_EQ); + if (ttisnil(tm)) { /* no tag method? */ + events->flags |= cast_byte(1u<metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(o)->metatable; + break; + default: + mt = G(L)->mt[ttnov(o)]; + } + return (mt ? luaH_getshortstr(mt, G(L)->tmname[event]) : luaO_nilobject); +} + + +/* +** Return the name of the type of an object. For tables and userdata +** with metatable, use their '__name' metafield, if present. +*/ +const char *luaT_objtypename (lua_State *L, const TValue *o) { + Table *mt; + if ((ttistable(o) && (mt = hvalue(o)->metatable) != NULL) || + (ttisfulluserdata(o) && (mt = uvalue(o)->metatable) != NULL)) { + const TValue *name = luaH_getshortstr(mt, luaS_new(L, "__name")); + if (ttisstring(name)) /* is '__name' a string? */ + return getstr(tsvalue(name)); /* use it as type name */ + } + return ttypename(ttnov(o)); /* else use standard type name */ +} + + +void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1, + const TValue *p2, TValue *p3, int hasres) { + ptrdiff_t result = savestack(L, p3); + StkId func = L->top; + setobj2s(L, func, f); /* push function (assume EXTRC_STACK) */ + setobj2s(L, func + 1, p1); /* 1st argument */ + setobj2s(L, func + 2, p2); /* 2nd argument */ + L->top += 3; + if (!hasres) /* no result? 'p3' is third argument */ + setobj2s(L, L->top++, p3); /* 3rd argument */ + /* metamethod may yield only when called from Lua code */ + if (isLua(L->ci)) + luaD_call(L, func, hasres); + else + luaD_callnoyield(L, func, hasres); + if (hasres) { /* if has result, move it to its place */ + p3 = restorestack(L, result); + setobjs2s(L, p3, --L->top); + } +} + + +int luaT_callbinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event) { + const TValue *tm = luaT_gettmbyobj(L, p1, event); /* try first operand */ + if (ttisnil(tm)) + tm = luaT_gettmbyobj(L, p2, event); /* try second operand */ + if (ttisnil(tm)) return 0; + luaT_callTM(L, tm, p1, p2, res, 1); + return 1; +} + + +void luaT_trybinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event) { + if (!luaT_callbinTM(L, p1, p2, res, event)) { + switch (event) { + case TM_CONCAT: + luaG_concaterror(L, p1, p2); + /* call never returns, but to avoid warnings: *//* FALLTHROUGH */ + case TM_BAND: case TM_BOR: case TM_BXOR: + case TM_SHL: case TM_SHR: case TM_BNOT: { + lua_Number dummy; + if (tonumber(p1, &dummy) && tonumber(p2, &dummy)) + luaG_tointerror(L, p1, p2); + else + luaG_opinterror(L, p1, p2, "perform bitwise operation on"); + } + /* calls never return, but to avoid warnings: *//* FALLTHROUGH */ + default: + luaG_opinterror(L, p1, p2, "perform arithmetic on"); + } + } +} + + +int luaT_callorderTM (lua_State *L, const TValue *p1, const TValue *p2, + TMS event) { + if (!luaT_callbinTM(L, p1, p2, L->top, event)) + return -1; /* no metamethod */ + else + return !l_isfalse(L->top); +} + diff --git a/src/rcheevos/test/lua/src/ltm.h b/src/rcheevos/test/lua/src/ltm.h new file mode 100644 index 000000000..63db7269b --- /dev/null +++ b/src/rcheevos/test/lua/src/ltm.h @@ -0,0 +1,76 @@ +/* +** $Id: ltm.h,v 2.22 2016/02/26 19:20:15 roberto Exp $ +** Tag methods +** See Copyright Notice in lua.h +*/ + +#ifndef ltm_h +#define ltm_h + + +#include "lobject.h" + + +/* +* WARNING: if you change the order of this enumeration, +* grep "ORDER TM" and "ORDER OP" +*/ +typedef enum { + TM_INDEX, + TM_NEWINDEX, + TM_GC, + TM_MODE, + TM_LEN, + TM_EQ, /* last tag method with fast access */ + TM_ADD, + TM_SUB, + TM_MUL, + TM_MOD, + TM_POW, + TM_DIV, + TM_IDIV, + TM_BAND, + TM_BOR, + TM_BXOR, + TM_SHL, + TM_SHR, + TM_UNM, + TM_BNOT, + TM_LT, + TM_LE, + TM_CONCAT, + TM_CALL, + TM_N /* number of elements in the enum */ +} TMS; + + + +#define gfasttm(g,et,e) ((et) == NULL ? NULL : \ + ((et)->flags & (1u<<(e))) ? NULL : luaT_gettm(et, e, (g)->tmname[e])) + +#define fasttm(l,et,e) gfasttm(G(l), et, e) + +#define ttypename(x) luaT_typenames_[(x) + 1] + +LUAI_DDEC const char *const luaT_typenames_[LUA_TOTALTAGS]; + + +LUAI_FUNC const char *luaT_objtypename (lua_State *L, const TValue *o); + +LUAI_FUNC const TValue *luaT_gettm (Table *events, TMS event, TString *ename); +LUAI_FUNC const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, + TMS event); +LUAI_FUNC void luaT_init (lua_State *L); + +LUAI_FUNC void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1, + const TValue *p2, TValue *p3, int hasres); +LUAI_FUNC int luaT_callbinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event); +LUAI_FUNC void luaT_trybinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event); +LUAI_FUNC int luaT_callorderTM (lua_State *L, const TValue *p1, + const TValue *p2, TMS event); + + + +#endif diff --git a/src/rcheevos/test/lua/src/lua.c b/src/rcheevos/test/lua/src/lua.c new file mode 100644 index 000000000..3f082da6b --- /dev/null +++ b/src/rcheevos/test/lua/src/lua.c @@ -0,0 +1,612 @@ +/* +** $Id: lua.c,v 1.230 2017/01/12 17:14:26 roberto Exp $ +** Lua stand-alone interpreter +** See Copyright Notice in lua.h +*/ + +#define lua_c + +#include "lprefix.h" + + +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + + +#if !defined(LUA_PROMPT) +#define LUA_PROMPT "> " +#define LUA_PROMPT2 ">> " +#endif + +#if !defined(LUA_PROGNAME) +#define LUA_PROGNAME "lua" +#endif + +#if !defined(LUA_MAXINPUT) +#define LUA_MAXINPUT 512 +#endif + +#if !defined(LUA_INIT_VAR) +#define LUA_INIT_VAR "LUA_INIT" +#endif + +#define LUA_INITVARVERSION LUA_INIT_VAR LUA_VERSUFFIX + + +/* +** lua_stdin_is_tty detects whether the standard input is a 'tty' (that +** is, whether we're running lua interactively). +*/ +#if !defined(lua_stdin_is_tty) /* { */ + +#if defined(LUA_USE_POSIX) /* { */ + +#include +#define lua_stdin_is_tty() isatty(0) + +#elif defined(LUA_USE_WINDOWS) /* }{ */ + +#include +#include + +#define lua_stdin_is_tty() _isatty(_fileno(stdin)) + +#else /* }{ */ + +/* ISO C definition */ +#define lua_stdin_is_tty() 1 /* assume stdin is a tty */ + +#endif /* } */ + +#endif /* } */ + + +/* +** lua_readline defines how to show a prompt and then read a line from +** the standard input. +** lua_saveline defines how to "save" a read line in a "history". +** lua_freeline defines how to free a line read by lua_readline. +*/ +#if !defined(lua_readline) /* { */ + +#if defined(LUA_USE_READLINE) /* { */ + +#include +#include +#define lua_readline(L,b,p) ((void)L, ((b)=readline(p)) != NULL) +#define lua_saveline(L,line) ((void)L, add_history(line)) +#define lua_freeline(L,b) ((void)L, free(b)) + +#else /* }{ */ + +#define lua_readline(L,b,p) \ + ((void)L, fputs(p, stdout), fflush(stdout), /* show prompt */ \ + fgets(b, LUA_MAXINPUT, stdin) != NULL) /* get line */ +#define lua_saveline(L,line) { (void)L; (void)line; } +#define lua_freeline(L,b) { (void)L; (void)b; } + +#endif /* } */ + +#endif /* } */ + + + + +static lua_State *globalL = NULL; + +static const char *progname = LUA_PROGNAME; + + +/* +** Hook set by signal function to stop the interpreter. +*/ +static void lstop (lua_State *L, lua_Debug *ar) { + (void)ar; /* unused arg. */ + lua_sethook(L, NULL, 0, 0); /* reset hook */ + luaL_error(L, "interrupted!"); +} + + +/* +** Function to be called at a C signal. Because a C signal cannot +** just change a Lua state (as there is no proper synchronization), +** this function only sets a hook that, when called, will stop the +** interpreter. +*/ +static void laction (int i) { + signal(i, SIG_DFL); /* if another SIGINT happens, terminate process */ + lua_sethook(globalL, lstop, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); +} + + +static void print_usage (const char *badoption) { + lua_writestringerror("%s: ", progname); + if (badoption[1] == 'e' || badoption[1] == 'l') + lua_writestringerror("'%s' needs argument\n", badoption); + else + lua_writestringerror("unrecognized option '%s'\n", badoption); + lua_writestringerror( + "usage: %s [options] [script [args]]\n" + "Available options are:\n" + " -e stat execute string 'stat'\n" + " -i enter interactive mode after executing 'script'\n" + " -l name require library 'name'\n" + " -v show version information\n" + " -E ignore environment variables\n" + " -- stop handling options\n" + " - stop handling options and execute stdin\n" + , + progname); +} + + +/* +** Prints an error message, adding the program name in front of it +** (if present) +*/ +static void l_message (const char *pname, const char *msg) { + if (pname) lua_writestringerror("%s: ", pname); + lua_writestringerror("%s\n", msg); +} + + +/* +** Check whether 'status' is not OK and, if so, prints the error +** message on the top of the stack. It assumes that the error object +** is a string, as it was either generated by Lua or by 'msghandler'. +*/ +static int report (lua_State *L, int status) { + if (status != LUA_OK) { + const char *msg = lua_tostring(L, -1); + l_message(progname, msg); + lua_pop(L, 1); /* remove message */ + } + return status; +} + + +/* +** Message handler used to run all chunks +*/ +static int msghandler (lua_State *L) { + const char *msg = lua_tostring(L, 1); + if (msg == NULL) { /* is error object not a string? */ + if (luaL_callmeta(L, 1, "__tostring") && /* does it have a metamethod */ + lua_type(L, -1) == LUA_TSTRING) /* that produces a string? */ + return 1; /* that is the message */ + else + msg = lua_pushfstring(L, "(error object is a %s value)", + luaL_typename(L, 1)); + } + luaL_traceback(L, L, msg, 1); /* append a standard traceback */ + return 1; /* return the traceback */ +} + + +/* +** Interface to 'lua_pcall', which sets appropriate message function +** and C-signal handler. Used to run all chunks. +*/ +static int docall (lua_State *L, int narg, int nres) { + int status; + int base = lua_gettop(L) - narg; /* function index */ + lua_pushcfunction(L, msghandler); /* push message handler */ + lua_insert(L, base); /* put it under function and args */ + globalL = L; /* to be available to 'laction' */ + signal(SIGINT, laction); /* set C-signal handler */ + status = lua_pcall(L, narg, nres, base); + signal(SIGINT, SIG_DFL); /* reset C-signal handler */ + lua_remove(L, base); /* remove message handler from the stack */ + return status; +} + + +static void print_version (void) { + lua_writestring(LUA_COPYRIGHT, strlen(LUA_COPYRIGHT)); + lua_writeline(); +} + + +/* +** Create the 'arg' table, which stores all arguments from the +** command line ('argv'). It should be aligned so that, at index 0, +** it has 'argv[script]', which is the script name. The arguments +** to the script (everything after 'script') go to positive indices; +** other arguments (before the script name) go to negative indices. +** If there is no script name, assume interpreter's name as base. +*/ +static void createargtable (lua_State *L, char **argv, int argc, int script) { + int i, narg; + if (script == argc) script = 0; /* no script name? */ + narg = argc - (script + 1); /* number of positive indices */ + lua_createtable(L, narg, script + 1); + for (i = 0; i < argc; i++) { + lua_pushstring(L, argv[i]); + lua_rawseti(L, -2, i - script); + } + lua_setglobal(L, "arg"); +} + + +static int dochunk (lua_State *L, int status) { + if (status == LUA_OK) status = docall(L, 0, 0); + return report(L, status); +} + + +static int dofile (lua_State *L, const char *name) { + return dochunk(L, luaL_loadfile(L, name)); +} + + +static int dostring (lua_State *L, const char *s, const char *name) { + return dochunk(L, luaL_loadbuffer(L, s, strlen(s), name)); +} + + +/* +** Calls 'require(name)' and stores the result in a global variable +** with the given name. +*/ +static int dolibrary (lua_State *L, const char *name) { + int status; + lua_getglobal(L, "require"); + lua_pushstring(L, name); + status = docall(L, 1, 1); /* call 'require(name)' */ + if (status == LUA_OK) + lua_setglobal(L, name); /* global[name] = require return */ + return report(L, status); +} + + +/* +** Returns the string to be used as a prompt by the interpreter. +*/ +static const char *get_prompt (lua_State *L, int firstline) { + const char *p; + lua_getglobal(L, firstline ? "_PROMPT" : "_PROMPT2"); + p = lua_tostring(L, -1); + if (p == NULL) p = (firstline ? LUA_PROMPT : LUA_PROMPT2); + return p; +} + +/* mark in error messages for incomplete statements */ +#define EOFMARK "" +#define marklen (sizeof(EOFMARK)/sizeof(char) - 1) + + +/* +** Check whether 'status' signals a syntax error and the error +** message at the top of the stack ends with the above mark for +** incomplete statements. +*/ +static int incomplete (lua_State *L, int status) { + if (status == LUA_ERRSYNTAX) { + size_t lmsg; + const char *msg = lua_tolstring(L, -1, &lmsg); + if (lmsg >= marklen && strcmp(msg + lmsg - marklen, EOFMARK) == 0) { + lua_pop(L, 1); + return 1; + } + } + return 0; /* else... */ +} + + +/* +** Prompt the user, read a line, and push it into the Lua stack. +*/ +static int pushline (lua_State *L, int firstline) { + char buffer[LUA_MAXINPUT]; + char *b = buffer; + size_t l; + const char *prmt = get_prompt(L, firstline); + int readstatus = lua_readline(L, b, prmt); + if (readstatus == 0) + return 0; /* no input (prompt will be popped by caller) */ + lua_pop(L, 1); /* remove prompt */ + l = strlen(b); + if (l > 0 && b[l-1] == '\n') /* line ends with newline? */ + b[--l] = '\0'; /* remove it */ + if (firstline && b[0] == '=') /* for compatibility with 5.2, ... */ + lua_pushfstring(L, "return %s", b + 1); /* change '=' to 'return' */ + else + lua_pushlstring(L, b, l); + lua_freeline(L, b); + return 1; +} + + +/* +** Try to compile line on the stack as 'return ;'; on return, stack +** has either compiled chunk or original line (if compilation failed). +*/ +static int addreturn (lua_State *L) { + const char *line = lua_tostring(L, -1); /* original line */ + const char *retline = lua_pushfstring(L, "return %s;", line); + int status = luaL_loadbuffer(L, retline, strlen(retline), "=stdin"); + if (status == LUA_OK) { + lua_remove(L, -2); /* remove modified line */ + if (line[0] != '\0') /* non empty? */ + lua_saveline(L, line); /* keep history */ + } + else + lua_pop(L, 2); /* pop result from 'luaL_loadbuffer' and modified line */ + return status; +} + + +/* +** Read multiple lines until a complete Lua statement +*/ +static int multiline (lua_State *L) { + for (;;) { /* repeat until gets a complete statement */ + size_t len; + const char *line = lua_tolstring(L, 1, &len); /* get what it has */ + int status = luaL_loadbuffer(L, line, len, "=stdin"); /* try it */ + if (!incomplete(L, status) || !pushline(L, 0)) { + lua_saveline(L, line); /* keep history */ + return status; /* cannot or should not try to add continuation line */ + } + lua_pushliteral(L, "\n"); /* add newline... */ + lua_insert(L, -2); /* ...between the two lines */ + lua_concat(L, 3); /* join them */ + } +} + + +/* +** Read a line and try to load (compile) it first as an expression (by +** adding "return " in front of it) and second as a statement. Return +** the final status of load/call with the resulting function (if any) +** in the top of the stack. +*/ +static int loadline (lua_State *L) { + int status; + lua_settop(L, 0); + if (!pushline(L, 1)) + return -1; /* no input */ + if ((status = addreturn(L)) != LUA_OK) /* 'return ...' did not work? */ + status = multiline(L); /* try as command, maybe with continuation lines */ + lua_remove(L, 1); /* remove line from the stack */ + lua_assert(lua_gettop(L) == 1); + return status; +} + + +/* +** Prints (calling the Lua 'print' function) any values on the stack +*/ +static void l_print (lua_State *L) { + int n = lua_gettop(L); + if (n > 0) { /* any result to be printed? */ + luaL_checkstack(L, LUA_MINSTACK, "too many results to print"); + lua_getglobal(L, "print"); + lua_insert(L, 1); + if (lua_pcall(L, n, 0, 0) != LUA_OK) + l_message(progname, lua_pushfstring(L, "error calling 'print' (%s)", + lua_tostring(L, -1))); + } +} + + +/* +** Do the REPL: repeatedly read (load) a line, evaluate (call) it, and +** print any results. +*/ +static void doREPL (lua_State *L) { + int status; + const char *oldprogname = progname; + progname = NULL; /* no 'progname' on errors in interactive mode */ + while ((status = loadline(L)) != -1) { + if (status == LUA_OK) + status = docall(L, 0, LUA_MULTRET); + if (status == LUA_OK) l_print(L); + else report(L, status); + } + lua_settop(L, 0); /* clear stack */ + lua_writeline(); + progname = oldprogname; +} + + +/* +** Push on the stack the contents of table 'arg' from 1 to #arg +*/ +static int pushargs (lua_State *L) { + int i, n; + if (lua_getglobal(L, "arg") != LUA_TTABLE) + luaL_error(L, "'arg' is not a table"); + n = (int)luaL_len(L, -1); + luaL_checkstack(L, n + 3, "too many arguments to script"); + for (i = 1; i <= n; i++) + lua_rawgeti(L, -i, i); + lua_remove(L, -i); /* remove table from the stack */ + return n; +} + + +static int handle_script (lua_State *L, char **argv) { + int status; + const char *fname = argv[0]; + if (strcmp(fname, "-") == 0 && strcmp(argv[-1], "--") != 0) + fname = NULL; /* stdin */ + status = luaL_loadfile(L, fname); + if (status == LUA_OK) { + int n = pushargs(L); /* push arguments to script */ + status = docall(L, n, LUA_MULTRET); + } + return report(L, status); +} + + + +/* bits of various argument indicators in 'args' */ +#define has_error 1 /* bad option */ +#define has_i 2 /* -i */ +#define has_v 4 /* -v */ +#define has_e 8 /* -e */ +#define has_E 16 /* -E */ + +/* +** Traverses all arguments from 'argv', returning a mask with those +** needed before running any Lua code (or an error code if it finds +** any invalid argument). 'first' returns the first not-handled argument +** (either the script name or a bad argument in case of error). +*/ +static int collectargs (char **argv, int *first) { + int args = 0; + int i; + for (i = 1; argv[i] != NULL; i++) { + *first = i; + if (argv[i][0] != '-') /* not an option? */ + return args; /* stop handling options */ + switch (argv[i][1]) { /* else check option */ + case '-': /* '--' */ + if (argv[i][2] != '\0') /* extra characters after '--'? */ + return has_error; /* invalid option */ + *first = i + 1; + return args; + case '\0': /* '-' */ + return args; /* script "name" is '-' */ + case 'E': + if (argv[i][2] != '\0') /* extra characters after 1st? */ + return has_error; /* invalid option */ + args |= has_E; + break; + case 'i': + args |= has_i; /* (-i implies -v) *//* FALLTHROUGH */ + case 'v': + if (argv[i][2] != '\0') /* extra characters after 1st? */ + return has_error; /* invalid option */ + args |= has_v; + break; + case 'e': + args |= has_e; /* FALLTHROUGH */ + case 'l': /* both options need an argument */ + if (argv[i][2] == '\0') { /* no concatenated argument? */ + i++; /* try next 'argv' */ + if (argv[i] == NULL || argv[i][0] == '-') + return has_error; /* no next argument or it is another option */ + } + break; + default: /* invalid option */ + return has_error; + } + } + *first = i; /* no script name */ + return args; +} + + +/* +** Processes options 'e' and 'l', which involve running Lua code. +** Returns 0 if some code raises an error. +*/ +static int runargs (lua_State *L, char **argv, int n) { + int i; + for (i = 1; i < n; i++) { + int option = argv[i][1]; + lua_assert(argv[i][0] == '-'); /* already checked */ + if (option == 'e' || option == 'l') { + int status; + const char *extra = argv[i] + 2; /* both options need an argument */ + if (*extra == '\0') extra = argv[++i]; + lua_assert(extra != NULL); + status = (option == 'e') + ? dostring(L, extra, "=(command line)") + : dolibrary(L, extra); + if (status != LUA_OK) return 0; + } + } + return 1; +} + + + +static int handle_luainit (lua_State *L) { + const char *name = "=" LUA_INITVARVERSION; + const char *init = getenv(name + 1); + if (init == NULL) { + name = "=" LUA_INIT_VAR; + init = getenv(name + 1); /* try alternative name */ + } + if (init == NULL) return LUA_OK; + else if (init[0] == '@') + return dofile(L, init+1); + else + return dostring(L, init, name); +} + + +/* +** Main body of stand-alone interpreter (to be called in protected mode). +** Reads the options and handles them all. +*/ +static int pmain (lua_State *L) { + int argc = (int)lua_tointeger(L, 1); + char **argv = (char **)lua_touserdata(L, 2); + int script; + int args = collectargs(argv, &script); + luaL_checkversion(L); /* check that interpreter has correct version */ + if (argv[0] && argv[0][0]) progname = argv[0]; + if (args == has_error) { /* bad arg? */ + print_usage(argv[script]); /* 'script' has index of bad arg. */ + return 0; + } + if (args & has_v) /* option '-v'? */ + print_version(); + if (args & has_E) { /* option '-E'? */ + lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */ + lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV"); + } + luaL_openlibs(L); /* open standard libraries */ + createargtable(L, argv, argc, script); /* create table 'arg' */ + if (!(args & has_E)) { /* no option '-E'? */ + if (handle_luainit(L) != LUA_OK) /* run LUA_INIT */ + return 0; /* error running LUA_INIT */ + } + if (!runargs(L, argv, script)) /* execute arguments -e and -l */ + return 0; /* something failed */ + if (script < argc && /* execute main script (if there is one) */ + handle_script(L, argv + script) != LUA_OK) + return 0; + if (args & has_i) /* -i option? */ + doREPL(L); /* do read-eval-print loop */ + else if (script == argc && !(args & (has_e | has_v))) { /* no arguments? */ + if (lua_stdin_is_tty()) { /* running in interactive mode? */ + print_version(); + doREPL(L); /* do read-eval-print loop */ + } + else dofile(L, NULL); /* executes stdin as a file */ + } + lua_pushboolean(L, 1); /* signal no errors */ + return 1; +} + + +int main (int argc, char **argv) { + int status, result; + lua_State *L = luaL_newstate(); /* create state */ + if (L == NULL) { + l_message(argv[0], "cannot create state: not enough memory"); + return EXIT_FAILURE; + } + lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */ + lua_pushinteger(L, argc); /* 1st argument */ + lua_pushlightuserdata(L, argv); /* 2nd argument */ + status = lua_pcall(L, 2, 1, 0); /* do the call */ + result = lua_toboolean(L, -1); /* get result */ + report(L, status); + lua_close(L); + return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE; +} + diff --git a/src/rcheevos/test/lua/src/lua.h b/src/rcheevos/test/lua/src/lua.h new file mode 100644 index 000000000..26c0e2d69 --- /dev/null +++ b/src/rcheevos/test/lua/src/lua.h @@ -0,0 +1,486 @@ +/* +** $Id: lua.h,v 1.332 2016/12/22 15:51:20 roberto Exp $ +** Lua - A Scripting Language +** Lua.org, PUC-Rio, Brazil (http://www.lua.org) +** See Copyright Notice at the end of this file +*/ + + +#ifndef lua_h +#define lua_h + +#include +#include + + +#include "luaconf.h" + + +#define LUA_VERSION_MAJOR "5" +#define LUA_VERSION_MINOR "3" +#define LUA_VERSION_NUM 503 +#define LUA_VERSION_RELEASE "4" + +#define LUA_VERSION "Lua " LUA_VERSION_MAJOR "." LUA_VERSION_MINOR +#define LUA_RELEASE LUA_VERSION "." LUA_VERSION_RELEASE +#define LUA_COPYRIGHT LUA_RELEASE " Copyright (C) 1994-2017 Lua.org, PUC-Rio" +#define LUA_AUTHORS "R. Ierusalimschy, L. H. de Figueiredo, W. Celes" + + +/* mark for precompiled code ('Lua') */ +#define LUA_SIGNATURE "\x1bLua" + +/* option for multiple returns in 'lua_pcall' and 'lua_call' */ +#define LUA_MULTRET (-1) + + +/* +** Pseudo-indices +** (-LUAI_MAXSTACK is the minimum valid index; we keep some free empty +** space after that to help overflow detection) +*/ +#define LUA_REGISTRYINDEX (-LUAI_MAXSTACK - 1000) +#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i)) + + +/* thread status */ +#define LUA_OK 0 +#define LUA_YIELD 1 +#define LUA_ERRRUN 2 +#define LUA_ERRSYNTAX 3 +#define LUA_ERRMEM 4 +#define LUA_ERRGCMM 5 +#define LUA_ERRERR 6 + + +typedef struct lua_State lua_State; + + +/* +** basic types +*/ +#define LUA_TNONE (-1) + +#define LUA_TNIL 0 +#define LUA_TBOOLEAN 1 +#define LUA_TLIGHTUSERDATA 2 +#define LUA_TNUMBER 3 +#define LUA_TSTRING 4 +#define LUA_TTABLE 5 +#define LUA_TFUNCTION 6 +#define LUA_TUSERDATA 7 +#define LUA_TTHREAD 8 + +#define LUA_NUMTAGS 9 + + + +/* minimum Lua stack available to a C function */ +#define LUA_MINSTACK 20 + + +/* predefined values in the registry */ +#define LUA_RIDX_MAINTHREAD 1 +#define LUA_RIDX_GLOBALS 2 +#define LUA_RIDX_LAST LUA_RIDX_GLOBALS + + +/* type of numbers in Lua */ +typedef LUA_NUMBER lua_Number; + + +/* type for integer functions */ +typedef LUA_INTEGER lua_Integer; + +/* unsigned integer type */ +typedef LUA_UNSIGNED lua_Unsigned; + +/* type for continuation-function contexts */ +typedef LUA_KCONTEXT lua_KContext; + + +/* +** Type for C functions registered with Lua +*/ +typedef int (*lua_CFunction) (lua_State *L); + +/* +** Type for continuation functions +*/ +typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx); + + +/* +** Type for functions that read/write blocks when loading/dumping Lua chunks +*/ +typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz); + +typedef int (*lua_Writer) (lua_State *L, const void *p, size_t sz, void *ud); + + +/* +** Type for memory-allocation functions +*/ +typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize); + + + +/* +** generic extra include file +*/ +#if defined(LUA_USER_H) +#include LUA_USER_H +#endif + + +/* +** RCS ident string +*/ +extern const char lua_ident[]; + + +/* +** state manipulation +*/ +LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud); +LUA_API void (lua_close) (lua_State *L); +LUA_API lua_State *(lua_newthread) (lua_State *L); + +LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf); + + +LUA_API const lua_Number *(lua_version) (lua_State *L); + + +/* +** basic stack manipulation +*/ +LUA_API int (lua_absindex) (lua_State *L, int idx); +LUA_API int (lua_gettop) (lua_State *L); +LUA_API void (lua_settop) (lua_State *L, int idx); +LUA_API void (lua_pushvalue) (lua_State *L, int idx); +LUA_API void (lua_rotate) (lua_State *L, int idx, int n); +LUA_API void (lua_copy) (lua_State *L, int fromidx, int toidx); +LUA_API int (lua_checkstack) (lua_State *L, int n); + +LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n); + + +/* +** access functions (stack -> C) +*/ + +LUA_API int (lua_isnumber) (lua_State *L, int idx); +LUA_API int (lua_isstring) (lua_State *L, int idx); +LUA_API int (lua_iscfunction) (lua_State *L, int idx); +LUA_API int (lua_isinteger) (lua_State *L, int idx); +LUA_API int (lua_isuserdata) (lua_State *L, int idx); +LUA_API int (lua_type) (lua_State *L, int idx); +LUA_API const char *(lua_typename) (lua_State *L, int tp); + +LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum); +LUA_API lua_Integer (lua_tointegerx) (lua_State *L, int idx, int *isnum); +LUA_API int (lua_toboolean) (lua_State *L, int idx); +LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len); +LUA_API size_t (lua_rawlen) (lua_State *L, int idx); +LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx); +LUA_API void *(lua_touserdata) (lua_State *L, int idx); +LUA_API lua_State *(lua_tothread) (lua_State *L, int idx); +LUA_API const void *(lua_topointer) (lua_State *L, int idx); + + +/* +** Comparison and arithmetic functions +*/ + +#define LUA_OPADD 0 /* ORDER TM, ORDER OP */ +#define LUA_OPSUB 1 +#define LUA_OPMUL 2 +#define LUA_OPMOD 3 +#define LUA_OPPOW 4 +#define LUA_OPDIV 5 +#define LUA_OPIDIV 6 +#define LUA_OPBAND 7 +#define LUA_OPBOR 8 +#define LUA_OPBXOR 9 +#define LUA_OPSHL 10 +#define LUA_OPSHR 11 +#define LUA_OPUNM 12 +#define LUA_OPBNOT 13 + +LUA_API void (lua_arith) (lua_State *L, int op); + +#define LUA_OPEQ 0 +#define LUA_OPLT 1 +#define LUA_OPLE 2 + +LUA_API int (lua_rawequal) (lua_State *L, int idx1, int idx2); +LUA_API int (lua_compare) (lua_State *L, int idx1, int idx2, int op); + + +/* +** push functions (C -> stack) +*/ +LUA_API void (lua_pushnil) (lua_State *L); +LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n); +LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n); +LUA_API const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len); +LUA_API const char *(lua_pushstring) (lua_State *L, const char *s); +LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, + va_list argp); +LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...); +LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); +LUA_API void (lua_pushboolean) (lua_State *L, int b); +LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p); +LUA_API int (lua_pushthread) (lua_State *L); + + +/* +** get functions (Lua -> stack) +*/ +LUA_API int (lua_getglobal) (lua_State *L, const char *name); +LUA_API int (lua_gettable) (lua_State *L, int idx); +LUA_API int (lua_getfield) (lua_State *L, int idx, const char *k); +LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n); +LUA_API int (lua_rawget) (lua_State *L, int idx); +LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n); +LUA_API int (lua_rawgetp) (lua_State *L, int idx, const void *p); + +LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec); +LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz); +LUA_API int (lua_getmetatable) (lua_State *L, int objindex); +LUA_API int (lua_getuservalue) (lua_State *L, int idx); + + +/* +** set functions (stack -> Lua) +*/ +LUA_API void (lua_setglobal) (lua_State *L, const char *name); +LUA_API void (lua_settable) (lua_State *L, int idx); +LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k); +LUA_API void (lua_seti) (lua_State *L, int idx, lua_Integer n); +LUA_API void (lua_rawset) (lua_State *L, int idx); +LUA_API void (lua_rawseti) (lua_State *L, int idx, lua_Integer n); +LUA_API void (lua_rawsetp) (lua_State *L, int idx, const void *p); +LUA_API int (lua_setmetatable) (lua_State *L, int objindex); +LUA_API void (lua_setuservalue) (lua_State *L, int idx); + + +/* +** 'load' and 'call' functions (load and run Lua code) +*/ +LUA_API void (lua_callk) (lua_State *L, int nargs, int nresults, + lua_KContext ctx, lua_KFunction k); +#define lua_call(L,n,r) lua_callk(L, (n), (r), 0, NULL) + +LUA_API int (lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, + lua_KContext ctx, lua_KFunction k); +#define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL) + +LUA_API int (lua_load) (lua_State *L, lua_Reader reader, void *dt, + const char *chunkname, const char *mode); + +LUA_API int (lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip); + + +/* +** coroutine functions +*/ +LUA_API int (lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, + lua_KFunction k); +LUA_API int (lua_resume) (lua_State *L, lua_State *from, int narg); +LUA_API int (lua_status) (lua_State *L); +LUA_API int (lua_isyieldable) (lua_State *L); + +#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL) + + +/* +** garbage-collection function and options +*/ + +#define LUA_GCSTOP 0 +#define LUA_GCRESTART 1 +#define LUA_GCCOLLECT 2 +#define LUA_GCCOUNT 3 +#define LUA_GCCOUNTB 4 +#define LUA_GCSTEP 5 +#define LUA_GCSETPAUSE 6 +#define LUA_GCSETSTEPMUL 7 +#define LUA_GCISRUNNING 9 + +LUA_API int (lua_gc) (lua_State *L, int what, int data); + + +/* +** miscellaneous functions +*/ + +LUA_API int (lua_error) (lua_State *L); + +LUA_API int (lua_next) (lua_State *L, int idx); + +LUA_API void (lua_concat) (lua_State *L, int n); +LUA_API void (lua_len) (lua_State *L, int idx); + +LUA_API size_t (lua_stringtonumber) (lua_State *L, const char *s); + +LUA_API lua_Alloc (lua_getallocf) (lua_State *L, void **ud); +LUA_API void (lua_setallocf) (lua_State *L, lua_Alloc f, void *ud); + + + +/* +** {============================================================== +** some useful macros +** =============================================================== +*/ + +#define lua_getextraspace(L) ((void *)((char *)(L) - LUA_EXTRASPACE)) + +#define lua_tonumber(L,i) lua_tonumberx(L,(i),NULL) +#define lua_tointeger(L,i) lua_tointegerx(L,(i),NULL) + +#define lua_pop(L,n) lua_settop(L, -(n)-1) + +#define lua_newtable(L) lua_createtable(L, 0, 0) + +#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n))) + +#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0) + +#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION) +#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE) +#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA) +#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL) +#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN) +#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD) +#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE) +#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0) + +#define lua_pushliteral(L, s) lua_pushstring(L, "" s) + +#define lua_pushglobaltable(L) \ + ((void)lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)) + +#define lua_tostring(L,i) lua_tolstring(L, (i), NULL) + + +#define lua_insert(L,idx) lua_rotate(L, (idx), 1) + +#define lua_remove(L,idx) (lua_rotate(L, (idx), -1), lua_pop(L, 1)) + +#define lua_replace(L,idx) (lua_copy(L, -1, (idx)), lua_pop(L, 1)) + +/* }============================================================== */ + + +/* +** {============================================================== +** compatibility macros for unsigned conversions +** =============================================================== +*/ +#if defined(LUA_COMPAT_APIINTCASTS) + +#define lua_pushunsigned(L,n) lua_pushinteger(L, (lua_Integer)(n)) +#define lua_tounsignedx(L,i,is) ((lua_Unsigned)lua_tointegerx(L,i,is)) +#define lua_tounsigned(L,i) lua_tounsignedx(L,(i),NULL) + +#endif +/* }============================================================== */ + +/* +** {====================================================================== +** Debug API +** ======================================================================= +*/ + + +/* +** Event codes +*/ +#define LUA_HOOKCALL 0 +#define LUA_HOOKRET 1 +#define LUA_HOOKLINE 2 +#define LUA_HOOKCOUNT 3 +#define LUA_HOOKTAILCALL 4 + + +/* +** Event masks +*/ +#define LUA_MASKCALL (1 << LUA_HOOKCALL) +#define LUA_MASKRET (1 << LUA_HOOKRET) +#define LUA_MASKLINE (1 << LUA_HOOKLINE) +#define LUA_MASKCOUNT (1 << LUA_HOOKCOUNT) + +typedef struct lua_Debug lua_Debug; /* activation record */ + + +/* Functions to be called by the debugger in specific events */ +typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar); + + +LUA_API int (lua_getstack) (lua_State *L, int level, lua_Debug *ar); +LUA_API int (lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar); +LUA_API const char *(lua_getlocal) (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *(lua_setlocal) (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *(lua_getupvalue) (lua_State *L, int funcindex, int n); +LUA_API const char *(lua_setupvalue) (lua_State *L, int funcindex, int n); + +LUA_API void *(lua_upvalueid) (lua_State *L, int fidx, int n); +LUA_API void (lua_upvaluejoin) (lua_State *L, int fidx1, int n1, + int fidx2, int n2); + +LUA_API void (lua_sethook) (lua_State *L, lua_Hook func, int mask, int count); +LUA_API lua_Hook (lua_gethook) (lua_State *L); +LUA_API int (lua_gethookmask) (lua_State *L); +LUA_API int (lua_gethookcount) (lua_State *L); + + +struct lua_Debug { + int event; + const char *name; /* (n) */ + const char *namewhat; /* (n) 'global', 'local', 'field', 'method' */ + const char *what; /* (S) 'Lua', 'C', 'main', 'tail' */ + const char *source; /* (S) */ + int currentline; /* (l) */ + int linedefined; /* (S) */ + int lastlinedefined; /* (S) */ + unsigned char nups; /* (u) number of upvalues */ + unsigned char nparams;/* (u) number of parameters */ + char isvararg; /* (u) */ + char istailcall; /* (t) */ + char short_src[LUA_IDSIZE]; /* (S) */ + /* private part */ + struct CallInfo *i_ci; /* active function */ +}; + +/* }====================================================================== */ + + +/****************************************************************************** +* Copyright (C) 1994-2017 Lua.org, PUC-Rio. +* +* 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. +******************************************************************************/ + + +#endif diff --git a/src/rcheevos/test/lua/src/lua.hpp b/src/rcheevos/test/lua/src/lua.hpp new file mode 100644 index 000000000..ec417f594 --- /dev/null +++ b/src/rcheevos/test/lua/src/lua.hpp @@ -0,0 +1,9 @@ +// lua.hpp +// Lua header files for C++ +// <> not supplied automatically because Lua also compiles as C++ + +extern "C" { +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" +} diff --git a/src/rcheevos/test/lua/src/luac.c b/src/rcheevos/test/lua/src/luac.c new file mode 100644 index 000000000..c0c91d017 --- /dev/null +++ b/src/rcheevos/test/lua/src/luac.c @@ -0,0 +1,449 @@ +/* +** $Id: luac.c,v 1.75 2015/03/12 01:58:27 lhf Exp $ +** Lua compiler (saves bytecodes to files; also lists bytecodes) +** See Copyright Notice in lua.h +*/ + +#define luac_c +#define LUA_CORE + +#include "lprefix.h" + +#include +#include +#include +#include +#include + +#include "lua.h" +#include "lauxlib.h" + +#include "lobject.h" +#include "lstate.h" +#include "lundump.h" + +static void PrintFunction(const Proto* f, int full); +#define luaU_print PrintFunction + +#define PROGNAME "luac" /* default program name */ +#define OUTPUT PROGNAME ".out" /* default output file */ + +static int listing=0; /* list bytecodes? */ +static int dumping=1; /* dump bytecodes? */ +static int stripping=0; /* strip debug information? */ +static char Output[]={ OUTPUT }; /* default output file name */ +static const char* output=Output; /* actual output file name */ +static const char* progname=PROGNAME; /* actual program name */ + +static void fatal(const char* message) +{ + fprintf(stderr,"%s: %s\n",progname,message); + exit(EXIT_FAILURE); +} + +static void cannot(const char* what) +{ + fprintf(stderr,"%s: cannot %s %s: %s\n",progname,what,output,strerror(errno)); + exit(EXIT_FAILURE); +} + +static void usage(const char* message) +{ + if (*message=='-') + fprintf(stderr,"%s: unrecognized option '%s'\n",progname,message); + else + fprintf(stderr,"%s: %s\n",progname,message); + fprintf(stderr, + "usage: %s [options] [filenames]\n" + "Available options are:\n" + " -l list (use -l -l for full listing)\n" + " -o name output to file 'name' (default is \"%s\")\n" + " -p parse only\n" + " -s strip debug information\n" + " -v show version information\n" + " -- stop handling options\n" + " - stop handling options and process stdin\n" + ,progname,Output); + exit(EXIT_FAILURE); +} + +#define IS(s) (strcmp(argv[i],s)==0) + +static int doargs(int argc, char* argv[]) +{ + int i; + int version=0; + if (argv[0]!=NULL && *argv[0]!=0) progname=argv[0]; + for (i=1; itop+(i)) + +static const Proto* combine(lua_State* L, int n) +{ + if (n==1) + return toproto(L,-1); + else + { + Proto* f; + int i=n; + if (lua_load(L,reader,&i,"=(" PROGNAME ")",NULL)!=LUA_OK) fatal(lua_tostring(L,-1)); + f=toproto(L,-1); + for (i=0; ip[i]=toproto(L,i-n-1); + if (f->p[i]->sizeupvalues>0) f->p[i]->upvalues[0].instack=0; + } + f->sizelineinfo=0; + return f; + } +} + +static int writer(lua_State* L, const void* p, size_t size, void* u) +{ + UNUSED(L); + return (fwrite(p,size,1,(FILE*)u)!=1) && (size!=0); +} + +static int pmain(lua_State* L) +{ + int argc=(int)lua_tointeger(L,1); + char** argv=(char**)lua_touserdata(L,2); + const Proto* f; + int i; + if (!lua_checkstack(L,argc)) fatal("too many input files"); + for (i=0; i1); + if (dumping) + { + FILE* D= (output==NULL) ? stdout : fopen(output,"wb"); + if (D==NULL) cannot("open"); + lua_lock(L); + luaU_dump(L,f,writer,D,stripping); + lua_unlock(L); + if (ferror(D)) cannot("write"); + if (fclose(D)) cannot("close"); + } + return 0; +} + +int main(int argc, char* argv[]) +{ + lua_State* L; + int i=doargs(argc,argv); + argc-=i; argv+=i; + if (argc<=0) usage("no input files given"); + L=luaL_newstate(); + if (L==NULL) fatal("cannot create state: not enough memory"); + lua_pushcfunction(L,&pmain); + lua_pushinteger(L,argc); + lua_pushlightuserdata(L,argv); + if (lua_pcall(L,2,0,0)!=LUA_OK) fatal(lua_tostring(L,-1)); + lua_close(L); + return EXIT_SUCCESS; +} + +/* +** $Id: luac.c,v 1.75 2015/03/12 01:58:27 lhf Exp $ +** print bytecodes +** See Copyright Notice in lua.h +*/ + +#include +#include + +#define luac_c +#define LUA_CORE + +#include "ldebug.h" +#include "lobject.h" +#include "lopcodes.h" + +#define VOID(p) ((const void*)(p)) + +static void PrintString(const TString* ts) +{ + const char* s=getstr(ts); + size_t i,n=tsslen(ts); + printf("%c",'"'); + for (i=0; ik[i]; + switch (ttype(o)) + { + case LUA_TNIL: + printf("nil"); + break; + case LUA_TBOOLEAN: + printf(bvalue(o) ? "true" : "false"); + break; + case LUA_TNUMFLT: + { + char buff[100]; + sprintf(buff,LUA_NUMBER_FMT,fltvalue(o)); + printf("%s",buff); + if (buff[strspn(buff,"-0123456789")]=='\0') printf(".0"); + break; + } + case LUA_TNUMINT: + printf(LUA_INTEGER_FMT,ivalue(o)); + break; + case LUA_TSHRSTR: case LUA_TLNGSTR: + PrintString(tsvalue(o)); + break; + default: /* cannot happen */ + printf("? type=%d",ttype(o)); + break; + } +} + +#define UPVALNAME(x) ((f->upvalues[x].name) ? getstr(f->upvalues[x].name) : "-") +#define MYK(x) (-1-(x)) + +static void PrintCode(const Proto* f) +{ + const Instruction* code=f->code; + int pc,n=f->sizecode; + for (pc=0; pc0) printf("[%d]\t",line); else printf("[-]\t"); + printf("%-9s\t",luaP_opnames[o]); + switch (getOpMode(o)) + { + case iABC: + printf("%d",a); + if (getBMode(o)!=OpArgN) printf(" %d",ISK(b) ? (MYK(INDEXK(b))) : b); + if (getCMode(o)!=OpArgN) printf(" %d",ISK(c) ? (MYK(INDEXK(c))) : c); + break; + case iABx: + printf("%d",a); + if (getBMode(o)==OpArgK) printf(" %d",MYK(bx)); + if (getBMode(o)==OpArgU) printf(" %d",bx); + break; + case iAsBx: + printf("%d %d",a,sbx); + break; + case iAx: + printf("%d",MYK(ax)); + break; + } + switch (o) + { + case OP_LOADK: + printf("\t; "); PrintConstant(f,bx); + break; + case OP_GETUPVAL: + case OP_SETUPVAL: + printf("\t; %s",UPVALNAME(b)); + break; + case OP_GETTABUP: + printf("\t; %s",UPVALNAME(b)); + if (ISK(c)) { printf(" "); PrintConstant(f,INDEXK(c)); } + break; + case OP_SETTABUP: + printf("\t; %s",UPVALNAME(a)); + if (ISK(b)) { printf(" "); PrintConstant(f,INDEXK(b)); } + if (ISK(c)) { printf(" "); PrintConstant(f,INDEXK(c)); } + break; + case OP_GETTABLE: + case OP_SELF: + if (ISK(c)) { printf("\t; "); PrintConstant(f,INDEXK(c)); } + break; + case OP_SETTABLE: + case OP_ADD: + case OP_SUB: + case OP_MUL: + case OP_POW: + case OP_DIV: + case OP_IDIV: + case OP_BAND: + case OP_BOR: + case OP_BXOR: + case OP_SHL: + case OP_SHR: + case OP_EQ: + case OP_LT: + case OP_LE: + if (ISK(b) || ISK(c)) + { + printf("\t; "); + if (ISK(b)) PrintConstant(f,INDEXK(b)); else printf("-"); + printf(" "); + if (ISK(c)) PrintConstant(f,INDEXK(c)); else printf("-"); + } + break; + case OP_JMP: + case OP_FORLOOP: + case OP_FORPREP: + case OP_TFORLOOP: + printf("\t; to %d",sbx+pc+2); + break; + case OP_CLOSURE: + printf("\t; %p",VOID(f->p[bx])); + break; + case OP_SETLIST: + if (c==0) printf("\t; %d",(int)code[++pc]); else printf("\t; %d",c); + break; + case OP_EXTRAARG: + printf("\t; "); PrintConstant(f,ax); + break; + default: + break; + } + printf("\n"); + } +} + +#define SS(x) ((x==1)?"":"s") +#define S(x) (int)(x),SS(x) + +static void PrintHeader(const Proto* f) +{ + const char* s=f->source ? getstr(f->source) : "=?"; + if (*s=='@' || *s=='=') + s++; + else if (*s==LUA_SIGNATURE[0]) + s="(bstring)"; + else + s="(string)"; + printf("\n%s <%s:%d,%d> (%d instruction%s at %p)\n", + (f->linedefined==0)?"main":"function",s, + f->linedefined,f->lastlinedefined, + S(f->sizecode),VOID(f)); + printf("%d%s param%s, %d slot%s, %d upvalue%s, ", + (int)(f->numparams),f->is_vararg?"+":"",SS(f->numparams), + S(f->maxstacksize),S(f->sizeupvalues)); + printf("%d local%s, %d constant%s, %d function%s\n", + S(f->sizelocvars),S(f->sizek),S(f->sizep)); +} + +static void PrintDebug(const Proto* f) +{ + int i,n; + n=f->sizek; + printf("constants (%d) for %p:\n",n,VOID(f)); + for (i=0; isizelocvars; + printf("locals (%d) for %p:\n",n,VOID(f)); + for (i=0; ilocvars[i].varname),f->locvars[i].startpc+1,f->locvars[i].endpc+1); + } + n=f->sizeupvalues; + printf("upvalues (%d) for %p:\n",n,VOID(f)); + for (i=0; iupvalues[i].instack,f->upvalues[i].idx); + } +} + +static void PrintFunction(const Proto* f, int full) +{ + int i,n=f->sizep; + PrintHeader(f); + PrintCode(f); + if (full) PrintDebug(f); + for (i=0; ip[i],full); +} diff --git a/src/rcheevos/test/lua/src/luaconf.h b/src/rcheevos/test/lua/src/luaconf.h new file mode 100644 index 000000000..f37bea096 --- /dev/null +++ b/src/rcheevos/test/lua/src/luaconf.h @@ -0,0 +1,783 @@ +/* +** $Id: luaconf.h,v 1.259 2016/12/22 13:08:50 roberto Exp $ +** Configuration file for Lua +** See Copyright Notice in lua.h +*/ + + +#ifndef luaconf_h +#define luaconf_h + +#include +#include + + +/* +** =================================================================== +** Search for "@@" to find all configurable definitions. +** =================================================================== +*/ + + +/* +** {==================================================================== +** System Configuration: macros to adapt (if needed) Lua to some +** particular platform, for instance compiling it with 32-bit numbers or +** restricting it to C89. +** ===================================================================== +*/ + +/* +@@ LUA_32BITS enables Lua with 32-bit integers and 32-bit floats. You +** can also define LUA_32BITS in the make file, but changing here you +** ensure that all software connected to Lua will be compiled with the +** same configuration. +*/ +/* #define LUA_32BITS */ + + +/* +@@ LUA_USE_C89 controls the use of non-ISO-C89 features. +** Define it if you want Lua to avoid the use of a few C99 features +** or Windows-specific features on Windows. +*/ +/* #define LUA_USE_C89 */ + + +/* +** By default, Lua on Windows use (some) specific Windows features +*/ +#if !defined(LUA_USE_C89) && defined(_WIN32) && !defined(_WIN32_WCE) +#define LUA_USE_WINDOWS /* enable goodies for regular Windows */ +#endif + + +#if defined(LUA_USE_WINDOWS) +#define LUA_DL_DLL /* enable support for DLL */ +#define LUA_USE_C89 /* broadly, Windows is C89 */ +#endif + + +#if defined(LUA_USE_LINUX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* needs an extra library: -ldl */ +#define LUA_USE_READLINE /* needs some extra libraries */ +#endif + + +#if defined(LUA_USE_MACOSX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* MacOS does not need -ldl */ +#define LUA_USE_READLINE /* needs an extra library: -lreadline */ +#endif + + +/* +@@ LUA_C89_NUMBERS ensures that Lua uses the largest types available for +** C89 ('long' and 'double'); Windows always has '__int64', so it does +** not need to use this case. +*/ +#if defined(LUA_USE_C89) && !defined(LUA_USE_WINDOWS) +#define LUA_C89_NUMBERS +#endif + + + +/* +@@ LUAI_BITSINT defines the (minimum) number of bits in an 'int'. +*/ +/* avoid undefined shifts */ +#if ((INT_MAX >> 15) >> 15) >= 1 +#define LUAI_BITSINT 32 +#else +/* 'int' always must have at least 16 bits */ +#define LUAI_BITSINT 16 +#endif + + +/* +@@ LUA_INT_TYPE defines the type for Lua integers. +@@ LUA_FLOAT_TYPE defines the type for Lua floats. +** Lua should work fine with any mix of these options (if supported +** by your C compiler). The usual configurations are 64-bit integers +** and 'double' (the default), 32-bit integers and 'float' (for +** restricted platforms), and 'long'/'double' (for C compilers not +** compliant with C99, which may not have support for 'long long'). +*/ + +/* predefined options for LUA_INT_TYPE */ +#define LUA_INT_INT 1 +#define LUA_INT_LONG 2 +#define LUA_INT_LONGLONG 3 + +/* predefined options for LUA_FLOAT_TYPE */ +#define LUA_FLOAT_FLOAT 1 +#define LUA_FLOAT_DOUBLE 2 +#define LUA_FLOAT_LONGDOUBLE 3 + +#if defined(LUA_32BITS) /* { */ +/* +** 32-bit integers and 'float' +*/ +#if LUAI_BITSINT >= 32 /* use 'int' if big enough */ +#define LUA_INT_TYPE LUA_INT_INT +#else /* otherwise use 'long' */ +#define LUA_INT_TYPE LUA_INT_LONG +#endif +#define LUA_FLOAT_TYPE LUA_FLOAT_FLOAT + +#elif defined(LUA_C89_NUMBERS) /* }{ */ +/* +** largest types available for C89 ('long' and 'double') +*/ +#define LUA_INT_TYPE LUA_INT_LONG +#define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE + +#endif /* } */ + + +/* +** default configuration for 64-bit Lua ('long long' and 'double') +*/ +#if !defined(LUA_INT_TYPE) +#define LUA_INT_TYPE LUA_INT_LONGLONG +#endif + +#if !defined(LUA_FLOAT_TYPE) +#define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE +#endif + +/* }================================================================== */ + + + + +/* +** {================================================================== +** Configuration for Paths. +** =================================================================== +*/ + +/* +** LUA_PATH_SEP is the character that separates templates in a path. +** LUA_PATH_MARK is the string that marks the substitution points in a +** template. +** LUA_EXEC_DIR in a Windows path is replaced by the executable's +** directory. +*/ +#define LUA_PATH_SEP ";" +#define LUA_PATH_MARK "?" +#define LUA_EXEC_DIR "!" + + +/* +@@ LUA_PATH_DEFAULT is the default path that Lua uses to look for +** Lua libraries. +@@ LUA_CPATH_DEFAULT is the default path that Lua uses to look for +** C libraries. +** CHANGE them if your machine has a non-conventional directory +** hierarchy or if you want to install your libraries in +** non-conventional directories. +*/ +#define LUA_VDIR LUA_VERSION_MAJOR "." LUA_VERSION_MINOR +#if defined(_WIN32) /* { */ +/* +** In Windows, any exclamation mark ('!') in the path is replaced by the +** path of the directory of the executable file of the current process. +*/ +#define LUA_LDIR "!\\lua\\" +#define LUA_CDIR "!\\" +#define LUA_SHRDIR "!\\..\\share\\lua\\" LUA_VDIR "\\" +#define LUA_PATH_DEFAULT \ + LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \ + LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua;" \ + LUA_SHRDIR"?.lua;" LUA_SHRDIR"?\\init.lua;" \ + ".\\?.lua;" ".\\?\\init.lua" +#define LUA_CPATH_DEFAULT \ + LUA_CDIR"?.dll;" \ + LUA_CDIR"..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \ + LUA_CDIR"loadall.dll;" ".\\?.dll" + +#else /* }{ */ + +#define LUA_ROOT "/usr/local/" +#define LUA_LDIR LUA_ROOT "share/lua/" LUA_VDIR "/" +#define LUA_CDIR LUA_ROOT "lib/lua/" LUA_VDIR "/" +#define LUA_PATH_DEFAULT \ + LUA_LDIR"?.lua;" LUA_LDIR"?/init.lua;" \ + LUA_CDIR"?.lua;" LUA_CDIR"?/init.lua;" \ + "./?.lua;" "./?/init.lua" +#define LUA_CPATH_DEFAULT \ + LUA_CDIR"?.so;" LUA_CDIR"loadall.so;" "./?.so" +#endif /* } */ + + +/* +@@ LUA_DIRSEP is the directory separator (for submodules). +** CHANGE it if your machine does not use "/" as the directory separator +** and is not Windows. (On Windows Lua automatically uses "\".) +*/ +#if defined(_WIN32) +#define LUA_DIRSEP "\\" +#else +#define LUA_DIRSEP "/" +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Marks for exported symbols in the C code +** =================================================================== +*/ + +/* +@@ LUA_API is a mark for all core API functions. +@@ LUALIB_API is a mark for all auxiliary library functions. +@@ LUAMOD_API is a mark for all standard library opening functions. +** CHANGE them if you need to define those functions in some special way. +** For instance, if you want to create one Windows DLL with the core and +** the libraries, you may want to use the following definition (define +** LUA_BUILD_AS_DLL to get it). +*/ +#if defined(LUA_BUILD_AS_DLL) /* { */ + +#if defined(LUA_CORE) || defined(LUA_LIB) /* { */ +#define LUA_API __declspec(dllexport) +#else /* }{ */ +#define LUA_API __declspec(dllimport) +#endif /* } */ + +#else /* }{ */ + +#define LUA_API extern + +#endif /* } */ + + +/* more often than not the libs go together with the core */ +#define LUALIB_API LUA_API +#define LUAMOD_API LUALIB_API + + +/* +@@ LUAI_FUNC is a mark for all extern functions that are not to be +** exported to outside modules. +@@ LUAI_DDEF and LUAI_DDEC are marks for all extern (const) variables +** that are not to be exported to outside modules (LUAI_DDEF for +** definitions and LUAI_DDEC for declarations). +** CHANGE them if you need to mark them in some special way. Elf/gcc +** (versions 3.2 and later) mark them as "hidden" to optimize access +** when Lua is compiled as a shared library. Not all elf targets support +** this attribute. Unfortunately, gcc does not offer a way to check +** whether the target offers that support, and those without support +** give a warning about it. To avoid these warnings, change to the +** default definition. +*/ +#if defined(__GNUC__) && ((__GNUC__*100 + __GNUC_MINOR__) >= 302) && \ + defined(__ELF__) /* { */ +#define LUAI_FUNC __attribute__((visibility("hidden"))) extern +#else /* }{ */ +#define LUAI_FUNC extern +#endif /* } */ + +#define LUAI_DDEC LUAI_FUNC +#define LUAI_DDEF /* empty */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Compatibility with previous versions +** =================================================================== +*/ + +/* +@@ LUA_COMPAT_5_2 controls other macros for compatibility with Lua 5.2. +@@ LUA_COMPAT_5_1 controls other macros for compatibility with Lua 5.1. +** You can define it to get all options, or change specific options +** to fit your specific needs. +*/ +#if defined(LUA_COMPAT_5_2) /* { */ + +/* +@@ LUA_COMPAT_MATHLIB controls the presence of several deprecated +** functions in the mathematical library. +*/ +#define LUA_COMPAT_MATHLIB + +/* +@@ LUA_COMPAT_BITLIB controls the presence of library 'bit32'. +*/ +#define LUA_COMPAT_BITLIB + +/* +@@ LUA_COMPAT_IPAIRS controls the effectiveness of the __ipairs metamethod. +*/ +#define LUA_COMPAT_IPAIRS + +/* +@@ LUA_COMPAT_APIINTCASTS controls the presence of macros for +** manipulating other integer types (lua_pushunsigned, lua_tounsigned, +** luaL_checkint, luaL_checklong, etc.) +*/ +#define LUA_COMPAT_APIINTCASTS + +#endif /* } */ + + +#if defined(LUA_COMPAT_5_1) /* { */ + +/* Incompatibilities from 5.2 -> 5.3 */ +#define LUA_COMPAT_MATHLIB +#define LUA_COMPAT_APIINTCASTS + +/* +@@ LUA_COMPAT_UNPACK controls the presence of global 'unpack'. +** You can replace it with 'table.unpack'. +*/ +#define LUA_COMPAT_UNPACK + +/* +@@ LUA_COMPAT_LOADERS controls the presence of table 'package.loaders'. +** You can replace it with 'package.searchers'. +*/ +#define LUA_COMPAT_LOADERS + +/* +@@ macro 'lua_cpcall' emulates deprecated function lua_cpcall. +** You can call your C function directly (with light C functions). +*/ +#define lua_cpcall(L,f,u) \ + (lua_pushcfunction(L, (f)), \ + lua_pushlightuserdata(L,(u)), \ + lua_pcall(L,1,0,0)) + + +/* +@@ LUA_COMPAT_LOG10 defines the function 'log10' in the math library. +** You can rewrite 'log10(x)' as 'log(x, 10)'. +*/ +#define LUA_COMPAT_LOG10 + +/* +@@ LUA_COMPAT_LOADSTRING defines the function 'loadstring' in the base +** library. You can rewrite 'loadstring(s)' as 'load(s)'. +*/ +#define LUA_COMPAT_LOADSTRING + +/* +@@ LUA_COMPAT_MAXN defines the function 'maxn' in the table library. +*/ +#define LUA_COMPAT_MAXN + +/* +@@ The following macros supply trivial compatibility for some +** changes in the API. The macros themselves document how to +** change your code to avoid using them. +*/ +#define lua_strlen(L,i) lua_rawlen(L, (i)) + +#define lua_objlen(L,i) lua_rawlen(L, (i)) + +#define lua_equal(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPEQ) +#define lua_lessthan(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPLT) + +/* +@@ LUA_COMPAT_MODULE controls compatibility with previous +** module functions 'module' (Lua) and 'luaL_register' (C). +*/ +#define LUA_COMPAT_MODULE + +#endif /* } */ + + +/* +@@ LUA_COMPAT_FLOATSTRING makes Lua format integral floats without a +@@ a float mark ('.0'). +** This macro is not on by default even in compatibility mode, +** because this is not really an incompatibility. +*/ +/* #define LUA_COMPAT_FLOATSTRING */ + +/* }================================================================== */ + + + +/* +** {================================================================== +** Configuration for Numbers. +** Change these definitions if no predefined LUA_FLOAT_* / LUA_INT_* +** satisfy your needs. +** =================================================================== +*/ + +/* +@@ LUA_NUMBER is the floating-point type used by Lua. +@@ LUAI_UACNUMBER is the result of a 'default argument promotion' +@@ over a floating number. +@@ l_mathlim(x) corrects limit name 'x' to the proper float type +** by prefixing it with one of FLT/DBL/LDBL. +@@ LUA_NUMBER_FRMLEN is the length modifier for writing floats. +@@ LUA_NUMBER_FMT is the format for writing floats. +@@ lua_number2str converts a float to a string. +@@ l_mathop allows the addition of an 'l' or 'f' to all math operations. +@@ l_floor takes the floor of a float. +@@ lua_str2number converts a decimal numeric string to a number. +*/ + + +/* The following definitions are good for most cases here */ + +#define l_floor(x) (l_mathop(floor)(x)) + +#define lua_number2str(s,sz,n) \ + l_sprintf((s), sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)(n)) + +/* +@@ lua_numbertointeger converts a float number to an integer, or +** returns 0 if float is not within the range of a lua_Integer. +** (The range comparisons are tricky because of rounding. The tests +** here assume a two-complement representation, where MININTEGER always +** has an exact representation as a float; MAXINTEGER may not have one, +** and therefore its conversion to float may have an ill-defined value.) +*/ +#define lua_numbertointeger(n,p) \ + ((n) >= (LUA_NUMBER)(LUA_MININTEGER) && \ + (n) < -(LUA_NUMBER)(LUA_MININTEGER) && \ + (*(p) = (LUA_INTEGER)(n), 1)) + + +/* now the variable definitions */ + +#if LUA_FLOAT_TYPE == LUA_FLOAT_FLOAT /* { single float */ + +#define LUA_NUMBER float + +#define l_mathlim(n) (FLT_##n) + +#define LUAI_UACNUMBER double + +#define LUA_NUMBER_FRMLEN "" +#define LUA_NUMBER_FMT "%.7g" + +#define l_mathop(op) op##f + +#define lua_str2number(s,p) strtof((s), (p)) + + +#elif LUA_FLOAT_TYPE == LUA_FLOAT_LONGDOUBLE /* }{ long double */ + +#define LUA_NUMBER long double + +#define l_mathlim(n) (LDBL_##n) + +#define LUAI_UACNUMBER long double + +#define LUA_NUMBER_FRMLEN "L" +#define LUA_NUMBER_FMT "%.19Lg" + +#define l_mathop(op) op##l + +#define lua_str2number(s,p) strtold((s), (p)) + +#elif LUA_FLOAT_TYPE == LUA_FLOAT_DOUBLE /* }{ double */ + +#define LUA_NUMBER double + +#define l_mathlim(n) (DBL_##n) + +#define LUAI_UACNUMBER double + +#define LUA_NUMBER_FRMLEN "" +#define LUA_NUMBER_FMT "%.14g" + +#define l_mathop(op) op + +#define lua_str2number(s,p) strtod((s), (p)) + +#else /* }{ */ + +#error "numeric float type not defined" + +#endif /* } */ + + + +/* +@@ LUA_INTEGER is the integer type used by Lua. +** +@@ LUA_UNSIGNED is the unsigned version of LUA_INTEGER. +** +@@ LUAI_UACINT is the result of a 'default argument promotion' +@@ over a lUA_INTEGER. +@@ LUA_INTEGER_FRMLEN is the length modifier for reading/writing integers. +@@ LUA_INTEGER_FMT is the format for writing integers. +@@ LUA_MAXINTEGER is the maximum value for a LUA_INTEGER. +@@ LUA_MININTEGER is the minimum value for a LUA_INTEGER. +@@ lua_integer2str converts an integer to a string. +*/ + + +/* The following definitions are good for most cases here */ + +#define LUA_INTEGER_FMT "%" LUA_INTEGER_FRMLEN "d" + +#define LUAI_UACINT LUA_INTEGER + +#define lua_integer2str(s,sz,n) \ + l_sprintf((s), sz, LUA_INTEGER_FMT, (LUAI_UACINT)(n)) + +/* +** use LUAI_UACINT here to avoid problems with promotions (which +** can turn a comparison between unsigneds into a signed comparison) +*/ +#define LUA_UNSIGNED unsigned LUAI_UACINT + + +/* now the variable definitions */ + +#if LUA_INT_TYPE == LUA_INT_INT /* { int */ + +#define LUA_INTEGER int +#define LUA_INTEGER_FRMLEN "" + +#define LUA_MAXINTEGER INT_MAX +#define LUA_MININTEGER INT_MIN + +#elif LUA_INT_TYPE == LUA_INT_LONG /* }{ long */ + +#define LUA_INTEGER long +#define LUA_INTEGER_FRMLEN "l" + +#define LUA_MAXINTEGER LONG_MAX +#define LUA_MININTEGER LONG_MIN + +#elif LUA_INT_TYPE == LUA_INT_LONGLONG /* }{ long long */ + +/* use presence of macro LLONG_MAX as proxy for C99 compliance */ +#if defined(LLONG_MAX) /* { */ +/* use ISO C99 stuff */ + +#define LUA_INTEGER long long +#define LUA_INTEGER_FRMLEN "ll" + +#define LUA_MAXINTEGER LLONG_MAX +#define LUA_MININTEGER LLONG_MIN + +#elif defined(LUA_USE_WINDOWS) /* }{ */ +/* in Windows, can use specific Windows types */ + +#define LUA_INTEGER __int64 +#define LUA_INTEGER_FRMLEN "I64" + +#define LUA_MAXINTEGER _I64_MAX +#define LUA_MININTEGER _I64_MIN + +#else /* }{ */ + +#error "Compiler does not support 'long long'. Use option '-DLUA_32BITS' \ + or '-DLUA_C89_NUMBERS' (see file 'luaconf.h' for details)" + +#endif /* } */ + +#else /* }{ */ + +#error "numeric integer type not defined" + +#endif /* } */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Dependencies with C99 and other C details +** =================================================================== +*/ + +/* +@@ l_sprintf is equivalent to 'snprintf' or 'sprintf' in C89. +** (All uses in Lua have only one format item.) +*/ +#if !defined(LUA_USE_C89) +#define l_sprintf(s,sz,f,i) snprintf(s,sz,f,i) +#else +#define l_sprintf(s,sz,f,i) ((void)(sz), sprintf(s,f,i)) +#endif + + +/* +@@ lua_strx2number converts an hexadecimal numeric string to a number. +** In C99, 'strtod' does that conversion. Otherwise, you can +** leave 'lua_strx2number' undefined and Lua will provide its own +** implementation. +*/ +#if !defined(LUA_USE_C89) +#define lua_strx2number(s,p) lua_str2number(s,p) +#endif + + +/* +@@ lua_number2strx converts a float to an hexadecimal numeric string. +** In C99, 'sprintf' (with format specifiers '%a'/'%A') does that. +** Otherwise, you can leave 'lua_number2strx' undefined and Lua will +** provide its own implementation. +*/ +#if !defined(LUA_USE_C89) +#define lua_number2strx(L,b,sz,f,n) \ + ((void)L, l_sprintf(b,sz,f,(LUAI_UACNUMBER)(n))) +#endif + + +/* +** 'strtof' and 'opf' variants for math functions are not valid in +** C89. Otherwise, the macro 'HUGE_VALF' is a good proxy for testing the +** availability of these variants. ('math.h' is already included in +** all files that use these macros.) +*/ +#if defined(LUA_USE_C89) || (defined(HUGE_VAL) && !defined(HUGE_VALF)) +#undef l_mathop /* variants not available */ +#undef lua_str2number +#define l_mathop(op) (lua_Number)op /* no variant */ +#define lua_str2number(s,p) ((lua_Number)strtod((s), (p))) +#endif + + +/* +@@ LUA_KCONTEXT is the type of the context ('ctx') for continuation +** functions. It must be a numerical type; Lua will use 'intptr_t' if +** available, otherwise it will use 'ptrdiff_t' (the nearest thing to +** 'intptr_t' in C89) +*/ +#define LUA_KCONTEXT ptrdiff_t + +#if !defined(LUA_USE_C89) && defined(__STDC_VERSION__) && \ + __STDC_VERSION__ >= 199901L +#include +#if defined(INTPTR_MAX) /* even in C99 this type is optional */ +#undef LUA_KCONTEXT +#define LUA_KCONTEXT intptr_t +#endif +#endif + + +/* +@@ lua_getlocaledecpoint gets the locale "radix character" (decimal point). +** Change that if you do not want to use C locales. (Code using this +** macro must include header 'locale.h'.) +*/ +#if !defined(lua_getlocaledecpoint) +#define lua_getlocaledecpoint() (localeconv()->decimal_point[0]) +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Language Variations +** ===================================================================== +*/ + +/* +@@ LUA_NOCVTN2S/LUA_NOCVTS2N control how Lua performs some +** coercions. Define LUA_NOCVTN2S to turn off automatic coercion from +** numbers to strings. Define LUA_NOCVTS2N to turn off automatic +** coercion from strings to numbers. +*/ +/* #define LUA_NOCVTN2S */ +/* #define LUA_NOCVTS2N */ + + +/* +@@ LUA_USE_APICHECK turns on several consistency checks on the C API. +** Define it as a help when debugging C code. +*/ +#if defined(LUA_USE_APICHECK) +#include +#define luai_apicheck(l,e) assert(e) +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Macros that affect the API and must be stable (that is, must be the +** same when you compile Lua and when you compile code that links to +** Lua). You probably do not want/need to change them. +** ===================================================================== +*/ + +/* +@@ LUAI_MAXSTACK limits the size of the Lua stack. +** CHANGE it if you need a different limit. This limit is arbitrary; +** its only purpose is to stop Lua from consuming unlimited stack +** space (and to reserve some numbers for pseudo-indices). +*/ +#if LUAI_BITSINT >= 32 +#define LUAI_MAXSTACK 1000000 +#else +#define LUAI_MAXSTACK 15000 +#endif + + +/* +@@ LUA_EXTRASPACE defines the size of a raw memory area associated with +** a Lua state with very fast access. +** CHANGE it if you need a different size. +*/ +#define LUA_EXTRASPACE (sizeof(void *)) + + +/* +@@ LUA_IDSIZE gives the maximum size for the description of the source +@@ of a function in debug information. +** CHANGE it if you want a different size. +*/ +#define LUA_IDSIZE 60 + + +/* +@@ LUAL_BUFFERSIZE is the buffer size used by the lauxlib buffer system. +** CHANGE it if it uses too much C-stack space. (For long double, +** 'string.format("%.99f", -1e4932)' needs 5034 bytes, so a +** smaller buffer would force a memory allocation for each call to +** 'string.format'.) +*/ +#if LUA_FLOAT_TYPE == LUA_FLOAT_LONGDOUBLE +#define LUAL_BUFFERSIZE 8192 +#else +#define LUAL_BUFFERSIZE ((int)(0x80 * sizeof(void*) * sizeof(lua_Integer))) +#endif + +/* }================================================================== */ + + +/* +@@ LUA_QL describes how error messages quote program elements. +** Lua does not use these macros anymore; they are here for +** compatibility only. +*/ +#define LUA_QL(x) "'" x "'" +#define LUA_QS LUA_QL("%s") + + + + +/* =================================================================== */ + +/* +** Local configuration. You can use this space to add your redefinitions +** without modifying the main part of the file. +*/ + + + + + +#endif + diff --git a/src/rcheevos/test/lua/src/lualib.h b/src/rcheevos/test/lua/src/lualib.h new file mode 100644 index 000000000..6c0bc4cb0 --- /dev/null +++ b/src/rcheevos/test/lua/src/lualib.h @@ -0,0 +1,61 @@ +/* +** $Id: lualib.h,v 1.45 2017/01/12 17:14:26 roberto Exp $ +** Lua standard libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lualib_h +#define lualib_h + +#include "lua.h" + + +/* version suffix for environment variable names */ +#define LUA_VERSUFFIX "_" LUA_VERSION_MAJOR "_" LUA_VERSION_MINOR + + +LUAMOD_API int (luaopen_base) (lua_State *L); + +#define LUA_COLIBNAME "coroutine" +LUAMOD_API int (luaopen_coroutine) (lua_State *L); + +#define LUA_TABLIBNAME "table" +LUAMOD_API int (luaopen_table) (lua_State *L); + +#define LUA_IOLIBNAME "io" +LUAMOD_API int (luaopen_io) (lua_State *L); + +#define LUA_OSLIBNAME "os" +LUAMOD_API int (luaopen_os) (lua_State *L); + +#define LUA_STRLIBNAME "string" +LUAMOD_API int (luaopen_string) (lua_State *L); + +#define LUA_UTF8LIBNAME "utf8" +LUAMOD_API int (luaopen_utf8) (lua_State *L); + +#define LUA_BITLIBNAME "bit32" +LUAMOD_API int (luaopen_bit32) (lua_State *L); + +#define LUA_MATHLIBNAME "math" +LUAMOD_API int (luaopen_math) (lua_State *L); + +#define LUA_DBLIBNAME "debug" +LUAMOD_API int (luaopen_debug) (lua_State *L); + +#define LUA_LOADLIBNAME "package" +LUAMOD_API int (luaopen_package) (lua_State *L); + + +/* open all previous libraries */ +LUALIB_API void (luaL_openlibs) (lua_State *L); + + + +#if !defined(lua_assert) +#define lua_assert(x) ((void)0) +#endif + + +#endif diff --git a/src/rcheevos/test/lua/src/lundump.c b/src/rcheevos/test/lua/src/lundump.c new file mode 100644 index 000000000..4080af9c0 --- /dev/null +++ b/src/rcheevos/test/lua/src/lundump.c @@ -0,0 +1,279 @@ +/* +** $Id: lundump.c,v 2.44 2015/11/02 16:09:30 roberto Exp $ +** load precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#define lundump_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstring.h" +#include "lundump.h" +#include "lzio.h" + + +#if !defined(luai_verifycode) +#define luai_verifycode(L,b,f) /* empty */ +#endif + + +typedef struct { + lua_State *L; + ZIO *Z; + const char *name; +} LoadState; + + +static l_noret error(LoadState *S, const char *why) { + luaO_pushfstring(S->L, "%s: %s precompiled chunk", S->name, why); + luaD_throw(S->L, LUA_ERRSYNTAX); +} + + +/* +** All high-level loads go through LoadVector; you can change it to +** adapt to the endianness of the input +*/ +#define LoadVector(S,b,n) LoadBlock(S,b,(n)*sizeof((b)[0])) + +static void LoadBlock (LoadState *S, void *b, size_t size) { + if (luaZ_read(S->Z, b, size) != 0) + error(S, "truncated"); +} + + +#define LoadVar(S,x) LoadVector(S,&x,1) + + +static lu_byte LoadByte (LoadState *S) { + lu_byte x; + LoadVar(S, x); + return x; +} + + +static int LoadInt (LoadState *S) { + int x; + LoadVar(S, x); + return x; +} + + +static lua_Number LoadNumber (LoadState *S) { + lua_Number x; + LoadVar(S, x); + return x; +} + + +static lua_Integer LoadInteger (LoadState *S) { + lua_Integer x; + LoadVar(S, x); + return x; +} + + +static TString *LoadString (LoadState *S) { + size_t size = LoadByte(S); + if (size == 0xFF) + LoadVar(S, size); + if (size == 0) + return NULL; + else if (--size <= LUAI_MAXSHORTLEN) { /* short string? */ + char buff[LUAI_MAXSHORTLEN]; + LoadVector(S, buff, size); + return luaS_newlstr(S->L, buff, size); + } + else { /* long string */ + TString *ts = luaS_createlngstrobj(S->L, size); + LoadVector(S, getstr(ts), size); /* load directly in final place */ + return ts; + } +} + + +static void LoadCode (LoadState *S, Proto *f) { + int n = LoadInt(S); + f->code = luaM_newvector(S->L, n, Instruction); + f->sizecode = n; + LoadVector(S, f->code, n); +} + + +static void LoadFunction(LoadState *S, Proto *f, TString *psource); + + +static void LoadConstants (LoadState *S, Proto *f) { + int i; + int n = LoadInt(S); + f->k = luaM_newvector(S->L, n, TValue); + f->sizek = n; + for (i = 0; i < n; i++) + setnilvalue(&f->k[i]); + for (i = 0; i < n; i++) { + TValue *o = &f->k[i]; + int t = LoadByte(S); + switch (t) { + case LUA_TNIL: + setnilvalue(o); + break; + case LUA_TBOOLEAN: + setbvalue(o, LoadByte(S)); + break; + case LUA_TNUMFLT: + setfltvalue(o, LoadNumber(S)); + break; + case LUA_TNUMINT: + setivalue(o, LoadInteger(S)); + break; + case LUA_TSHRSTR: + case LUA_TLNGSTR: + setsvalue2n(S->L, o, LoadString(S)); + break; + default: + lua_assert(0); + } + } +} + + +static void LoadProtos (LoadState *S, Proto *f) { + int i; + int n = LoadInt(S); + f->p = luaM_newvector(S->L, n, Proto *); + f->sizep = n; + for (i = 0; i < n; i++) + f->p[i] = NULL; + for (i = 0; i < n; i++) { + f->p[i] = luaF_newproto(S->L); + LoadFunction(S, f->p[i], f->source); + } +} + + +static void LoadUpvalues (LoadState *S, Proto *f) { + int i, n; + n = LoadInt(S); + f->upvalues = luaM_newvector(S->L, n, Upvaldesc); + f->sizeupvalues = n; + for (i = 0; i < n; i++) + f->upvalues[i].name = NULL; + for (i = 0; i < n; i++) { + f->upvalues[i].instack = LoadByte(S); + f->upvalues[i].idx = LoadByte(S); + } +} + + +static void LoadDebug (LoadState *S, Proto *f) { + int i, n; + n = LoadInt(S); + f->lineinfo = luaM_newvector(S->L, n, int); + f->sizelineinfo = n; + LoadVector(S, f->lineinfo, n); + n = LoadInt(S); + f->locvars = luaM_newvector(S->L, n, LocVar); + f->sizelocvars = n; + for (i = 0; i < n; i++) + f->locvars[i].varname = NULL; + for (i = 0; i < n; i++) { + f->locvars[i].varname = LoadString(S); + f->locvars[i].startpc = LoadInt(S); + f->locvars[i].endpc = LoadInt(S); + } + n = LoadInt(S); + for (i = 0; i < n; i++) + f->upvalues[i].name = LoadString(S); +} + + +static void LoadFunction (LoadState *S, Proto *f, TString *psource) { + f->source = LoadString(S); + if (f->source == NULL) /* no source in dump? */ + f->source = psource; /* reuse parent's source */ + f->linedefined = LoadInt(S); + f->lastlinedefined = LoadInt(S); + f->numparams = LoadByte(S); + f->is_vararg = LoadByte(S); + f->maxstacksize = LoadByte(S); + LoadCode(S, f); + LoadConstants(S, f); + LoadUpvalues(S, f); + LoadProtos(S, f); + LoadDebug(S, f); +} + + +static void checkliteral (LoadState *S, const char *s, const char *msg) { + char buff[sizeof(LUA_SIGNATURE) + sizeof(LUAC_DATA)]; /* larger than both */ + size_t len = strlen(s); + LoadVector(S, buff, len); + if (memcmp(s, buff, len) != 0) + error(S, msg); +} + + +static void fchecksize (LoadState *S, size_t size, const char *tname) { + if (LoadByte(S) != size) + error(S, luaO_pushfstring(S->L, "%s size mismatch in", tname)); +} + + +#define checksize(S,t) fchecksize(S,sizeof(t),#t) + +static void checkHeader (LoadState *S) { + checkliteral(S, LUA_SIGNATURE + 1, "not a"); /* 1st char already checked */ + if (LoadByte(S) != LUAC_VERSION) + error(S, "version mismatch in"); + if (LoadByte(S) != LUAC_FORMAT) + error(S, "format mismatch in"); + checkliteral(S, LUAC_DATA, "corrupted"); + checksize(S, int); + checksize(S, size_t); + checksize(S, Instruction); + checksize(S, lua_Integer); + checksize(S, lua_Number); + if (LoadInteger(S) != LUAC_INT) + error(S, "endianness mismatch in"); + if (LoadNumber(S) != LUAC_NUM) + error(S, "float format mismatch in"); +} + + +/* +** load precompiled chunk +*/ +LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) { + LoadState S; + LClosure *cl; + if (*name == '@' || *name == '=') + S.name = name + 1; + else if (*name == LUA_SIGNATURE[0]) + S.name = "binary string"; + else + S.name = name; + S.L = L; + S.Z = Z; + checkHeader(&S); + cl = luaF_newLclosure(L, LoadByte(&S)); + setclLvalue(L, L->top, cl); + luaD_inctop(L); + cl->p = luaF_newproto(L); + LoadFunction(&S, cl->p, NULL); + lua_assert(cl->nupvalues == cl->p->sizeupvalues); + luai_verifycode(L, buff, cl->p); + return cl; +} + diff --git a/src/rcheevos/test/lua/src/lundump.h b/src/rcheevos/test/lua/src/lundump.h new file mode 100644 index 000000000..aa5cc82f1 --- /dev/null +++ b/src/rcheevos/test/lua/src/lundump.h @@ -0,0 +1,32 @@ +/* +** $Id: lundump.h,v 1.45 2015/09/08 15:41:05 roberto Exp $ +** load precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#ifndef lundump_h +#define lundump_h + +#include "llimits.h" +#include "lobject.h" +#include "lzio.h" + + +/* data to catch conversion errors */ +#define LUAC_DATA "\x19\x93\r\n\x1a\n" + +#define LUAC_INT 0x5678 +#define LUAC_NUM cast_num(370.5) + +#define MYINT(s) (s[0]-'0') +#define LUAC_VERSION (MYINT(LUA_VERSION_MAJOR)*16+MYINT(LUA_VERSION_MINOR)) +#define LUAC_FORMAT 0 /* this is the official format */ + +/* load one chunk; from lundump.c */ +LUAI_FUNC LClosure* luaU_undump (lua_State* L, ZIO* Z, const char* name); + +/* dump one chunk; from ldump.c */ +LUAI_FUNC int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, + void* data, int strip); + +#endif diff --git a/src/rcheevos/test/lua/src/lutf8lib.c b/src/rcheevos/test/lua/src/lutf8lib.c new file mode 100644 index 000000000..de9e3dcdd --- /dev/null +++ b/src/rcheevos/test/lua/src/lutf8lib.c @@ -0,0 +1,256 @@ +/* +** $Id: lutf8lib.c,v 1.16 2016/12/22 13:08:50 roberto Exp $ +** Standard library for UTF-8 manipulation +** See Copyright Notice in lua.h +*/ + +#define lutf8lib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + +#define MAXUNICODE 0x10FFFF + +#define iscont(p) ((*(p) & 0xC0) == 0x80) + + +/* from strlib */ +/* translate a relative string position: negative means back from end */ +static lua_Integer u_posrelat (lua_Integer pos, size_t len) { + if (pos >= 0) return pos; + else if (0u - (size_t)pos > len) return 0; + else return (lua_Integer)len + pos + 1; +} + + +/* +** Decode one UTF-8 sequence, returning NULL if byte sequence is invalid. +*/ +static const char *utf8_decode (const char *o, int *val) { + static const unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF}; + const unsigned char *s = (const unsigned char *)o; + unsigned int c = s[0]; + unsigned int res = 0; /* final result */ + if (c < 0x80) /* ascii? */ + res = c; + else { + int count = 0; /* to count number of continuation bytes */ + while (c & 0x40) { /* still have continuation bytes? */ + int cc = s[++count]; /* read next byte */ + if ((cc & 0xC0) != 0x80) /* not a continuation byte? */ + return NULL; /* invalid byte sequence */ + res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ + c <<= 1; /* to test next bit */ + } + res |= ((c & 0x7F) << (count * 5)); /* add first byte */ + if (count > 3 || res > MAXUNICODE || res <= limits[count]) + return NULL; /* invalid byte sequence */ + s += count; /* skip continuation bytes read */ + } + if (val) *val = res; + return (const char *)s + 1; /* +1 to include first byte */ +} + + +/* +** utf8len(s [, i [, j]]) --> number of characters that start in the +** range [i,j], or nil + current position if 's' is not well formed in +** that interval +*/ +static int utflen (lua_State *L) { + int n = 0; + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer posi = u_posrelat(luaL_optinteger(L, 2, 1), len); + lua_Integer posj = u_posrelat(luaL_optinteger(L, 3, -1), len); + luaL_argcheck(L, 1 <= posi && --posi <= (lua_Integer)len, 2, + "initial position out of string"); + luaL_argcheck(L, --posj < (lua_Integer)len, 3, + "final position out of string"); + while (posi <= posj) { + const char *s1 = utf8_decode(s + posi, NULL); + if (s1 == NULL) { /* conversion error? */ + lua_pushnil(L); /* return nil ... */ + lua_pushinteger(L, posi + 1); /* ... and current position */ + return 2; + } + posi = s1 - s; + n++; + } + lua_pushinteger(L, n); + return 1; +} + + +/* +** codepoint(s, [i, [j]]) -> returns codepoints for all characters +** that start in the range [i,j] +*/ +static int codepoint (lua_State *L) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer posi = u_posrelat(luaL_optinteger(L, 2, 1), len); + lua_Integer pose = u_posrelat(luaL_optinteger(L, 3, posi), len); + int n; + const char *se; + luaL_argcheck(L, posi >= 1, 2, "out of range"); + luaL_argcheck(L, pose <= (lua_Integer)len, 3, "out of range"); + if (posi > pose) return 0; /* empty interval; return no values */ + if (pose - posi >= INT_MAX) /* (lua_Integer -> int) overflow? */ + return luaL_error(L, "string slice too long"); + n = (int)(pose - posi) + 1; + luaL_checkstack(L, n, "string slice too long"); + n = 0; + se = s + pose; + for (s += posi - 1; s < se;) { + int code; + s = utf8_decode(s, &code); + if (s == NULL) + return luaL_error(L, "invalid UTF-8 code"); + lua_pushinteger(L, code); + n++; + } + return n; +} + + +static void pushutfchar (lua_State *L, int arg) { + lua_Integer code = luaL_checkinteger(L, arg); + luaL_argcheck(L, 0 <= code && code <= MAXUNICODE, arg, "value out of range"); + lua_pushfstring(L, "%U", (long)code); +} + + +/* +** utfchar(n1, n2, ...) -> char(n1)..char(n2)... +*/ +static int utfchar (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + if (n == 1) /* optimize common case of single char */ + pushutfchar(L, 1); + else { + int i; + luaL_Buffer b; + luaL_buffinit(L, &b); + for (i = 1; i <= n; i++) { + pushutfchar(L, i); + luaL_addvalue(&b); + } + luaL_pushresult(&b); + } + return 1; +} + + +/* +** offset(s, n, [i]) -> index where n-th character counting from +** position 'i' starts; 0 means character at 'i'. +*/ +static int byteoffset (lua_State *L) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer n = luaL_checkinteger(L, 2); + lua_Integer posi = (n >= 0) ? 1 : len + 1; + posi = u_posrelat(luaL_optinteger(L, 3, posi), len); + luaL_argcheck(L, 1 <= posi && --posi <= (lua_Integer)len, 3, + "position out of range"); + if (n == 0) { + /* find beginning of current byte sequence */ + while (posi > 0 && iscont(s + posi)) posi--; + } + else { + if (iscont(s + posi)) + luaL_error(L, "initial position is a continuation byte"); + if (n < 0) { + while (n < 0 && posi > 0) { /* move back */ + do { /* find beginning of previous character */ + posi--; + } while (posi > 0 && iscont(s + posi)); + n++; + } + } + else { + n--; /* do not move for 1st character */ + while (n > 0 && posi < (lua_Integer)len) { + do { /* find beginning of next character */ + posi++; + } while (iscont(s + posi)); /* (cannot pass final '\0') */ + n--; + } + } + } + if (n == 0) /* did it find given character? */ + lua_pushinteger(L, posi + 1); + else /* no such character */ + lua_pushnil(L); + return 1; +} + + +static int iter_aux (lua_State *L) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer n = lua_tointeger(L, 2) - 1; + if (n < 0) /* first iteration? */ + n = 0; /* start from here */ + else if (n < (lua_Integer)len) { + n++; /* skip current byte */ + while (iscont(s + n)) n++; /* and its continuations */ + } + if (n >= (lua_Integer)len) + return 0; /* no more codepoints */ + else { + int code; + const char *next = utf8_decode(s + n, &code); + if (next == NULL || iscont(next)) + return luaL_error(L, "invalid UTF-8 code"); + lua_pushinteger(L, n + 1); + lua_pushinteger(L, code); + return 2; + } +} + + +static int iter_codes (lua_State *L) { + luaL_checkstring(L, 1); + lua_pushcfunction(L, iter_aux); + lua_pushvalue(L, 1); + lua_pushinteger(L, 0); + return 3; +} + + +/* pattern to match a single UTF-8 character */ +#define UTF8PATT "[\0-\x7F\xC2-\xF4][\x80-\xBF]*" + + +static const luaL_Reg funcs[] = { + {"offset", byteoffset}, + {"codepoint", codepoint}, + {"char", utfchar}, + {"len", utflen}, + {"codes", iter_codes}, + /* placeholders */ + {"charpattern", NULL}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_utf8 (lua_State *L) { + luaL_newlib(L, funcs); + lua_pushlstring(L, UTF8PATT, sizeof(UTF8PATT)/sizeof(char) - 1); + lua_setfield(L, -2, "charpattern"); + return 1; +} + diff --git a/src/rcheevos/test/lua/src/lvm.c b/src/rcheevos/test/lua/src/lvm.c new file mode 100644 index 000000000..84ade6b2f --- /dev/null +++ b/src/rcheevos/test/lua/src/lvm.c @@ -0,0 +1,1322 @@ +/* +** $Id: lvm.c,v 2.268 2016/02/05 19:59:14 roberto Exp $ +** Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#define lvm_c +#define LUA_CORE + +#include "lprefix.h" + +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + +/* limit for table tag-method chains (to avoid loops) */ +#define MAXTAGLOOP 2000 + + + +/* +** 'l_intfitsf' checks whether a given integer can be converted to a +** float without rounding. Used in comparisons. Left undefined if +** all integers fit in a float precisely. +*/ +#if !defined(l_intfitsf) + +/* number of bits in the mantissa of a float */ +#define NBM (l_mathlim(MANT_DIG)) + +/* +** Check whether some integers may not fit in a float, that is, whether +** (maxinteger >> NBM) > 0 (that implies (1 << NBM) <= maxinteger). +** (The shifts are done in parts to avoid shifting by more than the size +** of an integer. In a worst case, NBM == 113 for long double and +** sizeof(integer) == 32.) +*/ +#if ((((LUA_MAXINTEGER >> (NBM / 4)) >> (NBM / 4)) >> (NBM / 4)) \ + >> (NBM - (3 * (NBM / 4)))) > 0 + +#define l_intfitsf(i) \ + (-((lua_Integer)1 << NBM) <= (i) && (i) <= ((lua_Integer)1 << NBM)) + +#endif + +#endif + + + +/* +** Try to convert a value to a float. The float case is already handled +** by the macro 'tonumber'. +*/ +int luaV_tonumber_ (const TValue *obj, lua_Number *n) { + TValue v; + if (ttisinteger(obj)) { + *n = cast_num(ivalue(obj)); + return 1; + } + else if (cvt2num(obj) && /* string convertible to number? */ + luaO_str2num(svalue(obj), &v) == vslen(obj) + 1) { + *n = nvalue(&v); /* convert result of 'luaO_str2num' to a float */ + return 1; + } + else + return 0; /* conversion failed */ +} + + +/* +** try to convert a value to an integer, rounding according to 'mode': +** mode == 0: accepts only integral values +** mode == 1: takes the floor of the number +** mode == 2: takes the ceil of the number +*/ +int luaV_tointeger (const TValue *obj, lua_Integer *p, int mode) { + TValue v; + again: + if (ttisfloat(obj)) { + lua_Number n = fltvalue(obj); + lua_Number f = l_floor(n); + if (n != f) { /* not an integral value? */ + if (mode == 0) return 0; /* fails if mode demands integral value */ + else if (mode > 1) /* needs ceil? */ + f += 1; /* convert floor to ceil (remember: n != f) */ + } + return lua_numbertointeger(f, p); + } + else if (ttisinteger(obj)) { + *p = ivalue(obj); + return 1; + } + else if (cvt2num(obj) && + luaO_str2num(svalue(obj), &v) == vslen(obj) + 1) { + obj = &v; + goto again; /* convert result from 'luaO_str2num' to an integer */ + } + return 0; /* conversion failed */ +} + + +/* +** Try to convert a 'for' limit to an integer, preserving the +** semantics of the loop. +** (The following explanation assumes a non-negative step; it is valid +** for negative steps mutatis mutandis.) +** If the limit can be converted to an integer, rounding down, that is +** it. +** Otherwise, check whether the limit can be converted to a number. If +** the number is too large, it is OK to set the limit as LUA_MAXINTEGER, +** which means no limit. If the number is too negative, the loop +** should not run, because any initial integer value is larger than the +** limit. So, it sets the limit to LUA_MININTEGER. 'stopnow' corrects +** the extreme case when the initial value is LUA_MININTEGER, in which +** case the LUA_MININTEGER limit would still run the loop once. +*/ +static int forlimit (const TValue *obj, lua_Integer *p, lua_Integer step, + int *stopnow) { + *stopnow = 0; /* usually, let loops run */ + if (!luaV_tointeger(obj, p, (step < 0 ? 2 : 1))) { /* not fit in integer? */ + lua_Number n; /* try to convert to float */ + if (!tonumber(obj, &n)) /* cannot convert to float? */ + return 0; /* not a number */ + if (luai_numlt(0, n)) { /* if true, float is larger than max integer */ + *p = LUA_MAXINTEGER; + if (step < 0) *stopnow = 1; + } + else { /* float is smaller than min integer */ + *p = LUA_MININTEGER; + if (step >= 0) *stopnow = 1; + } + } + return 1; +} + + +/* +** Finish the table access 'val = t[key]'. +** if 'slot' is NULL, 't' is not a table; otherwise, 'slot' points to +** t[k] entry (which must be nil). +*/ +void luaV_finishget (lua_State *L, const TValue *t, TValue *key, StkId val, + const TValue *slot) { + int loop; /* counter to avoid infinite loops */ + const TValue *tm; /* metamethod */ + for (loop = 0; loop < MAXTAGLOOP; loop++) { + if (slot == NULL) { /* 't' is not a table? */ + lua_assert(!ttistable(t)); + tm = luaT_gettmbyobj(L, t, TM_INDEX); + if (ttisnil(tm)) + luaG_typeerror(L, t, "index"); /* no metamethod */ + /* else will try the metamethod */ + } + else { /* 't' is a table */ + lua_assert(ttisnil(slot)); + tm = fasttm(L, hvalue(t)->metatable, TM_INDEX); /* table's metamethod */ + if (tm == NULL) { /* no metamethod? */ + setnilvalue(val); /* result is nil */ + return; + } + /* else will try the metamethod */ + } + if (ttisfunction(tm)) { /* is metamethod a function? */ + luaT_callTM(L, tm, t, key, val, 1); /* call it */ + return; + } + t = tm; /* else try to access 'tm[key]' */ + if (luaV_fastget(L,t,key,slot,luaH_get)) { /* fast track? */ + setobj2s(L, val, slot); /* done */ + return; + } + /* else repeat (tail call 'luaV_finishget') */ + } + luaG_runerror(L, "'__index' chain too long; possible loop"); +} + + +/* +** Finish a table assignment 't[key] = val'. +** If 'slot' is NULL, 't' is not a table. Otherwise, 'slot' points +** to the entry 't[key]', or to 'luaO_nilobject' if there is no such +** entry. (The value at 'slot' must be nil, otherwise 'luaV_fastset' +** would have done the job.) +*/ +void luaV_finishset (lua_State *L, const TValue *t, TValue *key, + StkId val, const TValue *slot) { + int loop; /* counter to avoid infinite loops */ + for (loop = 0; loop < MAXTAGLOOP; loop++) { + const TValue *tm; /* '__newindex' metamethod */ + if (slot != NULL) { /* is 't' a table? */ + Table *h = hvalue(t); /* save 't' table */ + lua_assert(ttisnil(slot)); /* old value must be nil */ + tm = fasttm(L, h->metatable, TM_NEWINDEX); /* get metamethod */ + if (tm == NULL) { /* no metamethod? */ + if (slot == luaO_nilobject) /* no previous entry? */ + slot = luaH_newkey(L, h, key); /* create one */ + /* no metamethod and (now) there is an entry with given key */ + setobj2t(L, cast(TValue *, slot), val); /* set its new value */ + invalidateTMcache(h); + luaC_barrierback(L, h, val); + return; + } + /* else will try the metamethod */ + } + else { /* not a table; check metamethod */ + if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_NEWINDEX))) + luaG_typeerror(L, t, "index"); + } + /* try the metamethod */ + if (ttisfunction(tm)) { + luaT_callTM(L, tm, t, key, val, 0); + return; + } + t = tm; /* else repeat assignment over 'tm' */ + if (luaV_fastset(L, t, key, slot, luaH_get, val)) + return; /* done */ + /* else loop */ + } + luaG_runerror(L, "'__newindex' chain too long; possible loop"); +} + + +/* +** Compare two strings 'ls' x 'rs', returning an integer smaller-equal- +** -larger than zero if 'ls' is smaller-equal-larger than 'rs'. +** The code is a little tricky because it allows '\0' in the strings +** and it uses 'strcoll' (to respect locales) for each segments +** of the strings. +*/ +static int l_strcmp (const TString *ls, const TString *rs) { + const char *l = getstr(ls); + size_t ll = tsslen(ls); + const char *r = getstr(rs); + size_t lr = tsslen(rs); + for (;;) { /* for each segment */ + int temp = strcoll(l, r); + if (temp != 0) /* not equal? */ + return temp; /* done */ + else { /* strings are equal up to a '\0' */ + size_t len = strlen(l); /* index of first '\0' in both strings */ + if (len == lr) /* 'rs' is finished? */ + return (len == ll) ? 0 : 1; /* check 'ls' */ + else if (len == ll) /* 'ls' is finished? */ + return -1; /* 'ls' is smaller than 'rs' ('rs' is not finished) */ + /* both strings longer than 'len'; go on comparing after the '\0' */ + len++; + l += len; ll -= len; r += len; lr -= len; + } + } +} + + +/* +** Check whether integer 'i' is less than float 'f'. If 'i' has an +** exact representation as a float ('l_intfitsf'), compare numbers as +** floats. Otherwise, if 'f' is outside the range for integers, result +** is trivial. Otherwise, compare them as integers. (When 'i' has no +** float representation, either 'f' is "far away" from 'i' or 'f' has +** no precision left for a fractional part; either way, how 'f' is +** truncated is irrelevant.) When 'f' is NaN, comparisons must result +** in false. +*/ +static int LTintfloat (lua_Integer i, lua_Number f) { +#if defined(l_intfitsf) + if (!l_intfitsf(i)) { + if (f >= -cast_num(LUA_MININTEGER)) /* -minint == maxint + 1 */ + return 1; /* f >= maxint + 1 > i */ + else if (f > cast_num(LUA_MININTEGER)) /* minint < f <= maxint ? */ + return (i < cast(lua_Integer, f)); /* compare them as integers */ + else /* f <= minint <= i (or 'f' is NaN) --> not(i < f) */ + return 0; + } +#endif + return luai_numlt(cast_num(i), f); /* compare them as floats */ +} + + +/* +** Check whether integer 'i' is less than or equal to float 'f'. +** See comments on previous function. +*/ +static int LEintfloat (lua_Integer i, lua_Number f) { +#if defined(l_intfitsf) + if (!l_intfitsf(i)) { + if (f >= -cast_num(LUA_MININTEGER)) /* -minint == maxint + 1 */ + return 1; /* f >= maxint + 1 > i */ + else if (f >= cast_num(LUA_MININTEGER)) /* minint <= f <= maxint ? */ + return (i <= cast(lua_Integer, f)); /* compare them as integers */ + else /* f < minint <= i (or 'f' is NaN) --> not(i <= f) */ + return 0; + } +#endif + return luai_numle(cast_num(i), f); /* compare them as floats */ +} + + +/* +** Return 'l < r', for numbers. +*/ +static int LTnum (const TValue *l, const TValue *r) { + if (ttisinteger(l)) { + lua_Integer li = ivalue(l); + if (ttisinteger(r)) + return li < ivalue(r); /* both are integers */ + else /* 'l' is int and 'r' is float */ + return LTintfloat(li, fltvalue(r)); /* l < r ? */ + } + else { + lua_Number lf = fltvalue(l); /* 'l' must be float */ + if (ttisfloat(r)) + return luai_numlt(lf, fltvalue(r)); /* both are float */ + else if (luai_numisnan(lf)) /* 'r' is int and 'l' is float */ + return 0; /* NaN < i is always false */ + else /* without NaN, (l < r) <--> not(r <= l) */ + return !LEintfloat(ivalue(r), lf); /* not (r <= l) ? */ + } +} + + +/* +** Return 'l <= r', for numbers. +*/ +static int LEnum (const TValue *l, const TValue *r) { + if (ttisinteger(l)) { + lua_Integer li = ivalue(l); + if (ttisinteger(r)) + return li <= ivalue(r); /* both are integers */ + else /* 'l' is int and 'r' is float */ + return LEintfloat(li, fltvalue(r)); /* l <= r ? */ + } + else { + lua_Number lf = fltvalue(l); /* 'l' must be float */ + if (ttisfloat(r)) + return luai_numle(lf, fltvalue(r)); /* both are float */ + else if (luai_numisnan(lf)) /* 'r' is int and 'l' is float */ + return 0; /* NaN <= i is always false */ + else /* without NaN, (l <= r) <--> not(r < l) */ + return !LTintfloat(ivalue(r), lf); /* not (r < l) ? */ + } +} + + +/* +** Main operation less than; return 'l < r'. +*/ +int luaV_lessthan (lua_State *L, const TValue *l, const TValue *r) { + int res; + if (ttisnumber(l) && ttisnumber(r)) /* both operands are numbers? */ + return LTnum(l, r); + else if (ttisstring(l) && ttisstring(r)) /* both are strings? */ + return l_strcmp(tsvalue(l), tsvalue(r)) < 0; + else if ((res = luaT_callorderTM(L, l, r, TM_LT)) < 0) /* no metamethod? */ + luaG_ordererror(L, l, r); /* error */ + return res; +} + + +/* +** Main operation less than or equal to; return 'l <= r'. If it needs +** a metamethod and there is no '__le', try '__lt', based on +** l <= r iff !(r < l) (assuming a total order). If the metamethod +** yields during this substitution, the continuation has to know +** about it (to negate the result of r= 0) /* try 'le' */ + return res; + else { /* try 'lt': */ + L->ci->callstatus |= CIST_LEQ; /* mark it is doing 'lt' for 'le' */ + res = luaT_callorderTM(L, r, l, TM_LT); + L->ci->callstatus ^= CIST_LEQ; /* clear mark */ + if (res < 0) + luaG_ordererror(L, l, r); + return !res; /* result is negated */ + } +} + + +/* +** Main operation for equality of Lua values; return 't1 == t2'. +** L == NULL means raw equality (no metamethods) +*/ +int luaV_equalobj (lua_State *L, const TValue *t1, const TValue *t2) { + const TValue *tm; + if (ttype(t1) != ttype(t2)) { /* not the same variant? */ + if (ttnov(t1) != ttnov(t2) || ttnov(t1) != LUA_TNUMBER) + return 0; /* only numbers can be equal with different variants */ + else { /* two numbers with different variants */ + lua_Integer i1, i2; /* compare them as integers */ + return (tointeger(t1, &i1) && tointeger(t2, &i2) && i1 == i2); + } + } + /* values have same type and same variant */ + switch (ttype(t1)) { + case LUA_TNIL: return 1; + case LUA_TNUMINT: return (ivalue(t1) == ivalue(t2)); + case LUA_TNUMFLT: return luai_numeq(fltvalue(t1), fltvalue(t2)); + case LUA_TBOOLEAN: return bvalue(t1) == bvalue(t2); /* true must be 1 !! */ + case LUA_TLIGHTUSERDATA: return pvalue(t1) == pvalue(t2); + case LUA_TLCF: return fvalue(t1) == fvalue(t2); + case LUA_TSHRSTR: return eqshrstr(tsvalue(t1), tsvalue(t2)); + case LUA_TLNGSTR: return luaS_eqlngstr(tsvalue(t1), tsvalue(t2)); + case LUA_TUSERDATA: { + if (uvalue(t1) == uvalue(t2)) return 1; + else if (L == NULL) return 0; + tm = fasttm(L, uvalue(t1)->metatable, TM_EQ); + if (tm == NULL) + tm = fasttm(L, uvalue(t2)->metatable, TM_EQ); + break; /* will try TM */ + } + case LUA_TTABLE: { + if (hvalue(t1) == hvalue(t2)) return 1; + else if (L == NULL) return 0; + tm = fasttm(L, hvalue(t1)->metatable, TM_EQ); + if (tm == NULL) + tm = fasttm(L, hvalue(t2)->metatable, TM_EQ); + break; /* will try TM */ + } + default: + return gcvalue(t1) == gcvalue(t2); + } + if (tm == NULL) /* no TM? */ + return 0; /* objects are different */ + luaT_callTM(L, tm, t1, t2, L->top, 1); /* call TM */ + return !l_isfalse(L->top); +} + + +/* macro used by 'luaV_concat' to ensure that element at 'o' is a string */ +#define tostring(L,o) \ + (ttisstring(o) || (cvt2str(o) && (luaO_tostring(L, o), 1))) + +#define isemptystr(o) (ttisshrstring(o) && tsvalue(o)->shrlen == 0) + +/* copy strings in stack from top - n up to top - 1 to buffer */ +static void copy2buff (StkId top, int n, char *buff) { + size_t tl = 0; /* size already copied */ + do { + size_t l = vslen(top - n); /* length of string being copied */ + memcpy(buff + tl, svalue(top - n), l * sizeof(char)); + tl += l; + } while (--n > 0); +} + + +/* +** Main operation for concatenation: concat 'total' values in the stack, +** from 'L->top - total' up to 'L->top - 1'. +*/ +void luaV_concat (lua_State *L, int total) { + lua_assert(total >= 2); + do { + StkId top = L->top; + int n = 2; /* number of elements handled in this pass (at least 2) */ + if (!(ttisstring(top-2) || cvt2str(top-2)) || !tostring(L, top-1)) + luaT_trybinTM(L, top-2, top-1, top-2, TM_CONCAT); + else if (isemptystr(top - 1)) /* second operand is empty? */ + cast_void(tostring(L, top - 2)); /* result is first operand */ + else if (isemptystr(top - 2)) { /* first operand is an empty string? */ + setobjs2s(L, top - 2, top - 1); /* result is second op. */ + } + else { + /* at least two non-empty string values; get as many as possible */ + size_t tl = vslen(top - 1); + TString *ts; + /* collect total length and number of strings */ + for (n = 1; n < total && tostring(L, top - n - 1); n++) { + size_t l = vslen(top - n - 1); + if (l >= (MAX_SIZE/sizeof(char)) - tl) + luaG_runerror(L, "string length overflow"); + tl += l; + } + if (tl <= LUAI_MAXSHORTLEN) { /* is result a short string? */ + char buff[LUAI_MAXSHORTLEN]; + copy2buff(top, n, buff); /* copy strings to buffer */ + ts = luaS_newlstr(L, buff, tl); + } + else { /* long string; copy strings directly to final result */ + ts = luaS_createlngstrobj(L, tl); + copy2buff(top, n, getstr(ts)); + } + setsvalue2s(L, top - n, ts); /* create result */ + } + total -= n-1; /* got 'n' strings to create 1 new */ + L->top -= n-1; /* popped 'n' strings and pushed one */ + } while (total > 1); /* repeat until only 1 result left */ +} + + +/* +** Main operation 'ra' = #rb'. +*/ +void luaV_objlen (lua_State *L, StkId ra, const TValue *rb) { + const TValue *tm; + switch (ttype(rb)) { + case LUA_TTABLE: { + Table *h = hvalue(rb); + tm = fasttm(L, h->metatable, TM_LEN); + if (tm) break; /* metamethod? break switch to call it */ + setivalue(ra, luaH_getn(h)); /* else primitive len */ + return; + } + case LUA_TSHRSTR: { + setivalue(ra, tsvalue(rb)->shrlen); + return; + } + case LUA_TLNGSTR: { + setivalue(ra, tsvalue(rb)->u.lnglen); + return; + } + default: { /* try metamethod */ + tm = luaT_gettmbyobj(L, rb, TM_LEN); + if (ttisnil(tm)) /* no metamethod? */ + luaG_typeerror(L, rb, "get length of"); + break; + } + } + luaT_callTM(L, tm, rb, rb, ra, 1); +} + + +/* +** Integer division; return 'm // n', that is, floor(m/n). +** C division truncates its result (rounds towards zero). +** 'floor(q) == trunc(q)' when 'q >= 0' or when 'q' is integer, +** otherwise 'floor(q) == trunc(q) - 1'. +*/ +lua_Integer luaV_div (lua_State *L, lua_Integer m, lua_Integer n) { + if (l_castS2U(n) + 1u <= 1u) { /* special cases: -1 or 0 */ + if (n == 0) + luaG_runerror(L, "attempt to divide by zero"); + return intop(-, 0, m); /* n==-1; avoid overflow with 0x80000...//-1 */ + } + else { + lua_Integer q = m / n; /* perform C division */ + if ((m ^ n) < 0 && m % n != 0) /* 'm/n' would be negative non-integer? */ + q -= 1; /* correct result for different rounding */ + return q; + } +} + + +/* +** Integer modulus; return 'm % n'. (Assume that C '%' with +** negative operands follows C99 behavior. See previous comment +** about luaV_div.) +*/ +lua_Integer luaV_mod (lua_State *L, lua_Integer m, lua_Integer n) { + if (l_castS2U(n) + 1u <= 1u) { /* special cases: -1 or 0 */ + if (n == 0) + luaG_runerror(L, "attempt to perform 'n%%0'"); + return 0; /* m % -1 == 0; avoid overflow with 0x80000...%-1 */ + } + else { + lua_Integer r = m % n; + if (r != 0 && (m ^ n) < 0) /* 'm/n' would be non-integer negative? */ + r += n; /* correct result for different rounding */ + return r; + } +} + + +/* number of bits in an integer */ +#define NBITS cast_int(sizeof(lua_Integer) * CHAR_BIT) + +/* +** Shift left operation. (Shift right just negates 'y'.) +*/ +lua_Integer luaV_shiftl (lua_Integer x, lua_Integer y) { + if (y < 0) { /* shift right? */ + if (y <= -NBITS) return 0; + else return intop(>>, x, -y); + } + else { /* shift left */ + if (y >= NBITS) return 0; + else return intop(<<, x, y); + } +} + + +/* +** check whether cached closure in prototype 'p' may be reused, that is, +** whether there is a cached closure with the same upvalues needed by +** new closure to be created. +*/ +static LClosure *getcached (Proto *p, UpVal **encup, StkId base) { + LClosure *c = p->cache; + if (c != NULL) { /* is there a cached closure? */ + int nup = p->sizeupvalues; + Upvaldesc *uv = p->upvalues; + int i; + for (i = 0; i < nup; i++) { /* check whether it has right upvalues */ + TValue *v = uv[i].instack ? base + uv[i].idx : encup[uv[i].idx]->v; + if (c->upvals[i]->v != v) + return NULL; /* wrong upvalue; cannot reuse closure */ + } + } + return c; /* return cached closure (or NULL if no cached closure) */ +} + + +/* +** create a new Lua closure, push it in the stack, and initialize +** its upvalues. Note that the closure is not cached if prototype is +** already black (which means that 'cache' was already cleared by the +** GC). +*/ +static void pushclosure (lua_State *L, Proto *p, UpVal **encup, StkId base, + StkId ra) { + int nup = p->sizeupvalues; + Upvaldesc *uv = p->upvalues; + int i; + LClosure *ncl = luaF_newLclosure(L, nup); + ncl->p = p; + setclLvalue(L, ra, ncl); /* anchor new closure in stack */ + for (i = 0; i < nup; i++) { /* fill in its upvalues */ + if (uv[i].instack) /* upvalue refers to local variable? */ + ncl->upvals[i] = luaF_findupval(L, base + uv[i].idx); + else /* get upvalue from enclosing function */ + ncl->upvals[i] = encup[uv[i].idx]; + ncl->upvals[i]->refcount++; + /* new closure is white, so we do not need a barrier here */ + } + if (!isblack(p)) /* cache will not break GC invariant? */ + p->cache = ncl; /* save it on cache for reuse */ +} + + +/* +** finish execution of an opcode interrupted by an yield +*/ +void luaV_finishOp (lua_State *L) { + CallInfo *ci = L->ci; + StkId base = ci->u.l.base; + Instruction inst = *(ci->u.l.savedpc - 1); /* interrupted instruction */ + OpCode op = GET_OPCODE(inst); + switch (op) { /* finish its execution */ + case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_IDIV: + case OP_BAND: case OP_BOR: case OP_BXOR: case OP_SHL: case OP_SHR: + case OP_MOD: case OP_POW: + case OP_UNM: case OP_BNOT: case OP_LEN: + case OP_GETTABUP: case OP_GETTABLE: case OP_SELF: { + setobjs2s(L, base + GETARG_A(inst), --L->top); + break; + } + case OP_LE: case OP_LT: case OP_EQ: { + int res = !l_isfalse(L->top - 1); + L->top--; + if (ci->callstatus & CIST_LEQ) { /* "<=" using "<" instead? */ + lua_assert(op == OP_LE); + ci->callstatus ^= CIST_LEQ; /* clear mark */ + res = !res; /* negate result */ + } + lua_assert(GET_OPCODE(*ci->u.l.savedpc) == OP_JMP); + if (res != GETARG_A(inst)) /* condition failed? */ + ci->u.l.savedpc++; /* skip jump instruction */ + break; + } + case OP_CONCAT: { + StkId top = L->top - 1; /* top when 'luaT_trybinTM' was called */ + int b = GETARG_B(inst); /* first element to concatenate */ + int total = cast_int(top - 1 - (base + b)); /* yet to concatenate */ + setobj2s(L, top - 2, top); /* put TM result in proper position */ + if (total > 1) { /* are there elements to concat? */ + L->top = top - 1; /* top is one after last element (at top-2) */ + luaV_concat(L, total); /* concat them (may yield again) */ + } + /* move final result to final position */ + setobj2s(L, ci->u.l.base + GETARG_A(inst), L->top - 1); + L->top = ci->top; /* restore top */ + break; + } + case OP_TFORCALL: { + lua_assert(GET_OPCODE(*ci->u.l.savedpc) == OP_TFORLOOP); + L->top = ci->top; /* correct top */ + break; + } + case OP_CALL: { + if (GETARG_C(inst) - 1 >= 0) /* nresults >= 0? */ + L->top = ci->top; /* adjust results */ + break; + } + case OP_TAILCALL: case OP_SETTABUP: case OP_SETTABLE: + break; + default: lua_assert(0); + } +} + + + + +/* +** {================================================================== +** Function 'luaV_execute': main interpreter loop +** =================================================================== +*/ + + +/* +** some macros for common tasks in 'luaV_execute' +*/ + + +#define RA(i) (base+GETARG_A(i)) +#define RB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i)) +#define RC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgR, base+GETARG_C(i)) +#define RKB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \ + ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i)) +#define RKC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \ + ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i)) + + +/* execute a jump instruction */ +#define dojump(ci,i,e) \ + { int a = GETARG_A(i); \ + if (a != 0) luaF_close(L, ci->u.l.base + a - 1); \ + ci->u.l.savedpc += GETARG_sBx(i) + e; } + +/* for test instructions, execute the jump instruction that follows it */ +#define donextjump(ci) { i = *ci->u.l.savedpc; dojump(ci, i, 1); } + + +#define Protect(x) { {x;}; base = ci->u.l.base; } + +#define checkGC(L,c) \ + { luaC_condGC(L, L->top = (c), /* limit of live values */ \ + Protect(L->top = ci->top)); /* restore top */ \ + luai_threadyield(L); } + + +/* fetch an instruction and prepare its execution */ +#define vmfetch() { \ + i = *(ci->u.l.savedpc++); \ + if (L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) \ + Protect(luaG_traceexec(L)); \ + ra = RA(i); /* WARNING: any stack reallocation invalidates 'ra' */ \ + lua_assert(base == ci->u.l.base); \ + lua_assert(base <= L->top && L->top < L->stack + L->stacksize); \ +} + +#define vmdispatch(o) switch(o) +#define vmcase(l) case l: +#define vmbreak break + + +/* +** copy of 'luaV_gettable', but protecting the call to potential +** metamethod (which can reallocate the stack) +*/ +#define gettableProtected(L,t,k,v) { const TValue *slot; \ + if (luaV_fastget(L,t,k,slot,luaH_get)) { setobj2s(L, v, slot); } \ + else Protect(luaV_finishget(L,t,k,v,slot)); } + + +/* same for 'luaV_settable' */ +#define settableProtected(L,t,k,v) { const TValue *slot; \ + if (!luaV_fastset(L,t,k,slot,luaH_get,v)) \ + Protect(luaV_finishset(L,t,k,v,slot)); } + + + +void luaV_execute (lua_State *L) { + CallInfo *ci = L->ci; + LClosure *cl; + TValue *k; + StkId base; + ci->callstatus |= CIST_FRESH; /* fresh invocation of 'luaV_execute" */ + newframe: /* reentry point when frame changes (call/return) */ + lua_assert(ci == L->ci); + cl = clLvalue(ci->func); /* local reference to function's closure */ + k = cl->p->k; /* local reference to function's constant table */ + base = ci->u.l.base; /* local copy of function's base */ + /* main loop of interpreter */ + for (;;) { + Instruction i; + StkId ra; + vmfetch(); + vmdispatch (GET_OPCODE(i)) { + vmcase(OP_MOVE) { + setobjs2s(L, ra, RB(i)); + vmbreak; + } + vmcase(OP_LOADK) { + TValue *rb = k + GETARG_Bx(i); + setobj2s(L, ra, rb); + vmbreak; + } + vmcase(OP_LOADKX) { + TValue *rb; + lua_assert(GET_OPCODE(*ci->u.l.savedpc) == OP_EXTRAARG); + rb = k + GETARG_Ax(*ci->u.l.savedpc++); + setobj2s(L, ra, rb); + vmbreak; + } + vmcase(OP_LOADBOOL) { + setbvalue(ra, GETARG_B(i)); + if (GETARG_C(i)) ci->u.l.savedpc++; /* skip next instruction (if C) */ + vmbreak; + } + vmcase(OP_LOADNIL) { + int b = GETARG_B(i); + do { + setnilvalue(ra++); + } while (b--); + vmbreak; + } + vmcase(OP_GETUPVAL) { + int b = GETARG_B(i); + setobj2s(L, ra, cl->upvals[b]->v); + vmbreak; + } + vmcase(OP_GETTABUP) { + TValue *upval = cl->upvals[GETARG_B(i)]->v; + TValue *rc = RKC(i); + gettableProtected(L, upval, rc, ra); + vmbreak; + } + vmcase(OP_GETTABLE) { + StkId rb = RB(i); + TValue *rc = RKC(i); + gettableProtected(L, rb, rc, ra); + vmbreak; + } + vmcase(OP_SETTABUP) { + TValue *upval = cl->upvals[GETARG_A(i)]->v; + TValue *rb = RKB(i); + TValue *rc = RKC(i); + settableProtected(L, upval, rb, rc); + vmbreak; + } + vmcase(OP_SETUPVAL) { + UpVal *uv = cl->upvals[GETARG_B(i)]; + setobj(L, uv->v, ra); + luaC_upvalbarrier(L, uv); + vmbreak; + } + vmcase(OP_SETTABLE) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + settableProtected(L, ra, rb, rc); + vmbreak; + } + vmcase(OP_NEWTABLE) { + int b = GETARG_B(i); + int c = GETARG_C(i); + Table *t = luaH_new(L); + sethvalue(L, ra, t); + if (b != 0 || c != 0) + luaH_resize(L, t, luaO_fb2int(b), luaO_fb2int(c)); + checkGC(L, ra + 1); + vmbreak; + } + vmcase(OP_SELF) { + const TValue *aux; + StkId rb = RB(i); + TValue *rc = RKC(i); + TString *key = tsvalue(rc); /* key must be a string */ + setobjs2s(L, ra + 1, rb); + if (luaV_fastget(L, rb, key, aux, luaH_getstr)) { + setobj2s(L, ra, aux); + } + else Protect(luaV_finishget(L, rb, rc, ra, aux)); + vmbreak; + } + vmcase(OP_ADD) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (ttisinteger(rb) && ttisinteger(rc)) { + lua_Integer ib = ivalue(rb); lua_Integer ic = ivalue(rc); + setivalue(ra, intop(+, ib, ic)); + } + else if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + setfltvalue(ra, luai_numadd(L, nb, nc)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_ADD)); } + vmbreak; + } + vmcase(OP_SUB) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (ttisinteger(rb) && ttisinteger(rc)) { + lua_Integer ib = ivalue(rb); lua_Integer ic = ivalue(rc); + setivalue(ra, intop(-, ib, ic)); + } + else if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + setfltvalue(ra, luai_numsub(L, nb, nc)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_SUB)); } + vmbreak; + } + vmcase(OP_MUL) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (ttisinteger(rb) && ttisinteger(rc)) { + lua_Integer ib = ivalue(rb); lua_Integer ic = ivalue(rc); + setivalue(ra, intop(*, ib, ic)); + } + else if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + setfltvalue(ra, luai_nummul(L, nb, nc)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_MUL)); } + vmbreak; + } + vmcase(OP_DIV) { /* float division (always with floats) */ + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + setfltvalue(ra, luai_numdiv(L, nb, nc)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_DIV)); } + vmbreak; + } + vmcase(OP_BAND) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Integer ib; lua_Integer ic; + if (tointeger(rb, &ib) && tointeger(rc, &ic)) { + setivalue(ra, intop(&, ib, ic)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_BAND)); } + vmbreak; + } + vmcase(OP_BOR) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Integer ib; lua_Integer ic; + if (tointeger(rb, &ib) && tointeger(rc, &ic)) { + setivalue(ra, intop(|, ib, ic)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_BOR)); } + vmbreak; + } + vmcase(OP_BXOR) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Integer ib; lua_Integer ic; + if (tointeger(rb, &ib) && tointeger(rc, &ic)) { + setivalue(ra, intop(^, ib, ic)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_BXOR)); } + vmbreak; + } + vmcase(OP_SHL) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Integer ib; lua_Integer ic; + if (tointeger(rb, &ib) && tointeger(rc, &ic)) { + setivalue(ra, luaV_shiftl(ib, ic)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_SHL)); } + vmbreak; + } + vmcase(OP_SHR) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Integer ib; lua_Integer ic; + if (tointeger(rb, &ib) && tointeger(rc, &ic)) { + setivalue(ra, luaV_shiftl(ib, -ic)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_SHR)); } + vmbreak; + } + vmcase(OP_MOD) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (ttisinteger(rb) && ttisinteger(rc)) { + lua_Integer ib = ivalue(rb); lua_Integer ic = ivalue(rc); + setivalue(ra, luaV_mod(L, ib, ic)); + } + else if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + lua_Number m; + luai_nummod(L, nb, nc, m); + setfltvalue(ra, m); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_MOD)); } + vmbreak; + } + vmcase(OP_IDIV) { /* floor division */ + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (ttisinteger(rb) && ttisinteger(rc)) { + lua_Integer ib = ivalue(rb); lua_Integer ic = ivalue(rc); + setivalue(ra, luaV_div(L, ib, ic)); + } + else if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + setfltvalue(ra, luai_numidiv(L, nb, nc)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_IDIV)); } + vmbreak; + } + vmcase(OP_POW) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + lua_Number nb; lua_Number nc; + if (tonumber(rb, &nb) && tonumber(rc, &nc)) { + setfltvalue(ra, luai_numpow(L, nb, nc)); + } + else { Protect(luaT_trybinTM(L, rb, rc, ra, TM_POW)); } + vmbreak; + } + vmcase(OP_UNM) { + TValue *rb = RB(i); + lua_Number nb; + if (ttisinteger(rb)) { + lua_Integer ib = ivalue(rb); + setivalue(ra, intop(-, 0, ib)); + } + else if (tonumber(rb, &nb)) { + setfltvalue(ra, luai_numunm(L, nb)); + } + else { + Protect(luaT_trybinTM(L, rb, rb, ra, TM_UNM)); + } + vmbreak; + } + vmcase(OP_BNOT) { + TValue *rb = RB(i); + lua_Integer ib; + if (tointeger(rb, &ib)) { + setivalue(ra, intop(^, ~l_castS2U(0), ib)); + } + else { + Protect(luaT_trybinTM(L, rb, rb, ra, TM_BNOT)); + } + vmbreak; + } + vmcase(OP_NOT) { + TValue *rb = RB(i); + int res = l_isfalse(rb); /* next assignment may change this value */ + setbvalue(ra, res); + vmbreak; + } + vmcase(OP_LEN) { + Protect(luaV_objlen(L, ra, RB(i))); + vmbreak; + } + vmcase(OP_CONCAT) { + int b = GETARG_B(i); + int c = GETARG_C(i); + StkId rb; + L->top = base + c + 1; /* mark the end of concat operands */ + Protect(luaV_concat(L, c - b + 1)); + ra = RA(i); /* 'luaV_concat' may invoke TMs and move the stack */ + rb = base + b; + setobjs2s(L, ra, rb); + checkGC(L, (ra >= rb ? ra + 1 : rb)); + L->top = ci->top; /* restore top */ + vmbreak; + } + vmcase(OP_JMP) { + dojump(ci, i, 0); + vmbreak; + } + vmcase(OP_EQ) { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + Protect( + if (luaV_equalobj(L, rb, rc) != GETARG_A(i)) + ci->u.l.savedpc++; + else + donextjump(ci); + ) + vmbreak; + } + vmcase(OP_LT) { + Protect( + if (luaV_lessthan(L, RKB(i), RKC(i)) != GETARG_A(i)) + ci->u.l.savedpc++; + else + donextjump(ci); + ) + vmbreak; + } + vmcase(OP_LE) { + Protect( + if (luaV_lessequal(L, RKB(i), RKC(i)) != GETARG_A(i)) + ci->u.l.savedpc++; + else + donextjump(ci); + ) + vmbreak; + } + vmcase(OP_TEST) { + if (GETARG_C(i) ? l_isfalse(ra) : !l_isfalse(ra)) + ci->u.l.savedpc++; + else + donextjump(ci); + vmbreak; + } + vmcase(OP_TESTSET) { + TValue *rb = RB(i); + if (GETARG_C(i) ? l_isfalse(rb) : !l_isfalse(rb)) + ci->u.l.savedpc++; + else { + setobjs2s(L, ra, rb); + donextjump(ci); + } + vmbreak; + } + vmcase(OP_CALL) { + int b = GETARG_B(i); + int nresults = GETARG_C(i) - 1; + if (b != 0) L->top = ra+b; /* else previous instruction set top */ + if (luaD_precall(L, ra, nresults)) { /* C function? */ + if (nresults >= 0) + L->top = ci->top; /* adjust results */ + Protect((void)0); /* update 'base' */ + } + else { /* Lua function */ + ci = L->ci; + goto newframe; /* restart luaV_execute over new Lua function */ + } + vmbreak; + } + vmcase(OP_TAILCALL) { + int b = GETARG_B(i); + if (b != 0) L->top = ra+b; /* else previous instruction set top */ + lua_assert(GETARG_C(i) - 1 == LUA_MULTRET); + if (luaD_precall(L, ra, LUA_MULTRET)) { /* C function? */ + Protect((void)0); /* update 'base' */ + } + else { + /* tail call: put called frame (n) in place of caller one (o) */ + CallInfo *nci = L->ci; /* called frame */ + CallInfo *oci = nci->previous; /* caller frame */ + StkId nfunc = nci->func; /* called function */ + StkId ofunc = oci->func; /* caller function */ + /* last stack slot filled by 'precall' */ + StkId lim = nci->u.l.base + getproto(nfunc)->numparams; + int aux; + /* close all upvalues from previous call */ + if (cl->p->sizep > 0) luaF_close(L, oci->u.l.base); + /* move new frame into old one */ + for (aux = 0; nfunc + aux < lim; aux++) + setobjs2s(L, ofunc + aux, nfunc + aux); + oci->u.l.base = ofunc + (nci->u.l.base - nfunc); /* correct base */ + oci->top = L->top = ofunc + (L->top - nfunc); /* correct top */ + oci->u.l.savedpc = nci->u.l.savedpc; + oci->callstatus |= CIST_TAIL; /* function was tail called */ + ci = L->ci = oci; /* remove new frame */ + lua_assert(L->top == oci->u.l.base + getproto(ofunc)->maxstacksize); + goto newframe; /* restart luaV_execute over new Lua function */ + } + vmbreak; + } + vmcase(OP_RETURN) { + int b = GETARG_B(i); + if (cl->p->sizep > 0) luaF_close(L, base); + b = luaD_poscall(L, ci, ra, (b != 0 ? b - 1 : cast_int(L->top - ra))); + if (ci->callstatus & CIST_FRESH) /* local 'ci' still from callee */ + return; /* external invocation: return */ + else { /* invocation via reentry: continue execution */ + ci = L->ci; + if (b) L->top = ci->top; + lua_assert(isLua(ci)); + lua_assert(GET_OPCODE(*((ci)->u.l.savedpc - 1)) == OP_CALL); + goto newframe; /* restart luaV_execute over new Lua function */ + } + } + vmcase(OP_FORLOOP) { + if (ttisinteger(ra)) { /* integer loop? */ + lua_Integer step = ivalue(ra + 2); + lua_Integer idx = intop(+, ivalue(ra), step); /* increment index */ + lua_Integer limit = ivalue(ra + 1); + if ((0 < step) ? (idx <= limit) : (limit <= idx)) { + ci->u.l.savedpc += GETARG_sBx(i); /* jump back */ + chgivalue(ra, idx); /* update internal index... */ + setivalue(ra + 3, idx); /* ...and external index */ + } + } + else { /* floating loop */ + lua_Number step = fltvalue(ra + 2); + lua_Number idx = luai_numadd(L, fltvalue(ra), step); /* inc. index */ + lua_Number limit = fltvalue(ra + 1); + if (luai_numlt(0, step) ? luai_numle(idx, limit) + : luai_numle(limit, idx)) { + ci->u.l.savedpc += GETARG_sBx(i); /* jump back */ + chgfltvalue(ra, idx); /* update internal index... */ + setfltvalue(ra + 3, idx); /* ...and external index */ + } + } + vmbreak; + } + vmcase(OP_FORPREP) { + TValue *init = ra; + TValue *plimit = ra + 1; + TValue *pstep = ra + 2; + lua_Integer ilimit; + int stopnow; + if (ttisinteger(init) && ttisinteger(pstep) && + forlimit(plimit, &ilimit, ivalue(pstep), &stopnow)) { + /* all values are integer */ + lua_Integer initv = (stopnow ? 0 : ivalue(init)); + setivalue(plimit, ilimit); + setivalue(init, intop(-, initv, ivalue(pstep))); + } + else { /* try making all values floats */ + lua_Number ninit; lua_Number nlimit; lua_Number nstep; + if (!tonumber(plimit, &nlimit)) + luaG_runerror(L, "'for' limit must be a number"); + setfltvalue(plimit, nlimit); + if (!tonumber(pstep, &nstep)) + luaG_runerror(L, "'for' step must be a number"); + setfltvalue(pstep, nstep); + if (!tonumber(init, &ninit)) + luaG_runerror(L, "'for' initial value must be a number"); + setfltvalue(init, luai_numsub(L, ninit, nstep)); + } + ci->u.l.savedpc += GETARG_sBx(i); + vmbreak; + } + vmcase(OP_TFORCALL) { + StkId cb = ra + 3; /* call base */ + setobjs2s(L, cb+2, ra+2); + setobjs2s(L, cb+1, ra+1); + setobjs2s(L, cb, ra); + L->top = cb + 3; /* func. + 2 args (state and index) */ + Protect(luaD_call(L, cb, GETARG_C(i))); + L->top = ci->top; + i = *(ci->u.l.savedpc++); /* go to next instruction */ + ra = RA(i); + lua_assert(GET_OPCODE(i) == OP_TFORLOOP); + goto l_tforloop; + } + vmcase(OP_TFORLOOP) { + l_tforloop: + if (!ttisnil(ra + 1)) { /* continue loop? */ + setobjs2s(L, ra, ra + 1); /* save control variable */ + ci->u.l.savedpc += GETARG_sBx(i); /* jump back */ + } + vmbreak; + } + vmcase(OP_SETLIST) { + int n = GETARG_B(i); + int c = GETARG_C(i); + unsigned int last; + Table *h; + if (n == 0) n = cast_int(L->top - ra) - 1; + if (c == 0) { + lua_assert(GET_OPCODE(*ci->u.l.savedpc) == OP_EXTRAARG); + c = GETARG_Ax(*ci->u.l.savedpc++); + } + h = hvalue(ra); + last = ((c-1)*LFIELDS_PER_FLUSH) + n; + if (last > h->sizearray) /* needs more space? */ + luaH_resizearray(L, h, last); /* preallocate it at once */ + for (; n > 0; n--) { + TValue *val = ra+n; + luaH_setint(L, h, last--, val); + luaC_barrierback(L, h, val); + } + L->top = ci->top; /* correct top (in case of previous open call) */ + vmbreak; + } + vmcase(OP_CLOSURE) { + Proto *p = cl->p->p[GETARG_Bx(i)]; + LClosure *ncl = getcached(p, cl->upvals, base); /* cached closure */ + if (ncl == NULL) /* no match? */ + pushclosure(L, p, cl->upvals, base, ra); /* create a new one */ + else + setclLvalue(L, ra, ncl); /* push cashed closure */ + checkGC(L, ra + 1); + vmbreak; + } + vmcase(OP_VARARG) { + int b = GETARG_B(i) - 1; /* required results */ + int j; + int n = cast_int(base - ci->func) - cl->p->numparams - 1; + if (n < 0) /* less arguments than parameters? */ + n = 0; /* no vararg arguments */ + if (b < 0) { /* B == 0? */ + b = n; /* get all var. arguments */ + Protect(luaD_checkstack(L, n)); + ra = RA(i); /* previous call may change the stack */ + L->top = ra + n; + } + for (j = 0; j < b && j < n; j++) + setobjs2s(L, ra + j, base - n + j); + for (; j < b; j++) /* complete required results with nil */ + setnilvalue(ra + j); + vmbreak; + } + vmcase(OP_EXTRAARG) { + lua_assert(0); + vmbreak; + } + } + } +} + +/* }================================================================== */ + diff --git a/src/rcheevos/test/lua/src/lvm.h b/src/rcheevos/test/lua/src/lvm.h new file mode 100644 index 000000000..422f87194 --- /dev/null +++ b/src/rcheevos/test/lua/src/lvm.h @@ -0,0 +1,113 @@ +/* +** $Id: lvm.h,v 2.41 2016/12/22 13:08:50 roberto Exp $ +** Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#ifndef lvm_h +#define lvm_h + + +#include "ldo.h" +#include "lobject.h" +#include "ltm.h" + + +#if !defined(LUA_NOCVTN2S) +#define cvt2str(o) ttisnumber(o) +#else +#define cvt2str(o) 0 /* no conversion from numbers to strings */ +#endif + + +#if !defined(LUA_NOCVTS2N) +#define cvt2num(o) ttisstring(o) +#else +#define cvt2num(o) 0 /* no conversion from strings to numbers */ +#endif + + +/* +** You can define LUA_FLOORN2I if you want to convert floats to integers +** by flooring them (instead of raising an error if they are not +** integral values) +*/ +#if !defined(LUA_FLOORN2I) +#define LUA_FLOORN2I 0 +#endif + + +#define tonumber(o,n) \ + (ttisfloat(o) ? (*(n) = fltvalue(o), 1) : luaV_tonumber_(o,n)) + +#define tointeger(o,i) \ + (ttisinteger(o) ? (*(i) = ivalue(o), 1) : luaV_tointeger(o,i,LUA_FLOORN2I)) + +#define intop(op,v1,v2) l_castU2S(l_castS2U(v1) op l_castS2U(v2)) + +#define luaV_rawequalobj(t1,t2) luaV_equalobj(NULL,t1,t2) + + +/* +** fast track for 'gettable': if 't' is a table and 't[k]' is not nil, +** return 1 with 'slot' pointing to 't[k]' (final result). Otherwise, +** return 0 (meaning it will have to check metamethod) with 'slot' +** pointing to a nil 't[k]' (if 't' is a table) or NULL (otherwise). +** 'f' is the raw get function to use. +*/ +#define luaV_fastget(L,t,k,slot,f) \ + (!ttistable(t) \ + ? (slot = NULL, 0) /* not a table; 'slot' is NULL and result is 0 */ \ + : (slot = f(hvalue(t), k), /* else, do raw access */ \ + !ttisnil(slot))) /* result not nil? */ + +/* +** standard implementation for 'gettable' +*/ +#define luaV_gettable(L,t,k,v) { const TValue *slot; \ + if (luaV_fastget(L,t,k,slot,luaH_get)) { setobj2s(L, v, slot); } \ + else luaV_finishget(L,t,k,v,slot); } + + +/* +** Fast track for set table. If 't' is a table and 't[k]' is not nil, +** call GC barrier, do a raw 't[k]=v', and return true; otherwise, +** return false with 'slot' equal to NULL (if 't' is not a table) or +** 'nil'. (This is needed by 'luaV_finishget'.) Note that, if the macro +** returns true, there is no need to 'invalidateTMcache', because the +** call is not creating a new entry. +*/ +#define luaV_fastset(L,t,k,slot,f,v) \ + (!ttistable(t) \ + ? (slot = NULL, 0) \ + : (slot = f(hvalue(t), k), \ + ttisnil(slot) ? 0 \ + : (luaC_barrierback(L, hvalue(t), v), \ + setobj2t(L, cast(TValue *,slot), v), \ + 1))) + + +#define luaV_settable(L,t,k,v) { const TValue *slot; \ + if (!luaV_fastset(L,t,k,slot,luaH_get,v)) \ + luaV_finishset(L,t,k,v,slot); } + + + +LUAI_FUNC int luaV_equalobj (lua_State *L, const TValue *t1, const TValue *t2); +LUAI_FUNC int luaV_lessthan (lua_State *L, const TValue *l, const TValue *r); +LUAI_FUNC int luaV_lessequal (lua_State *L, const TValue *l, const TValue *r); +LUAI_FUNC int luaV_tonumber_ (const TValue *obj, lua_Number *n); +LUAI_FUNC int luaV_tointeger (const TValue *obj, lua_Integer *p, int mode); +LUAI_FUNC void luaV_finishget (lua_State *L, const TValue *t, TValue *key, + StkId val, const TValue *slot); +LUAI_FUNC void luaV_finishset (lua_State *L, const TValue *t, TValue *key, + StkId val, const TValue *slot); +LUAI_FUNC void luaV_finishOp (lua_State *L); +LUAI_FUNC void luaV_execute (lua_State *L); +LUAI_FUNC void luaV_concat (lua_State *L, int total); +LUAI_FUNC lua_Integer luaV_div (lua_State *L, lua_Integer x, lua_Integer y); +LUAI_FUNC lua_Integer luaV_mod (lua_State *L, lua_Integer x, lua_Integer y); +LUAI_FUNC lua_Integer luaV_shiftl (lua_Integer x, lua_Integer y); +LUAI_FUNC void luaV_objlen (lua_State *L, StkId ra, const TValue *rb); + +#endif diff --git a/src/rcheevos/test/lua/src/lzio.c b/src/rcheevos/test/lua/src/lzio.c new file mode 100644 index 000000000..c9e1f491f --- /dev/null +++ b/src/rcheevos/test/lua/src/lzio.c @@ -0,0 +1,68 @@ +/* +** $Id: lzio.c,v 1.37 2015/09/08 15:41:05 roberto Exp $ +** Buffered streams +** See Copyright Notice in lua.h +*/ + +#define lzio_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "llimits.h" +#include "lmem.h" +#include "lstate.h" +#include "lzio.h" + + +int luaZ_fill (ZIO *z) { + size_t size; + lua_State *L = z->L; + const char *buff; + lua_unlock(L); + buff = z->reader(L, z->data, &size); + lua_lock(L); + if (buff == NULL || size == 0) + return EOZ; + z->n = size - 1; /* discount char being returned */ + z->p = buff; + return cast_uchar(*(z->p++)); +} + + +void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, void *data) { + z->L = L; + z->reader = reader; + z->data = data; + z->n = 0; + z->p = NULL; +} + + +/* --------------------------------------------------------------- read --- */ +size_t luaZ_read (ZIO *z, void *b, size_t n) { + while (n) { + size_t m; + if (z->n == 0) { /* no bytes in buffer? */ + if (luaZ_fill(z) == EOZ) /* try to read more */ + return n; /* no more input; return number of missing bytes */ + else { + z->n++; /* luaZ_fill consumed first byte; put it back */ + z->p--; + } + } + m = (n <= z->n) ? n : z->n; /* min. between n and z->n */ + memcpy(b, z->p, m); + z->n -= m; + z->p += m; + b = (char *)b + m; + n -= m; + } + return 0; +} + diff --git a/src/rcheevos/test/lua/src/lzio.h b/src/rcheevos/test/lua/src/lzio.h new file mode 100644 index 000000000..e7b6f34b1 --- /dev/null +++ b/src/rcheevos/test/lua/src/lzio.h @@ -0,0 +1,66 @@ +/* +** $Id: lzio.h,v 1.31 2015/09/08 15:41:05 roberto Exp $ +** Buffered streams +** See Copyright Notice in lua.h +*/ + + +#ifndef lzio_h +#define lzio_h + +#include "lua.h" + +#include "lmem.h" + + +#define EOZ (-1) /* end of stream */ + +typedef struct Zio ZIO; + +#define zgetc(z) (((z)->n--)>0 ? cast_uchar(*(z)->p++) : luaZ_fill(z)) + + +typedef struct Mbuffer { + char *buffer; + size_t n; + size_t buffsize; +} Mbuffer; + +#define luaZ_initbuffer(L, buff) ((buff)->buffer = NULL, (buff)->buffsize = 0) + +#define luaZ_buffer(buff) ((buff)->buffer) +#define luaZ_sizebuffer(buff) ((buff)->buffsize) +#define luaZ_bufflen(buff) ((buff)->n) + +#define luaZ_buffremove(buff,i) ((buff)->n -= (i)) +#define luaZ_resetbuffer(buff) ((buff)->n = 0) + + +#define luaZ_resizebuffer(L, buff, size) \ + ((buff)->buffer = luaM_reallocvchar(L, (buff)->buffer, \ + (buff)->buffsize, size), \ + (buff)->buffsize = size) + +#define luaZ_freebuffer(L, buff) luaZ_resizebuffer(L, buff, 0) + + +LUAI_FUNC void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, + void *data); +LUAI_FUNC size_t luaZ_read (ZIO* z, void *b, size_t n); /* read next n bytes */ + + + +/* --------- Private Part ------------------ */ + +struct Zio { + size_t n; /* bytes still unread */ + const char *p; /* current position in buffer */ + lua_Reader reader; /* reader function */ + void *data; /* additional data */ + lua_State *L; /* Lua state (for reader) */ +}; + + +LUAI_FUNC int luaZ_fill (ZIO *z); + +#endif diff --git a/src/rcheevos/test/rapi/test_rc_api_common.c b/src/rcheevos/test/rapi/test_rc_api_common.c new file mode 100644 index 000000000..1a02d2da3 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_common.c @@ -0,0 +1,744 @@ +#include "../rapi/rc_api_common.h" + +#include "rc_api_runtime.h" /* for rc_fetch_image */ + +#include "rc_compat.h" + +#include "../test_framework.h" + +#define IMAGEREQUEST_URL "https://media.retroachievements.org" + +static void _assert_json_parse_response(rc_api_response_t* response, rc_json_field_t* field, const char* json, int expected_result) { + int result; + rc_api_server_response_t server_response; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buf_init(&response->buffer); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = json; + server_response.body_length = strlen(json); + + result = rc_json_parse_server_response(response, &server_response, fields, sizeof(fields)/sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, expected_result); + + ASSERT_NUM_EQUALS(response->succeeded, 1); + ASSERT_PTR_NULL(response->error_message); + + memcpy(field, &fields[2], sizeof(fields[2])); +} +#define assert_json_parse_response(response, field, json, expected_result) ASSERT_HELPER(_assert_json_parse_response(response, field, json, expected_result), "assert_json_parse_operand") + +static void _assert_field_value(rc_json_field_t* field, const char* expected_value) { + char buffer[256]; + + ASSERT_PTR_NOT_NULL(field->value_start); + ASSERT_PTR_NOT_NULL(field->value_end); + ASSERT_NUM_LESS(field->value_end - field->value_start, sizeof(buffer)); + + memcpy(buffer, field->value_start, field->value_end - field->value_start); + buffer[field->value_end - field->value_start] = '\0'; + ASSERT_STR_EQUALS(buffer, expected_value); +} +#define assert_field_value(field, expected_value) ASSERT_HELPER(_assert_field_value(field, expected_value), "assert_field_value") + +static void test_json_parse_response_empty() { + rc_api_response_t response; + rc_json_field_t field; + + assert_json_parse_response(&response, &field, "{}", RC_OK); + + ASSERT_STR_EQUALS(field.name, "Test"); + ASSERT_PTR_NULL(field.value_start); + ASSERT_PTR_NULL(field.value_end); +} + +static void test_json_parse_response_field(const char* json, const char* value) { + rc_api_response_t response; + rc_json_field_t field; + + assert_json_parse_response(&response, &field, json, RC_OK); + + ASSERT_STR_EQUALS(field.name, "Test"); + assert_field_value(&field, value); +} + +static void test_json_parse_response_non_json() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* error_message = "This is an error."; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buf_init(&response.buffer); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = error_message; + server_response.body_length = strlen(error_message); + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "This is an error."); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_parse_response_error_from_server() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* json; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buf_init(&response.buffer); + + json = "{\"Success\":false,\"Error\":\"Oops\"}"; + memset(&server_response, 0, sizeof(server_response)); + server_response.body = json; + server_response.body_length = strlen(json); + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields)/sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_OK); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Oops"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_parse_response_incorrect_size() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* json; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buf_init(&response.buffer); + + json = "{\"Success\":false,\"Error\":\"Oops\"}"; + memset(&server_response, 0, sizeof(server_response)); + server_response.body = json; + server_response.body_length = strlen(json) - 1; + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + + /* the error message was found before the parser failed */ + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Oops"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_string(const char* escaped, const char* expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[256]; + const char *value = NULL; + snprintf(buffer, sizeof(buffer), "{\"Test\":\"%s\"}", escaped); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + ASSERT_TRUE(rc_json_get_string(&value, &response.buffer, &field, "Test")); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, expected); +} + +static void test_json_get_optional_string() { + rc_api_response_t response; + rc_json_field_t field; + const char *value = NULL; + + assert_json_parse_response(&response, &field, "{\"Test\":\"Value\"}", RC_OK); + + rc_json_get_optional_string(&value, &response, &field, "Test", "Default"); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, "Value"); + + assert_json_parse_response(&response, &field, "{\"Test2\":\"Value\"}", RC_OK); + + rc_json_get_optional_string(&value, &response, &field, "Test", "Default"); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, "Default"); +} + +static void test_json_get_required_string() { + rc_api_response_t response; + rc_json_field_t field; + const char *value = NULL; + + assert_json_parse_response(&response, &field, "{\"Test\":\"Value\"}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_string(&value, &response, &field, "Test")); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, "Value"); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":\"Value\"}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_string(&value, &response, &field, "Test")); + ASSERT_PTR_NULL(value); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_num(const char* input, int expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + int value = 0; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected) { + ASSERT_TRUE(rc_json_get_num(&value, &field, "Test")); + } + else { + ASSERT_FALSE(rc_json_get_num(&value, &field, "Test")); + } + + ASSERT_NUM_EQUALS(value, expected); +} + +static void test_json_get_optional_num() { + rc_api_response_t response; + rc_json_field_t field; + int value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + rc_json_get_optional_num(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 12345678); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + rc_json_get_optional_num(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 9999); +} + +static void test_json_get_required_num() { + rc_api_response_t response; + rc_json_field_t field; + int value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_num(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 12345678); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_num(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_unum(const char* input, unsigned expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + unsigned value = 0; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected) { + ASSERT_TRUE(rc_json_get_unum(&value, &field, "Test")); + } + else { + ASSERT_FALSE(rc_json_get_unum(&value, &field, "Test")); + } + + ASSERT_NUM_EQUALS(value, expected); +} + +static void test_json_get_optional_unum() { + rc_api_response_t response; + rc_json_field_t field; + unsigned value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + rc_json_get_optional_unum(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 12345678); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + rc_json_get_optional_unum(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 9999); +} + +static void test_json_get_required_unum() { + rc_api_response_t response; + rc_json_field_t field; + unsigned value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_unum(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 12345678); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_unum(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_bool(const char* input, int expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + int value = 2; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected != -1) { + ASSERT_TRUE(rc_json_get_bool(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, expected); + } + else { + ASSERT_FALSE(rc_json_get_bool(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + } +} + +static void test_json_get_optional_bool() { + rc_api_response_t response; + rc_json_field_t field; + int value = 3; + + assert_json_parse_response(&response, &field, "{\"Test\":true}", RC_OK); + + rc_json_get_optional_bool(&value, &field, "Test", 2); + ASSERT_NUM_EQUALS(value, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":true}", RC_OK); + + rc_json_get_optional_bool(&value, &field, "Test", 2); + ASSERT_NUM_EQUALS(value, 2); +} + +static void test_json_get_required_bool() { + rc_api_response_t response; + rc_json_field_t field; + int value = 3; + + assert_json_parse_response(&response, &field, "{\"Test\":true}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_bool(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 1); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":True}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_bool(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_datetime(const char* input, int expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + time_t value = 2; + snprintf(buffer, sizeof(buffer), "{\"Test\":\"%s\"}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected != -1) { + ASSERT_TRUE(rc_json_get_datetime(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, (time_t)expected); + } + else { + ASSERT_FALSE(rc_json_get_datetime(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + } +} + +static void test_json_get_unum_array(const char* input, unsigned expected_count, int expected_result) { + rc_api_response_t response; + rc_json_field_t field; + int result; + unsigned count = 0xFFFFFFFF; + unsigned *values; + char buffer[128]; + + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + assert_json_parse_response(&response, &field, buffer, RC_OK); + + result = rc_json_get_required_unum_array(&values, &count, &response, &field, "Test"); + ASSERT_NUM_EQUALS(result, expected_result); + ASSERT_NUM_EQUALS(count, expected_count); + + rc_buf_destroy(&response.buffer); +} + +static void test_json_get_unum_array_trailing_comma() { + rc_api_response_t response; + rc_json_field_t field; + + assert_json_parse_response(&response, &field, "{\"Test\":[1,2,3,]}", RC_INVALID_JSON); +} + +static void test_url_build_dorequest_url_default_host() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + + rc_api_url_build_dorequest_url(&request); + ASSERT_STR_EQUALS(request.url, "https://retroachievements.org/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request(&request, &api_params); + ASSERT_STR_EQUALS(request.url, "https://media.retroachievements.org/Badge/12345.png"); + rc_api_destroy_request(&request); +} + +static void test_url_build_dorequest_url_default_host_nonssl() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + + rc_api_set_host("http://retroachievements.org"); + + rc_api_url_build_dorequest_url(&request); + ASSERT_STR_EQUALS(request.url, "http://retroachievements.org/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request(&request, &api_params); + ASSERT_STR_EQUALS(request.url, "http://media.retroachievements.org/Badge/12345.png"); + rc_api_destroy_request(&request); + + rc_api_set_host(NULL); +} + +static void test_url_build_dorequest_url_custom_host() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + + rc_api_set_host("http://localhost"); + + rc_api_url_build_dorequest_url(&request); + ASSERT_STR_EQUALS(request.url, "http://localhost/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request(&request, &api_params); + ASSERT_STR_EQUALS(request.url, "http://localhost/Badge/12345.png"); + rc_api_destroy_request(&request); + + rc_api_set_host(NULL); +} + +static void test_url_build_dorequest_url_custom_host_no_protocol() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + + rc_api_set_host("my.host"); + + rc_api_url_build_dorequest_url(&request); + ASSERT_STR_EQUALS(request.url, "http://my.host/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request(&request, &api_params); + ASSERT_STR_EQUALS(request.url, "http://my.host/Badge/12345.png"); + rc_api_destroy_request(&request); + + rc_api_set_host(NULL); +} + +static void test_url_builder_append_encoded_str(const char* input, const char* expected) { + rc_api_url_builder_t builder; + rc_api_buffer_t buffer; + const char* output; + + rc_buf_init(&buffer); + rc_url_builder_init(&builder, &buffer, 128); + rc_url_builder_append_encoded_str(&builder, input); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, expected); + + rc_buf_destroy(&buffer); +} + +static void test_url_builder_append_str_param() { + rc_api_url_builder_t builder; + rc_api_buffer_t buffer; + const char* output; + + rc_buf_init(&buffer); + rc_url_builder_init(&builder, &buffer, 64); + rc_url_builder_append_str_param(&builder, "a", "Apple"); + rc_url_builder_append_str_param(&builder, "b", "Banana"); + rc_url_builder_append_str_param(&builder, "t", "Test 1"); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, "a=Apple&b=Banana&t=Test+1"); + + rc_buf_destroy(&buffer); +} + +static void test_url_builder_append_unum_param() { + rc_api_url_builder_t builder; + rc_api_buffer_t buffer; + const char* output; + + rc_buf_init(&buffer); + rc_url_builder_init(&builder, &buffer, 32); + rc_url_builder_append_unum_param(&builder, "a", 0); + rc_url_builder_append_unum_param(&builder, "b", 123456); + rc_url_builder_append_unum_param(&builder, "t", (unsigned)-1); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, "a=0&b=123456&t=4294967295"); + + rc_buf_destroy(&buffer); +} + +static void test_url_builder_append_num_param() { + rc_api_url_builder_t builder; + rc_api_buffer_t buffer; + const char* output; + + rc_buf_init(&buffer); + rc_url_builder_init(&builder, &buffer, 32); + rc_url_builder_append_num_param(&builder, "a", 0); + rc_url_builder_append_num_param(&builder, "b", 123456); + rc_url_builder_append_num_param(&builder, "t", -1); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, "a=0&b=123456&t=-1"); + + rc_buf_destroy(&buffer); +} + +static void test_init_fetch_image_request_game() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "0123324"; + fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Images/0123324.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_achievement() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "135764"; + fetch_image_request.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Badge/135764.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_achievement_locked() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "135764"; + fetch_image_request.image_type = RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Badge/135764_lock.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_user() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "Username"; + fetch_image_request.image_type = RC_IMAGE_TYPE_USER; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/UserPic/Username.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_unknown() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "12345"; + fetch_image_request.image_type = -1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +void test_rapi_common(void) { + TEST_SUITE_BEGIN(); + + /* rc_json_parse_response */ + TEST(test_json_parse_response_empty); + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":\"Test\"}", "\"Test\""); /* string */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":\"Te\\\"st\"}", "\"Te\\\"st\""); /* escaped string */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":12345678}", "12345678"); /* integer */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":+12345678}", "+12345678"); /* positive integer */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":-12345678}", "-12345678"); /* negatvie integer */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":1234.5678}", "1234.5678"); /* decimal */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":+1234.5678}", "+1234.5678"); /* positive decimal */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":-1234.5678}", "-1234.5678"); /* negatvie decimal */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":[1,2,3]}", "[1,2,3]"); /* array */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":{\"Foo\":1}}", "{\"Foo\":1}"); /* object */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":null}", "null"); /* null */ + TEST_PARAMS2(test_json_parse_response_field, "{ \"Test\" : 0 }", "0"); /* ignore whitespace */ + TEST_PARAMS2(test_json_parse_response_field, "{ \"Other\" : 1, \"Test\" : 2 }", "2"); /* preceding field */ + TEST_PARAMS2(test_json_parse_response_field, "{ \"Test\" : 1, \"Other\" : 2 }", "1"); /* trailing field */ + TEST(test_json_parse_response_non_json); + TEST(test_json_parse_response_error_from_server); + TEST(test_json_parse_response_incorrect_size); + + /* rc_json_get_string */ + TEST_PARAMS2(test_json_get_string, "", ""); + TEST_PARAMS2(test_json_get_string, "Banana", "Banana"); + TEST_PARAMS2(test_json_get_string, "A \\\"Quoted\\\" String", "A \"Quoted\" String"); + TEST_PARAMS2(test_json_get_string, "This\\r\\nThat", "This\r\nThat"); + TEST_PARAMS2(test_json_get_string, "This\\/That", "This/That"); + TEST_PARAMS2(test_json_get_string, "\\u0065", "e"); + TEST_PARAMS2(test_json_get_string, "\\u00a9", "\xc2\xa9"); + TEST_PARAMS2(test_json_get_string, "\\u2260", "\xe2\x89\xa0"); + TEST_PARAMS2(test_json_get_string, "\\ud83d\\udeb6", "\xf0\x9f\x9a\xb6"); /* surrogate pair */ + TEST_PARAMS2(test_json_get_string, "\\ud83d", "\xef\xbf\xbd"); /* surrogate lead with no tail */ + TEST_PARAMS2(test_json_get_string, "\\udeb6", "\xef\xbf\xbd"); /* surrogate tail with no lead */ + TEST(test_json_get_optional_string); + TEST(test_json_get_required_string); + + /* rc_json_get_num */ + TEST_PARAMS2(test_json_get_num, "Banana", 0); + TEST_PARAMS2(test_json_get_num, "True", 0); + TEST_PARAMS2(test_json_get_num, "2468", 2468); + TEST_PARAMS2(test_json_get_num, "+55", 55); + TEST_PARAMS2(test_json_get_num, "-16", -16); + TEST_PARAMS2(test_json_get_num, "3.14159", 3); + TEST(test_json_get_optional_num); + TEST(test_json_get_required_num); + + /* rc_json_get_unum */ + TEST_PARAMS2(test_json_get_unum, "Banana", 0); + TEST_PARAMS2(test_json_get_unum, "True", 0); + TEST_PARAMS2(test_json_get_unum, "2468", 2468); + TEST_PARAMS2(test_json_get_unum, "+55", 0); + TEST_PARAMS2(test_json_get_unum, "-16", 0); + TEST_PARAMS2(test_json_get_unum, "3.14159", 3); + TEST(test_json_get_optional_unum); + TEST(test_json_get_required_unum); + + /* rc_json_get_bool */ + TEST_PARAMS2(test_json_get_bool, "true", 1); + TEST_PARAMS2(test_json_get_bool, "false", 0); + TEST_PARAMS2(test_json_get_bool, "TRUE", 1); + TEST_PARAMS2(test_json_get_bool, "True", 1); + TEST_PARAMS2(test_json_get_bool, "Banana", -1); + TEST_PARAMS2(test_json_get_bool, "1", 1); + TEST_PARAMS2(test_json_get_bool, "0", 0); + TEST(test_json_get_optional_bool); + TEST(test_json_get_required_bool); + + /* rc_json_get_datetime */ + TEST_PARAMS2(test_json_get_datetime, "", -1); + TEST_PARAMS2(test_json_get_datetime, "2015-01-01 08:15:00", 1420100100); + TEST_PARAMS2(test_json_get_datetime, "2016-02-29 20:01:47", 1456776107); + + /* rc_json_get_unum_array */ + TEST_PARAMS3(test_json_get_unum_array, "[]", 0, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "1", 0, RC_MISSING_VALUE); + TEST_PARAMS3(test_json_get_unum_array, "[1]", 1, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[ 1 ]", 1, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[1,2,3,4]", 4, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[ 1 , 2 ]", 2, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[1,1,1]", 3, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[A,B,C]", 3, RC_MISSING_VALUE); + TEST(test_json_get_unum_array_trailing_comma); + + /* rc_api_url_build_dorequest_url / rc_api_set_host */ + TEST(test_url_build_dorequest_url_default_host); + TEST(test_url_build_dorequest_url_default_host_nonssl); + TEST(test_url_build_dorequest_url_custom_host); + TEST(test_url_build_dorequest_url_custom_host_no_protocol); + + /* rc_api_url_builder_append_encoded_str */ + TEST_PARAMS2(test_url_builder_append_encoded_str, "", ""); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Apple", "Apple"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Test 1", "Test+1"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Test+1", "Test%2b1"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Test%1", "Test%251"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "%Test%", "%25Test%25"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "%%", "%25%25"); + + /* rc_api_url_builder_append_param */ + TEST(test_url_builder_append_str_param); + TEST(test_url_builder_append_num_param); + TEST(test_url_builder_append_unum_param); + + /* fetch_image */ + TEST(test_init_fetch_image_request_game); + TEST(test_init_fetch_image_request_achievement); + TEST(test_init_fetch_image_request_achievement_locked); + TEST(test_init_fetch_image_request_user); + TEST(test_init_fetch_image_request_unknown); + + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_editor.c b/src/rcheevos/test/rapi/test_rc_api_editor.c new file mode 100644 index 000000000..5a5980090 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_editor.c @@ -0,0 +1,722 @@ +#include "rc_api_editor.h" +#include "rc_api_runtime.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" +#include "rc_compat.h" +#include "rc_consoles.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_fetch_code_notes_request() +{ + rc_api_fetch_code_notes_request_t fetch_code_notes_request; + rc_api_request_t request; + + memset(&fetch_code_notes_request, 0, sizeof(fetch_code_notes_request)); + fetch_code_notes_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_code_notes_request(&request, &fetch_code_notes_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=codenotes2&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_code_notes_request_no_game_id() +{ + rc_api_fetch_code_notes_request_t fetch_code_notes_request; + rc_api_request_t request; + + memset(&fetch_code_notes_request, 0, sizeof(fetch_code_notes_request)); + + ASSERT_NUM_EQUALS(rc_api_init_fetch_code_notes_request(&request, &fetch_code_notes_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_code_notes_response_empty_array() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_PTR_NULL(fetch_code_notes_response.notes); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 0); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_one_item() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_several_items() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}," + "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," + "{\"User\":\"User2\",\"Address\":\"0x002002\",\"Note\":\"Sad\"}," + "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"Banana\\n0=a\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 4); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[1].address, 0x2000); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].note, "Happy"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[2].address, 0x2002); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].author, "User2"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].note, "Sad"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[3].address, 0x2ABC); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].note, "Banana\n0=a"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_deleted_items() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"\"}," + "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," + "{\"User\":\"User2\",\"Address\":\"0x002002\",\"Note\":\"''\"}," + "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x2000); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "Happy"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_update_code_note_request() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.game_id = 1234; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = "flags\n1=first\n2=second"; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168&n=flags%0a1%3dfirst%0a2%3dsecond"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_request_no_game_id() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = "flags\n1=first\n2=second"; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_request_no_note() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.game_id = 1234; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = NULL; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_request_empty_note() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.game_id = 1234; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = ""; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_response() +{ + rc_api_update_code_note_response_t update_code_note_response; + const char* server_response = "{\"Success\":true,\"GameID\":1234,\"Address\":7168,\"Note\":\"test\"}"; + memset(&update_code_note_response, 0, sizeof(update_code_note_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_code_note_response(&update_code_note_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_code_note_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_code_note_response.response.error_message); + + rc_api_destroy_update_code_note_response(&update_code_note_response); +} + +static void test_init_update_code_note_response_invalid_credentials() +{ + rc_api_update_code_note_response_t update_code_note_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&update_code_note_response, 0, sizeof(update_code_note_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_code_note_response(&update_code_note_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_code_note_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_code_note_response.response.error_message, "Credentials invalid (0)"); + + rc_api_destroy_update_code_note_response(&update_code_note_response); +} + +static void test_init_update_achievement_request() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.game_id = 1234; + update_achievement_request.achievement_id = 5555; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&h=7cd9d3f0bfdf84734968353b5a430cfd"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_request_new() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.game_id = 1234; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=5&b=123456&h=10dd1fd6e0201f634b1b7536d4860ccb"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_request_no_game_id() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.achievement_id = 5555; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_response() +{ + rc_api_update_achievement_response_t update_achievement_response; + const char* server_response = "{\"Success\":true,\"AchievementID\":1234}"; + memset(&update_achievement_response, 0, sizeof(update_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 1234); + + rc_api_destroy_update_achievement_response(&update_achievement_response); +} + +static void test_init_update_achievement_response_invalid_credentials() +{ + rc_api_update_achievement_response_t update_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&update_achievement_response, 0, sizeof(update_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); + + rc_api_destroy_update_achievement_response(&update_achievement_response); +} + +static void test_init_update_achievement_response_invalid_perms() +{ + rc_api_update_achievement_response_t update_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; + memset(&update_achievement_response, 0, sizeof(update_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); + ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); + + rc_api_destroy_update_achievement_response(&update_achievement_response); +} + +static void test_init_update_leaderboard_request() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.leaderboard_id = 5555; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_new() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=739e28608a9e93d7351103d2f43fc6dc"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_no_game_id() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_no_description() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.leaderboard_id = 5555; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = ""; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_response() +{ + rc_api_update_leaderboard_response_t update_leaderboard_response; + const char* server_response = "{\"Success\":true,\"LeaderboardID\":1234}"; + memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_leaderboard_response.response.error_message); + ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 1234); + + rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); +} + +static void test_init_update_leaderboard_response_invalid_credentials() +{ + rc_api_update_leaderboard_response_t update_leaderboard_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); + + rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); +} + +static void test_init_update_leaderboard_response_invalid_perms() +{ + rc_api_update_leaderboard_response_t update_leaderboard_response; + const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; + memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); + ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); + + rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); +} + +static void test_init_fetch_badge_range_request() +{ + rc_api_fetch_badge_range_request_t fetch_badge_range_request; + rc_api_request_t request; + + memset(&fetch_badge_range_request, 0, sizeof(fetch_badge_range_request)); + + ASSERT_NUM_EQUALS(rc_api_init_fetch_badge_range_request(&request, &fetch_badge_range_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=badgeiter"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_badge_range_response() +{ + rc_api_fetch_badge_range_response_t fetch_badge_range_response; + const char* server_response = "{\"Success\":true,\"FirstBadge\":12,\"NextBadge\":123456}"; + memset(&fetch_badge_range_response, 0, sizeof(fetch_badge_range_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_badge_range_response(&fetch_badge_range_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_badge_range_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_badge_range_response.response.error_message); + ASSERT_UNUM_EQUALS(fetch_badge_range_response.first_badge_id, 12); + ASSERT_UNUM_EQUALS(fetch_badge_range_response.next_badge_id, 123456); + + rc_api_destroy_fetch_badge_range_response(&fetch_badge_range_response); +} + +static void test_init_add_game_hash_request() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.game_id = 1234; + add_game_hash_request.title = "Game Name"; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&i=Game+Name&g=1234&d=Game+Name+%5bNo+Intro%5d.nes"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_game_id() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.title = "Game Name"; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&i=Game+Name&d=Game+Name+%5bNo+Intro%5d.nes"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_console_id() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.game_id = 1234; + add_game_hash_request.title = "Game Name"; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_title() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.game_id = 1234; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + /* title is not required when a game id is provided (at least at the client + * level - the server will generate an error, but that could change) */ + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&g=1234&d=Game+Name+%5bNo+Intro%5d.nes"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_title_or_game_id() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_response() +{ + rc_api_add_game_hash_response_t add_game_hash_response; + const char* server_response = "{\"Success\":true,\"Response\":{\"GameID\":1234}}"; + memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 1); + ASSERT_PTR_NULL(add_game_hash_response.response.error_message); + ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 1234); + + rc_api_destroy_add_game_hash_response(&add_game_hash_response); +} + +static void test_init_add_game_hash_response_error() +{ + rc_api_add_game_hash_response_t add_game_hash_response; + const char* server_response = "{\"Success\":false,\"Error\":\"The ROM you are trying to load is not in the database.\"}"; + memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 0); + ASSERT_STR_EQUALS(add_game_hash_response.response.error_message, "The ROM you are trying to load is not in the database."); + ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 0); + + rc_api_destroy_add_game_hash_response(&add_game_hash_response); +} + +static void test_init_add_game_hash_response_invalid_credentials() +{ + rc_api_add_game_hash_response_t add_game_hash_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 0); + ASSERT_STR_EQUALS(add_game_hash_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 0); + + rc_api_destroy_add_game_hash_response(&add_game_hash_response); +} + +void test_rapi_editor(void) { + TEST_SUITE_BEGIN(); + + /* fetch code notes */ + TEST(test_init_fetch_code_notes_request); + TEST(test_init_fetch_code_notes_request_no_game_id); + + TEST(test_init_fetch_code_notes_response_empty_array); + TEST(test_init_fetch_code_notes_response_one_item); + TEST(test_init_fetch_code_notes_response_several_items); + TEST(test_init_fetch_code_notes_response_deleted_items); + + /* update code note */ + TEST(test_init_update_code_note_request); + TEST(test_init_update_code_note_request_no_game_id); + TEST(test_init_update_code_note_request_no_note); + TEST(test_init_update_code_note_request_empty_note); + + TEST(test_init_update_code_note_response); + TEST(test_init_update_code_note_response_invalid_credentials); + + /* update achievement */ + TEST(test_init_update_achievement_request); + TEST(test_init_update_achievement_request_new); + TEST(test_init_update_achievement_request_no_game_id); + + TEST(test_init_update_achievement_response); + TEST(test_init_update_achievement_response_invalid_credentials); + TEST(test_init_update_achievement_response_invalid_perms); + + /* update leaderboard */ + TEST(test_init_update_leaderboard_request); + TEST(test_init_update_leaderboard_request_new); + TEST(test_init_update_leaderboard_request_no_game_id); + TEST(test_init_update_leaderboard_request_no_description); + + TEST(test_init_update_leaderboard_response); + TEST(test_init_update_leaderboard_response_invalid_credentials); + TEST(test_init_update_leaderboard_response_invalid_perms); + + /* fetch badge range */ + TEST(test_init_fetch_badge_range_request); + + TEST(test_init_fetch_badge_range_response); + + /* add game hash */ + TEST(test_init_add_game_hash_request); + TEST(test_init_add_game_hash_request_no_game_id); + TEST(test_init_add_game_hash_request_no_console_id); + TEST(test_init_add_game_hash_request_no_title); + TEST(test_init_add_game_hash_request_no_title_or_game_id); + + TEST(test_init_add_game_hash_response); + TEST(test_init_add_game_hash_response_error); + TEST(test_init_add_game_hash_response_invalid_credentials); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_info.c b/src/rcheevos/test/rapi/test_rc_api_info.c new file mode 100644 index 000000000..22e43fb8c --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_info.c @@ -0,0 +1,350 @@ +#include "rc_api_info.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" +#include "rc_compat.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_fetch_achievement_info_request() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.first_entry = 100; + fetch_achievement_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&o=99&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_achievement_info_request_no_first() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_achievement_info_request_one_first() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.first_entry = 1; + fetch_achievement_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_achievement_info_request_friends_only() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.count = 50; + fetch_achievement_info_request.friends_only = 1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&f=1&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_achievement_info_response() { + rc_api_fetch_achievement_info_response_t fetch_achievement_info_response; + rc_api_achievement_awarded_entry_t* entry; + const char* server_response = "{\"Success\":true,\"AchievementID\":1234,\"Response\":{" + "\"NumEarned\":17,\"GameID\":2345,\"TotalPlayers\":25," + "\"RecentWinner\":[{\"User\":\"Player1\",\"DateAwarded\":1615654895}," + "{\"User\":\"Player2\",\"DateAwarded\":1600604303}]" + "}}"; + + memset(&fetch_achievement_info_response, 0, sizeof(fetch_achievement_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_achievement_info_response(&fetch_achievement_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_achievement_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.id, 1234); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.game_id, 2345); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_awarded, 17); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_players, 25); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_recently_awarded, 2); + + entry = &fetch_achievement_info_response.recently_awarded[0]; + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->awarded, 1615654895); + entry = &fetch_achievement_info_response.recently_awarded[1]; + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->awarded, 1600604303); + + rc_api_destroy_fetch_achievement_info_response(&fetch_achievement_info_response); +} + +static void test_init_fetch_leaderboard_info_request() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.first_entry = 101; + fetch_leaderboard_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&o=100&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_leaderboard_info_request_no_first() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_leaderboard_info_request_for_user() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.username = "Username"; + fetch_leaderboard_info_request.count = 20; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&u=Username&c=20"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_leaderboard_info_request_for_user_with_offset() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.username = "Username"; + fetch_leaderboard_info_request.first_entry = 11; /* should be ignored */ + fetch_leaderboard_info_request.count = 20; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&u=Username&c=20"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_leaderboard_info_response() { + rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; + rc_api_lboard_info_entry_t* entry; + const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":1234,\"GameID\":2345," + "\"LowerIsBetter\":1,\"LBTitle\":\"Title\",\"LBDesc\":\"Description\",\"LBFormat\":\"TIME\"," + "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1615654895}," + "{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2,\"Index\":6,\"DateSubmitted\":1600604303}]" + "}}"; + + memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 1234); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2345); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 1); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.author); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1382307141); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1623658699); + + entry = &fetch_leaderboard_info_response.entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 8765); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + entry = &fetch_leaderboard_info_response.entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 7654); + ASSERT_NUM_EQUALS(entry->submitted, 1600604303); + + rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); +} + +static void test_process_fetch_leaderboard_info_response2() { + rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; + rc_api_lboard_info_entry_t* entry; + const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":9999,\"GameID\":2222," + "\"LowerIsBetter\":0,\"LBTitle\":\"Title2\",\"LBDesc\":\"Description2\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":\"AuthorName\"," + "\"LBCreated\":\"2021-06-18 15:32:16\",\"LBUpdated\":\"2021-06-18 15:32:16\"," + "\"Entries\":[{\"User\":\"Player1\",\"Score\":1013580,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1624055310}," + "{\"User\":\"Player2\",\"Score\":133340,\"Rank\":1,\"Index\":6,\"DateSubmitted\":1624166772}]" + "}}"; + + memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 9999); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2222); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 0); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title2"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description2"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.author, "AuthorName"); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1624030336); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1624030336); + + entry = &fetch_leaderboard_info_response.entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 1013580); + ASSERT_NUM_EQUALS(entry->submitted, 1624055310); + entry = &fetch_leaderboard_info_response.entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 133340); + ASSERT_NUM_EQUALS(entry->submitted, 1624166772); + + rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); +} + +static void test_init_fetch_games_list_request() { + rc_api_fetch_games_list_request_t fetch_games_list_request; + rc_api_request_t request; + + memset(&fetch_games_list_request, 0, sizeof(fetch_games_list_request)); + fetch_games_list_request.console_id = 12; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_games_list_request(&request, &fetch_games_list_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameslist&c=12"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_games_list_response() { + rc_api_fetch_games_list_response_t fetch_games_list_response; + rc_api_game_list_entry_t* entry; + const char* server_response = "{\"Success\":true,\"Response\":{" + "\"1234\":\"Game Name 1\"," + "\"17\":\"Game Name 2\"," + "\"9923\":\"Game Name 3\"," + "\"12303\":\"Game Name 4\"," + "\"4338\":\"Game Name 5\"," + "\"5437\":\"Game Name 6\"" + "}}"; + + memset(&fetch_games_list_response, 0, sizeof(fetch_games_list_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_games_list_response(&fetch_games_list_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_games_list_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_games_list_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_games_list_response.num_entries, 6); + + entry = &fetch_games_list_response.entries[0]; + ASSERT_NUM_EQUALS(entry->id, 1234); + ASSERT_STR_EQUALS(entry->name, "Game Name 1"); + entry = &fetch_games_list_response.entries[1]; + ASSERT_NUM_EQUALS(entry->id, 17); + ASSERT_STR_EQUALS(entry->name, "Game Name 2"); + entry = &fetch_games_list_response.entries[2]; + ASSERT_NUM_EQUALS(entry->id, 9923); + ASSERT_STR_EQUALS(entry->name, "Game Name 3"); + entry = &fetch_games_list_response.entries[3]; + ASSERT_NUM_EQUALS(entry->id, 12303); + ASSERT_STR_EQUALS(entry->name, "Game Name 4"); + entry = &fetch_games_list_response.entries[4]; + ASSERT_NUM_EQUALS(entry->id, 4338); + ASSERT_STR_EQUALS(entry->name, "Game Name 5"); + entry = &fetch_games_list_response.entries[5]; + ASSERT_NUM_EQUALS(entry->id, 5437); + ASSERT_STR_EQUALS(entry->name, "Game Name 6"); + + rc_api_destroy_fetch_games_list_response(&fetch_games_list_response); +} + +void test_rapi_info(void) { + TEST_SUITE_BEGIN(); + + /* achievement info */ + TEST(test_init_fetch_achievement_info_request); + TEST(test_init_fetch_achievement_info_request_no_first); + TEST(test_init_fetch_achievement_info_request_one_first); + TEST(test_init_fetch_achievement_info_request_friends_only); + + TEST(test_process_fetch_achievement_info_response); + + /* leaderboard info */ + TEST(test_init_fetch_leaderboard_info_request); + TEST(test_init_fetch_leaderboard_info_request_no_first); + TEST(test_init_fetch_leaderboard_info_request_for_user); + TEST(test_init_fetch_leaderboard_info_request_for_user_with_offset); + + TEST(test_process_fetch_leaderboard_info_response); + TEST(test_process_fetch_leaderboard_info_response2); + + /* games list */ + TEST(test_init_fetch_games_list_request); + + TEST(test_process_fetch_games_list_response); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_runtime.c b/src/rcheevos/test/rapi/test_rc_api_runtime.c new file mode 100644 index 000000000..1f8ff3e73 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_runtime.c @@ -0,0 +1,1005 @@ +#include "rc_api_runtime.h" + +#include "rc_runtime_types.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_resolve_hash_request() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.username = "Username"; /* credentials are ignored - turns out server doesn't validate this API */ + resolve_hash_request.api_token = "API_TOKEN"; + resolve_hash_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameid&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_resolve_hash_request_no_credentials() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameid&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_resolve_hash_request_no_hash() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_resolve_hash_request_empty_hash() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = ""; + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_resolve_hash_response_match() { + rc_api_resolve_hash_response_t resolve_hash_response; + const char* server_response = "{\"Success\":true,\"GameID\":1446}"; + + memset(&resolve_hash_response, 0, sizeof(resolve_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_resolve_hash_response(&resolve_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(resolve_hash_response.response.succeeded, 1); + ASSERT_PTR_NULL(resolve_hash_response.response.error_message); + ASSERT_NUM_EQUALS(resolve_hash_response.game_id, 1446); + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +static void test_process_resolve_hash_response_no_match() { + rc_api_resolve_hash_response_t resolve_hash_response; + const char* server_response = "{\"Success\":true,\"GameID\":0}"; + + memset(&resolve_hash_response, 0, sizeof(resolve_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_resolve_hash_response(&resolve_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(resolve_hash_response.response.succeeded, 1); + ASSERT_PTR_NULL(resolve_hash_response.response.error_message); + ASSERT_NUM_EQUALS(resolve_hash_response.game_id, 0); + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +static void test_init_fetch_game_data_request() { + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_request_t request; + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = "Username"; + fetch_game_data_request.api_token = "API_TOKEN"; + fetch_game_data_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_data_request_no_id() { + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_request_t request; + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = "Username"; + fetch_game_data_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_game_data_response_empty() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"My Game\",\"ConsoleID\":23,\"ImageIcon\":\"/Images/012345.png\"," + "\"Achievements\":[],\"Leaderboards\":[]" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 177); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "My Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 23); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "012345"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_invalid_credentials() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_game_data_response.response.error_message, "Credentials invalid (0)"); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 0); + ASSERT_PTR_NULL(fetch_game_data_response.title); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 0); + ASSERT_PTR_NULL(fetch_game_data_response.image_name); + ASSERT_PTR_NULL(fetch_game_data_response.rich_presence_script); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_achievements() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\"," + "\"Created\":1376969412,\"Modified\":1376969412}," + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," + "\"Created\":1504474554,\"Modified\":1504474554}" + "],\"Leaderboards\":[]" + "}}"; + rc_api_achievement_definition_t* achievement; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); + achievement = fetch_game_data_response.achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_STR_EQUALS(achievement->description, "Desc1"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_STR_EQUALS(achievement->description, "Desc2"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 2); + ASSERT_STR_EQUALS(achievement->definition, "0=2"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->created, 1376970283); + ASSERT_NUM_EQUALS(achievement->updated, 1376970283); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_STR_EQUALS(achievement->description, "Desc3"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); + ASSERT_NUM_EQUALS(achievement->points, 0); + ASSERT_STR_EQUALS(achievement->definition, "0=3"); + ASSERT_STR_EQUALS(achievement->author, "User2"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->created, 1376969412); + ASSERT_NUM_EQUALS(achievement->updated, 1376969412); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_STR_EQUALS(achievement->description, "Desc4"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_leaderboards() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[],\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}," + "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," + "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}," + "{\"ID\":4403,\"Title\":\"Leaderboard3\",\"Description\":\"Desc3\"," + "\"Mem\":\"0=1\",\"Format\":\"UNKNOWN\",\"LowerIsBetter\":true,\"Hidden\":false}" + "]}}"; + rc_api_leaderboard_definition_t* leaderboard; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 3); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.leaderboards); + leaderboard = fetch_game_data_response.leaderboards; + + ASSERT_NUM_EQUALS(leaderboard->id, 4401); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + ++leaderboard; + ASSERT_NUM_EQUALS(leaderboard->id, 4402); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 1); + + ++leaderboard; + ASSERT_NUM_EQUALS(leaderboard->id, 4403); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard3"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc3"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_VALUE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_rich_presence() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]," + "\"RichPresencePatch\":\"Display:\\r\\nTest\\r\\n\"" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, "Display:\r\nTest\r\n"); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_rich_presence_null() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]," + "\"RichPresencePatch\":null" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_rich_presence_tab() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]," + "\"RichPresencePatch\":\"Display:\\r\\nTest\\tTab\\r\\n\"" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, "Display:\r\nTest\tTab\r\n"); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_init_ping_request() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_no_game_id() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_rich_presence() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + ping_request.rich_presence = "Level 1, 70% complete"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&m=Level+1%2c+70%25+complete"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_rich_presence_unicode() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1446; + ping_request.rich_presence = "\xf0\x9f\x9a\xb6:3, 1st Quest"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1446&m=%f0%9f%9a%b6%3a3%2c+1st+Quest"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_rich_presence_empty() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + ping_request.rich_presence = ""; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_ping_response() { + rc_api_ping_response_t ping_response; + const char* server_response = "{\"Success\":true}"; + + memset(&ping_response, 0, sizeof(ping_response)); + + ASSERT_NUM_EQUALS(rc_api_process_ping_response(&ping_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(ping_response.response.succeeded, 1); + ASSERT_PTR_NULL(ping_response.response.error_message); + + rc_api_destroy_ping_response(&ping_response); +} + +static void test_init_award_achievement_request_hardcore() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 1234; + award_achievement_request.hardcore = 1; + award_achievement_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=1&m=ABCDEF0123456789&v=b8aefaad6f9659e2164bc60da0c3b64d"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_non_hardcore() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 1234; + award_achievement_request.hardcore = 0; + award_achievement_request.game_hash = "ABABCBCBDEDEFFFF"; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=0&m=ABABCBCBDEDEFFFF&v=ed81d6ecf825f8cbe3ae1edace098892"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_no_hash() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 5432; + award_achievement_request.hardcore = 1; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=5432&h=1&v=31048257ab1788386e71ab0c222aa5c8"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_no_achievement_id() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.hardcore = 1; + award_achievement_request.game_hash = "ABABCBCBDEDEFFFF"; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_award_achievement_response_success() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":true,\"Score\":119102,\"SoftcoreScore\":777,\"AchievementID\":56481,\"AchievementsRemaining\":11}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119102); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56481); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 11); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_hardcore_already_unlocked() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494,\"AchievementsRemaining\":17}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has hardcore and regular achievements awarded."); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 17); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_non_hardcore_already_unlocked() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"User already has this achievement awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has this achievement awarded."); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_generic_failure() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_empty() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = ""; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_NO_RESPONSE); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_invalid_credentials() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_text() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "You do not have access to that resource"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "You do not have access to that resource"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_no_fields() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":true}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_429() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = + "\n" + "429 Too Many Requests\n" + "\n" + "

429 Too Many Requests

\n" + "
nginx
\n" + "\n" + ""; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "429 Too Many Requests"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_429_json() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = + "{\"Success\": false,\"Error\":\"Too Many Attempts\"}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Too Many Attempts"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_503_fancy() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = + "\n" + "\n" + "\n" + " \n" + " \n" + " \n" + " 503 Service Temporarily Unavailable\n" + "\n" + "\n" + "
\n" + " \n" + "

503

\n" + "

Service Temporarily Unavailable

\n" + " Sad Cheevo\n" + "

The RetroAchievements website is currently offline, we apologize for any inconvenience caused.

\n" + "

You can still earn achievements in any supported emulator.

\n" + "

Make sure that the emulator has informed you that you have successfully logged in before you begin playing to avoid missing unlocks.

\n" + "

For more information and updates, please join our Discord.

\n" + "
\n" + " retroachievements.org\n" + "
\n" + "\n" + ""; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "503 Service Temporarily Unavailable"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_init_submit_lboard_entry_request() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1234; + submit_lboard_entry_request.score = 10999; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1234&s=10999&m=ABCDEF0123456789&v=e13c9132ee651256f9d2ee8f06f75d76"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_zero_value() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1111; + submit_lboard_entry_request.score = 0; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1111&s=0&m=ABCDEF0123456789&v=9c2ac665157d68b8a26e83bb71dd8aaf"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_negative_value() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1111; + submit_lboard_entry_request.score = -234781; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1111&s=-234781&m=ABCDEF0123456789&v=fbe290266f2d121a7a37942e1e90f453"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_no_leaderboard_id() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.score = 12345; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_submit_lb_entry_response_success() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + rc_api_lboard_entry_t* entry; + const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1},{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 1); + ASSERT_PTR_NULL(submit_lb_entry_response.response.error_message); + ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 1234); + ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 2345); + ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 5); + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 17); + + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 2); + entry = &submit_lb_entry_response.top_entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 8765); + entry = &submit_lb_entry_response.top_entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 7654); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +static void test_process_submit_lb_entry_response_no_entries() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," + "\"TopEntries\":[]," + "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 1); + ASSERT_PTR_NULL(submit_lb_entry_response.response.error_message); + ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 1234); + ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 2345); + ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 5); + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 17); + + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 0); + ASSERT_PTR_NULL(submit_lb_entry_response.top_entries); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +static void test_process_submit_lb_entry_response_invalid_credentials() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 0); + ASSERT_STR_EQUALS(submit_lb_entry_response.response.error_message, "Credentials invalid (0)"); + ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 0); + ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 0); + ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 0); + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 0); + + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 0); + ASSERT_PTR_NULL(submit_lb_entry_response.top_entries); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +static void test_process_submit_lb_entry_response_entries_not_array() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," + "\"TopEntries\":{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1}," + "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 0); + ASSERT_STR_EQUALS(submit_lb_entry_response.response.error_message, "TopEntries not found in response"); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +void test_rapi_runtime(void) { + TEST_SUITE_BEGIN(); + + /* gameid */ + TEST(test_init_resolve_hash_request); + TEST(test_init_resolve_hash_request_no_credentials); + TEST(test_init_resolve_hash_request_no_hash); + TEST(test_init_resolve_hash_request_empty_hash); + + TEST(test_process_resolve_hash_response_match); + TEST(test_process_resolve_hash_response_no_match); + + /* patch */ + TEST(test_init_fetch_game_data_request); + TEST(test_init_fetch_game_data_request_no_id); + + TEST(test_process_fetch_game_data_response_empty); + TEST(test_process_fetch_game_data_response_invalid_credentials); + TEST(test_process_fetch_game_data_response_achievements); + TEST(test_process_fetch_game_data_response_leaderboards); + TEST(test_process_fetch_game_data_response_rich_presence); + TEST(test_process_fetch_game_data_response_rich_presence_null); + TEST(test_process_fetch_game_data_response_rich_presence_tab); + + /* ping */ + TEST(test_init_ping_request); + TEST(test_init_ping_request_no_game_id); + TEST(test_init_ping_request_rich_presence); + TEST(test_init_ping_request_rich_presence_unicode); + TEST(test_init_ping_request_rich_presence_empty); + + TEST(test_process_ping_response); + + /* awardachievement */ + TEST(test_init_award_achievement_request_hardcore); + TEST(test_init_award_achievement_request_non_hardcore); + TEST(test_init_award_achievement_request_no_hash); + TEST(test_init_award_achievement_request_no_achievement_id); + + TEST(test_process_award_achievement_response_success); + TEST(test_process_award_achievement_response_hardcore_already_unlocked); + TEST(test_process_award_achievement_response_non_hardcore_already_unlocked); + TEST(test_process_award_achievement_response_generic_failure); + TEST(test_process_award_achievement_response_empty); + TEST(test_process_award_achievement_response_invalid_credentials); + TEST(test_process_award_achievement_response_text); + TEST(test_process_award_achievement_response_no_fields); + TEST(test_process_award_achievement_response_429); + TEST(test_process_award_achievement_response_429_json); + TEST(test_process_award_achievement_response_503_fancy); + + /* submitlbentry */ + TEST(test_init_submit_lboard_entry_request); + TEST(test_init_submit_lboard_entry_request_zero_value); + TEST(test_init_submit_lboard_entry_request_negative_value); + TEST(test_init_submit_lboard_entry_request_no_leaderboard_id); + + TEST(test_process_submit_lb_entry_response_success); + TEST(test_process_submit_lb_entry_response_no_entries); + TEST(test_process_submit_lb_entry_response_invalid_credentials); + TEST(test_process_submit_lb_entry_response_entries_not_array); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_user.c b/src/rcheevos/test/rapi/test_rc_api_user.c new file mode 100644 index 000000000..5cf00b9b2 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_user.c @@ -0,0 +1,579 @@ +#include "rc_api_user.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" +#include "rc_compat.h" +#include "rc_version.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_start_session_request() +{ + rc_api_start_session_request_t start_session_request; + rc_api_request_t request; + + memset(&start_session_request, 0, sizeof(start_session_request)); + start_session_request.username = "Username"; + start_session_request.api_token = "API_TOKEN"; + start_session_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&l=" RCHEEVOS_VERSION_STRING); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_start_session_request_no_game() +{ + rc_api_start_session_request_t start_session_request; + rc_api_request_t request; + + memset(&start_session_request, 0, sizeof(start_session_request)); + start_session_request.username = "Username"; + start_session_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_start_session_response_legacy() +{ + rc_api_start_session_response_t start_session_response; + const char* server_response = "{\"Success\":true}"; + + memset(&start_session_response, 0, sizeof(start_session_response)); + + ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 1); + ASSERT_PTR_NULL(start_session_response.response.error_message); + ASSERT_NUM_EQUALS(start_session_response.num_unlocks, 0); + ASSERT_NUM_EQUALS(start_session_response.num_hardcore_unlocks, 0); + ASSERT_NUM_EQUALS(start_session_response.server_now, 0); + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void test_process_start_session_response() +{ + rc_api_start_session_response_t start_session_response; + /* startsession API only returns HardcoreUnlocks if an achievement has been earned in hardcore, + * even if the softcore unlock has a different timestamp. Unlocks are only returned for things + * only unlocked in softcore. */ + const char* server_response = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":111,\"When\":1234567890}," + "{\"ID\":112,\"When\":1234567891}," + "{\"ID\":113,\"When\":1234567860}" + "],\"Unlocks\":[" + "{\"ID\":114,\"When\":1234567840}" + "],\"ServerNow\":1234577777}"; + + memset(&start_session_response, 0, sizeof(start_session_response)); + + ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 1); + ASSERT_PTR_NULL(start_session_response.response.error_message); + ASSERT_NUM_EQUALS(start_session_response.num_unlocks, 1); + ASSERT_NUM_EQUALS(start_session_response.unlocks[0].achievement_id, 114); + ASSERT_NUM_EQUALS(start_session_response.unlocks[0].when, 1234567840); + ASSERT_NUM_EQUALS(start_session_response.num_hardcore_unlocks, 3); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[0].achievement_id, 111); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[0].when, 1234567890); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[1].achievement_id, 112); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[1].when, 1234567891); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[2].achievement_id, 113); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[2].when, 1234567860); + ASSERT_NUM_EQUALS(start_session_response.server_now, 1234577777); + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void test_process_start_session_response_invalid_credentials() +{ + rc_api_start_session_response_t start_session_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&start_session_response, 0, sizeof(start_session_response)); + + ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 0); + ASSERT_STR_EQUALS(start_session_response.response.error_message, "Credentials invalid (0)"); + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void test_init_login_request_password() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.password = "Pa$$w0rd!"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=login&u=Username&p=Pa%24%24w0rd%21"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_password_long() +{ + char buffer[1024], *ptr, *password_start; + rc_api_login_request_t login_request; + rc_api_request_t request; + int i; + + /* this generates a password that's 830 characters long */ + ptr = password_start = buffer + snprintf(buffer, sizeof(buffer), "r=login&u=ThisUsernameIsAlsoReallyLongAtRoughlyFiftyCharacters&p="); + for (i = 0; i < 30; i++) + ptr += snprintf(ptr, sizeof(buffer) - (ptr - buffer), "%dABCDEFGHIJKLMNOPQRSTUVWXYZ", i); + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "ThisUsernameIsAlsoReallyLongAtRoughlyFiftyCharacters"; + login_request.password = password_start; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, buffer); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_token() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.api_token = "ABCDEFGHIJKLMNOP"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=login&u=Username&t=ABCDEFGHIJKLMNOP"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_password_and_token() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.password = "Pa$$w0rd!"; + login_request.api_token = "ABCDEFGHIJKLMNOP"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=login&u=Username&p=Pa%24%24w0rd%21"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_no_password_or_token() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_alternate_host() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.password = "Pa$$w0rd!"; + + rc_api_set_host("localhost"); + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, "http://localhost/dorequest.php"); + ASSERT_STR_EQUALS(request.post_data, "r=login&u=Username&p=Pa%24%24w0rd%21"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_set_host(NULL); + rc_api_destroy_request(&request); +} + +static void test_process_login_response_success() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_unique_display_name() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"DisplayName\":\"Gaming Hero\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); + ASSERT_STR_EQUALS(login_response.display_name, "Gaming Hero"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_error() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Invalid User/Password combination. Please try again"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_generic_failure() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":false}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_empty() +{ + rc_api_login_response_t login_response; + const char* server_response = ""; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_NO_RESPONSE); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_text() +{ + rc_api_login_response_t login_response; + const char* server_response = "You do not have access to that resource"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "You do not have access to that resource"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_html() +{ + rc_api_login_response_t login_response; + const char* server_response = "You do not have access to that resource"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "You do not have access to that resource"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_no_required_fields() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "User not found in response"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_no_token() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"Username\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Token not found in response"); + ASSERT_STR_EQUALS(login_response.username, "Username"); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_no_optional_fields() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_null_score() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":null,\"SoftcoreScore\":null}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_init_fetch_user_unlocks_request_non_hardcore() +{ + rc_api_fetch_user_unlocks_request_t fetch_user_unlocks_request; + rc_api_request_t request; + + memset(&fetch_user_unlocks_request, 0, sizeof(fetch_user_unlocks_request)); + fetch_user_unlocks_request.username = "Username"; + fetch_user_unlocks_request.api_token = "API_TOKEN"; + fetch_user_unlocks_request.game_id = 1234; + fetch_user_unlocks_request.hardcore = 0; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_user_unlocks_request(&request, &fetch_user_unlocks_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=unlocks&u=Username&t=API_TOKEN&g=1234&h=0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_user_unlocks_request_hardcore() +{ + rc_api_fetch_user_unlocks_request_t fetch_user_unlocks_request; + rc_api_request_t request; + + memset(&fetch_user_unlocks_request, 0, sizeof(fetch_user_unlocks_request)); + fetch_user_unlocks_request.username = "Username"; + fetch_user_unlocks_request.api_token = "API_TOKEN"; + fetch_user_unlocks_request.game_id = 2345; + fetch_user_unlocks_request.hardcore = 1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_user_unlocks_request(&request, &fetch_user_unlocks_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=unlocks&u=Username&t=API_TOKEN&g=2345&h=1"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_user_unlocks_response_empty_array() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":true,\"UserUnlocks\":[],\"GameID\":11277,\"HardcoreMode\":false}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); + ASSERT_PTR_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 0); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_user_unlocks_response_invalid_credentials() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_user_unlocks_response.response.error_message, "Credentials invalid (0)"); + ASSERT_PTR_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 0); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_user_unlocks_response_one_item() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":true,\"UserUnlocks\":[1234],\"GameID\":11277,\"HardcoreMode\":false}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 1); + ASSERT_PTR_NOT_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[0], 1234); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_user_unlocks_response_several_items() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":true,\"UserUnlocks\":[1,2,3,4],\"GameID\":11277,\"HardcoreMode\":false}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 4); + ASSERT_PTR_NOT_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[0], 1); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[1], 2); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[2], 3); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[3], 4); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +void test_rapi_user(void) { + TEST_SUITE_BEGIN(); + + /* start session */ + TEST(test_init_start_session_request); + TEST(test_init_start_session_request_no_game); + + TEST(test_process_start_session_response_legacy); + TEST(test_process_start_session_response); + TEST(test_process_start_session_response_invalid_credentials); + + /* login */ + TEST(test_init_login_request_password); + TEST(test_init_login_request_password_long); + TEST(test_init_login_request_token); + TEST(test_init_login_request_password_and_token); + TEST(test_init_login_request_no_password_or_token); + TEST(test_init_login_request_alternate_host); + + TEST(test_process_login_response_success); + TEST(test_process_login_response_unique_display_name); + TEST(test_process_login_response_error); + TEST(test_process_login_response_generic_failure); + TEST(test_process_login_response_empty); + TEST(test_process_login_response_text); + TEST(test_process_login_response_html); + TEST(test_process_login_response_no_required_fields); + TEST(test_process_login_response_no_token); + TEST(test_process_login_response_no_optional_fields); + TEST(test_process_login_response_null_score); + + /* unlocks */ + TEST(test_init_fetch_user_unlocks_request_non_hardcore); + TEST(test_init_fetch_user_unlocks_request_hardcore); + + TEST(test_init_fetch_user_unlocks_response_empty_array); + TEST(test_init_fetch_user_unlocks_response_invalid_credentials); + TEST(test_init_fetch_user_unlocks_response_one_item); + TEST(test_init_fetch_user_unlocks_response_several_items); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos-test.sln b/src/rcheevos/test/rcheevos-test.sln new file mode 100644 index 000000000..5335edb28 --- /dev/null +++ b/src/rcheevos/test/rcheevos-test.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rcheevos-test", "rcheevos-test.vcxproj", "{74FBBFC4-5AC5-4A86-B292-B2F535E9912C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{900F3B07-2B47-4967-8296-CEC29233458F}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "validator", "..\validator\validator.vcxproj", "{16FABFA7-A2EC-4CD0-9E04-50315A2BB613}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x64.ActiveCfg = Debug|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x64.Build.0 = Debug|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x86.ActiveCfg = Debug|Win32 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x86.Build.0 = Debug|Win32 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x64.ActiveCfg = Release|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x64.Build.0 = Release|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x86.ActiveCfg = Release|Win32 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x86.Build.0 = Release|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x64.ActiveCfg = Debug|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x64.Build.0 = Debug|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x86.ActiveCfg = Debug|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x86.Build.0 = Debug|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x64.ActiveCfg = Release|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x64.Build.0 = Release|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x86.ActiveCfg = Release|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {02ADF144-5109-40C0-AA02-5BC5585A9883} + EndGlobalSection +EndGlobal diff --git a/src/rcheevos/test/rcheevos-test.vcxproj b/src/rcheevos/test/rcheevos-test.vcxproj new file mode 100644 index 000000000..64efa5dea --- /dev/null +++ b/src/rcheevos/test/rcheevos-test.vcxproj @@ -0,0 +1,245 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C} + rcheevostest + Application + MultiByte + v143 + + + v142 + + + v141 + + + + true + + + false + true + + + true + + + false + true + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)lua\src + true + + + Console + + + + + Level3 + Disabled + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)lua\src + true + + + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)lua\src + true + + + true + true + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)lua\src;%(AdditionalIncludeDirectories) + true + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/rcheevos/test/rcheevos-test.vcxproj.filters b/src/rcheevos/test/rcheevos-test.vcxproj.filters new file mode 100644 index 000000000..b7bcf4e57 --- /dev/null +++ b/src/rcheevos/test/rcheevos-test.vcxproj.filters @@ -0,0 +1,378 @@ + + + + + {c1b8966b-c711-43ce-ac8a-1dcca6b41ff3} + + + {9d6d4e6d-a0b7-4181-b536-93e34de1e625} + + + {43b6b53b-c37e-453e-97bf-df56c515f1a7} + + + {9060be9f-a1f8-4940-a6e6-8ada5c89ae94} + + + {faf643ae-e095-4db5-a701-3ea9ed343cc0} + + + {f890b4f1-8de5-4730-b612-3a7dbf65ca74} + + + {21341552-6b14-4f5b-a26c-d9393ffbdbcc} + + + {8789fd50-d142-4c00-8ef5-c33fbac7bd2c} + + + {b10820d1-368c-422b-a8ef-b3f7e804464e} + + + {d049182b-0721-46b5-b93e-6f8f7f572b45} + + + {0b946a47-0089-4118-a5b2-cd57245dc58b} + + + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rhash + + + tests\rcheevos + + + tests + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + src\rhash + + + tests\rhash + + + tests\rhash + + + tests\rcheevos + + + src\rcheevos + + + tests\rcheevos + + + src\rcheevos + + + tests\rcheevos + + + src\rcheevos + + + tests\rcheevos + + + src\rurl + + + tests\rurl + + + src\rhash + + + tests\rhash + + + tests\rhash + + + src\rapi + + + src\rapi + + + src\rapi + + + tests\rapi + + + tests\rapi + + + tests\rapi + + + src\rcheevos + + + tests\rcheevos + + + src\rapi + + + tests\rapi + + + src\rapi + + + tests\rapi + + + src\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + src\rcheevos + + + tests\rcheevos + + + + + src\rcheevos + + + tests\rcheevos + + + tests + + + tests\rhash + + + src\rhash + + + tests\rhash + + + src\rhash + + + src\rcheevos + + + src\rurl + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rapi + + + src\rapi + + + src\rapi + + + src\rapi + + + src\rcheevos + + + tests\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rapi + + + src\rapi + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + \ No newline at end of file diff --git a/src/rcheevos/test/rcheevos/mock_memory.h b/src/rcheevos/test/rcheevos/mock_memory.h new file mode 100644 index 000000000..32c9e4ea3 --- /dev/null +++ b/src/rcheevos/test/rcheevos/mock_memory.h @@ -0,0 +1,32 @@ +#ifndef MOCK_MEMORY_H +#define MOCK_MEMORY_H + +typedef struct { + unsigned char* ram; + unsigned size; +} +memory_t; + +static unsigned peekb(unsigned address, memory_t* memory) { + return address < memory->size ? memory->ram[address] : 0; +} + +static unsigned peek(unsigned address, unsigned num_bytes, void* ud) { + memory_t* memory = (memory_t*)ud; + + switch (num_bytes) { + case 1: return peekb(address, memory); + + case 2: return peekb(address, memory) | + peekb(address + 1, memory) << 8; + + case 4: return peekb(address, memory) | + peekb(address + 1, memory) << 8 | + peekb(address + 2, memory) << 16 | + peekb(address + 3, memory) << 24; + } + + return 0; +} + +#endif /* MOCK_MEMORY_H */ diff --git a/src/rcheevos/test/rcheevos/test_condition.c b/src/rcheevos/test/rcheevos/test_condition.c new file mode 100644 index 000000000..da190321f --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_condition.c @@ -0,0 +1,567 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_operand(rc_operand_t* self, char expected_type, char expected_size, unsigned expected_address) { + ASSERT_NUM_EQUALS(self->type, expected_type); + + switch (expected_type) { + case RC_OPERAND_ADDRESS: + case RC_OPERAND_DELTA: + case RC_OPERAND_PRIOR: + ASSERT_NUM_EQUALS(self->size, expected_size); + ASSERT_NUM_EQUALS(self->value.memref->address, expected_address); + break; + + case RC_OPERAND_CONST: + ASSERT_NUM_EQUALS(self->value.num, expected_address); + break; + } +} +#define assert_operand(operand, expected_type, expected_size, expected_address) ASSERT_HELPER(_assert_operand(operand, expected_type, expected_size, expected_address), "assert_operand") + +static void _assert_parse_condition( + const char* memaddr, char expected_type, + char expected_left_type, char expected_left_size, unsigned expected_left_value, + char expected_operator, + char expected_right_type, char expected_right_size, unsigned expected_right_value, + unsigned expected_required_hits +) { + rc_condition_t* self; + rc_parse_state_t parse; + rc_memref_t* memrefs; + char buffer[512]; + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + self = rc_parse_condition(&memaddr, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_EQUALS(self->type, expected_type); + assert_operand(&self->operand1, expected_left_type, expected_left_size, expected_left_value); + ASSERT_NUM_EQUALS(self->oper, expected_operator); + assert_operand(&self->operand2, expected_right_type, expected_right_size, expected_right_value); + ASSERT_NUM_EQUALS(self->required_hits, expected_required_hits); +} +#define assert_parse_condition(memaddr, expected_type, expected_left_type, expected_left_size, expected_left_value, \ + expected_operator, expected_right_type, expected_right_size, expected_right_value, expected_required_hits) \ + ASSERT_HELPER(_assert_parse_condition(memaddr, expected_type, expected_left_type, expected_left_size, expected_left_value, \ + expected_operator, expected_right_type, expected_right_size, expected_right_value, expected_required_hits), "assert_parse_condition") + +static void test_parse_condition(const char* memaddr, char expected_type, char expected_left_type, + char expected_operator, int expected_required_hits) { + if (expected_operator == RC_OPERATOR_NONE) { + assert_parse_condition(memaddr, expected_type, + expected_left_type, RC_MEMSIZE_8_BITS, 0x1234U, + expected_operator, + RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U, + expected_required_hits + ); + } + else { + assert_parse_condition(memaddr, expected_type, + expected_left_type, RC_MEMSIZE_8_BITS, 0x1234U, + expected_operator, + RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 8U, + expected_required_hits + ); + } +} + +static void test_parse_operands(const char* memaddr, + char expected_left_type, char expected_left_size, unsigned expected_left_value, + char expected_right_type, char expected_right_size, unsigned expected_right_value) { + assert_parse_condition(memaddr, RC_CONDITION_STANDARD, + expected_left_type, expected_left_size, expected_left_value, + RC_OPERATOR_EQ, + expected_right_type, expected_right_size, expected_right_value, + 0 + ); +} + +static void test_parse_modifier(const char* memaddr, char expected_operator, char expected_operand, double expected_multiplier) { + assert_parse_condition(memaddr, RC_CONDITION_ADD_SOURCE, + RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, + expected_operator, + expected_operand, RC_MEMSIZE_8_BITS, (int)expected_multiplier, + 0 + ); +} + +static void test_parse_modifier_shorthand(const char* memaddr, char expected_type) { + assert_parse_condition(memaddr, expected_type, + RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, + RC_OPERATOR_NONE, + RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1U, + 0 + ); +} + +static void test_parse_condition_error(const char* memaddr, int expected_error) { + if (expected_error == RC_OK) { + ASSERT_NUM_GREATER(rc_trigger_size(memaddr), 0); + } else { + ASSERT_NUM_EQUALS(rc_trigger_size(memaddr), expected_error); + } +} + +static int evaluate_condition(rc_condition_t* cond, memory_t* memory, rc_memref_t* memrefs) { + rc_eval_state_t eval_state; + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + rc_update_memref_values(memrefs, peek, memory); + return rc_test_condition(cond, &eval_state); +} + +static void test_evaluate_condition(const char* memaddr, int expected_comparator, int expected_result) { + rc_condition_t* self; + rc_parse_state_t parse; + char buffer[512]; + rc_memref_t* memrefs; + int ret; + unsigned char ram[] = {0x00, 0x11, 0x34, 0xAB, 0x56}; + memory_t memory; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + self = rc_parse_condition(&memaddr, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*memaddr, 0); + + rc_update_memref_values(memrefs, peek, &memory); /* capture delta for ram[1] */ + ram[1] = 0x12; + + ASSERT_NUM_EQUALS(self->optimized_comparator, expected_comparator); + ret = evaluate_condition(self, &memory, memrefs); + + if (expected_result) { + ASSERT_NUM_EQUALS(ret, 1); + } else { + ASSERT_NUM_EQUALS(ret, 0); + } +} + +static void test_default_comparator(const char* memaddr) { + rc_condset_t* condset; + rc_condition_t* condition; + rc_parse_state_t parse; + char buffer[512]; + rc_memref_t* memrefs; + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + condset = rc_parse_condset(&memaddr, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*memaddr, 0); + + condition = condset->conditions; + while (condition->next) + condition = condition->next; + + /* expect last condition to have default comparator - that's the point of this test */ + ASSERT_NUM_EQUALS(condition->optimized_comparator, RC_PROCESSING_COMPARE_DEFAULT); +} + +static void test_evaluate_condition_float(const char* memaddr, int expected_result) { + rc_condition_t* self; + rc_parse_state_t parse; + char buffer[512]; + rc_memref_t* memrefs; + int ret; + unsigned char ram[] = {0x00, 0x00, 0x00, 0x40, 0x83, 0x49, 0x0F, 0xDB}; /* FF0=2, FF4=2*pi */ + memory_t memory; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + self = rc_parse_condition(&memaddr, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*memaddr, 0); + + ret = evaluate_condition(self, &memory, memrefs); + + if (expected_result) { + ASSERT_NUM_EQUALS(ret, 1); + } else { + ASSERT_NUM_EQUALS(ret, 0); + } +} + +static void test_condition_compare_delta() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condition_t* cond; + rc_parse_state_t parse; + char buffer[512]; + rc_memref_t* memrefs; + + const char* cond_str = "0xH0001>d0xH0001"; + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + cond = rc_parse_condition(&cond_str, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*cond_str, 0); + memory.ram = ram; + memory.size = sizeof(ram); + + /* initial delta value is 0, 0x12 > 0 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* delta value is now 0x12, 0x12 = 0x12 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* delta value is now 0x12, 0x11 < 0x12 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* delta value is now 0x13, 0x12 > 0x11 */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); +} + +static void test_condition_delta_24bit() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condition_t* cond; + rc_parse_state_t parse; + char buffer[512]; + rc_memref_t* memrefs; + + const char* cond_str = "0xW0001>d0xW0001"; + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + cond = rc_parse_condition(&cond_str, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*cond_str, 0); + memory.ram = ram; + memory.size = sizeof(ram); + + /* initial delta value is 0x000000, 0xAB3412 > 0x000000 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* delta value is now 0xAB3412, 0xAB3412 == 0xAB3412 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* value changes to 0xAB3411, delta value is now 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* value changes to 0xAB3412, delta value is now 0xAB3411, 0xAB3412 > 0xAB3411 */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* ram[4] should not affect the 24-bit value, 0xAB3412 == 0xAB3412 */ + ram[4] = 0xAC; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* value changes to 0xAB3411, delta is still 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* ram[4] should not affect the 24-bit value, 0xAB3411 == 0xAB3411 */ + ram[4] = 0xAD; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); +} + +static void test_condition_prior_24bit() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condition_t* cond; + rc_parse_state_t parse; + char buffer[512]; + rc_memref_t* memrefs; + + const char* cond_str = "0xW0001>p0xW0001"; + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + cond = rc_parse_condition(&cond_str, &parse, 0); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*cond_str, 0); + memory.ram = ram; + memory.size = sizeof(ram); + + /* initial prior value is 0x000000, 0xAB3412 > 0x000000 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* delta value is now 0xAB3412, but prior is still 0x000000, 0xAB3412 > 0x000000 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* value changes to 0xAB3411, delta and prior values are now 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* value changes to 0xAB3412, delta and prior values are now 0xAB3411, 0xAB3412 > 0xAB3411 */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* ram[4] should not affect the 24-bit value, 0xAB3412 > 0xAB3411 */ + ram[4] = 0xAC; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* ram[4] should not affect the 24-bit value, 0xAB3412 > 0xAB3411 */ + ram[4] = 0xAD; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 1); + + /* value changes to 0xAB3411, delta and prior values are now 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* ram[4] should not affect the 24-bit value, 0xAB3411 < 0xAB3412 */ + ram[4] = 0xAE; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); + + /* ram[4] should not affect the 24-bit value, 0xAB3411 < 0xAB3412 */ + ram[4] = 0xAF; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, memrefs), 0); +} + +void test_condition(void) { + TEST_SUITE_BEGIN(); + + /* different comparison operators */ + TEST_PARAMS5(test_parse_condition, "0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234==8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234!=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_NE, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234<8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LT, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234<=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LE, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234>8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_GT, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234>=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_GE, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234<8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LT, 0); + + /* special accessors */ + TEST_PARAMS5(test_parse_condition, "d0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_DELTA, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "p0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_PRIOR, RC_OPERATOR_EQ, 0); + + /* flags */ + TEST_PARAMS5(test_parse_condition, "R:0xH1234=8", RC_CONDITION_RESET_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "P:0xH1234=8", RC_CONDITION_PAUSE_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234=8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); + TEST_PARAMS5(test_parse_condition, "B:0xH1234=8", RC_CONDITION_SUB_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); + TEST_PARAMS5(test_parse_condition, "C:0xH1234=8", RC_CONDITION_ADD_HITS, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "D:0xH1234=8", RC_CONDITION_SUB_HITS, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "M:0xH1234=8", RC_CONDITION_MEASURED, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "G:0xH1234=8", RC_CONDITION_MEASURED, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "Q:0xH1234=8", RC_CONDITION_MEASURED_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "I:0xH1234=8", RC_CONDITION_ADD_ADDRESS, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); + TEST_PARAMS5(test_parse_condition, "T:0xH1234=8", RC_CONDITION_TRIGGER, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "Z:0xH1234=8", RC_CONDITION_RESET_NEXT_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + + /* modifiers (only valid with some flags, use A:) */ + TEST_PARAMS5(test_parse_condition, "A:0xH1234*8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_MULT, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234/8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_DIV, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234&8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_AND, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234^8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_XOR, 0); + + TEST_PARAMS4(test_parse_modifier, "A:0xH1234", RC_OPERATOR_NONE, RC_OPERAND_CONST, 1); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*1", RC_OPERATOR_MULT, RC_OPERAND_CONST, 1); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*3", RC_OPERATOR_MULT, RC_OPERAND_CONST, 3); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*f0.5", RC_OPERATOR_MULT, RC_OPERAND_FP, 0.5); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*f.5", RC_OPERATOR_MULT, RC_OPERAND_FP, 0.5); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*-1", RC_OPERATOR_MULT, RC_OPERAND_CONST, -1); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*0xH3456", RC_OPERATOR_MULT, RC_OPERAND_ADDRESS, 0x3456); + + /* legacy serializers would include whatever happened to be in the right side before it was converted to a modifier. + * they should be ignored */ + TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234!=0xH1234", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0.60.", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0(60)", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + + /* hit counts */ + TEST_PARAMS5(test_parse_condition, "0xH1234=8(1)", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1); + TEST_PARAMS5(test_parse_condition, "0xH1234=8.1.", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1); /* legacy format */ + TEST_PARAMS5(test_parse_condition, "0xH1234=8(1000)", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1000); + + /* hex value is interpreted as a 16-bit memory reference */ + TEST_PARAMS7(test_parse_operands, "0xH1234=0x80", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x80U); + + TEST_PARAMS7(test_parse_operands, "0xL1234=0xU3456", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U, RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x3456U); + + /* shorthard for modifier conditions */ + TEST_PARAMS2(test_parse_modifier_shorthand, "A:0xH1234", RC_CONDITION_ADD_SOURCE); + TEST_PARAMS2(test_parse_modifier_shorthand, "B:0xH1234", RC_CONDITION_SUB_SOURCE); + TEST_PARAMS2(test_parse_modifier_shorthand, "I:0xH1234", RC_CONDITION_ADD_ADDRESS); + + /* parse errors */ + TEST_PARAMS2(test_parse_condition_error, "0xH1234==0", RC_OK); + TEST_PARAMS2(test_parse_condition_error, "H0x1234==0", RC_INVALID_CONST_OPERAND); + TEST_PARAMS2(test_parse_condition_error, "0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "C:0x1234", RC_INVALID_OPERATOR); /* shorthand only valid on modifier conditions */ + TEST_PARAMS2(test_parse_condition_error, "N:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "O:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "P:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "R:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "M:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "G:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "Y:0x1234", RC_INVALID_CONDITION_TYPE); + TEST_PARAMS2(test_parse_condition_error, "0x1234=1.2", RC_INVALID_REQUIRED_HITS); + TEST_PARAMS2(test_parse_condition_error, "0.1234==0", RC_INVALID_OPERATOR); /* period is assumed to be operator */ + TEST_PARAMS2(test_parse_condition_error, "0==0.1234", RC_INVALID_REQUIRED_HITS); /* period is assumed to be start of hit target, no end marker */ + + /* simple evaluations (ram[1] = 18, delta(ram[1]) = 17, ram[2] = 52, delta(ram[2]) = 52) */ + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>0", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001<=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001<18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0001=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001!=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0002=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002!=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001<0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001=0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001<=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xH0002=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0002!=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0002<=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0002>=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0002d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0001=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001!=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0002=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002!=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xM0001=1", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=1", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xK0001=2", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xK0001!=2", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xM0001=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xM0001!=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xM0002=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xM0002!=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xM0001=0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xM0001=0xH0000", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=0xH0000", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0000=0xM0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0000!=0xM0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xM0001=d0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=d0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xM0001=0xN0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xM0001!=0xN0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001=0xH0001", RC_PROCESSING_COMPARE_ALWAYS_TRUE, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0xH0001", RC_PROCESSING_COMPARE_ALWAYS_FALSE, 0); + TEST_PARAMS3(test_evaluate_condition, "1=1", RC_PROCESSING_COMPARE_ALWAYS_TRUE, 1); + TEST_PARAMS3(test_evaluate_condition, "0=1", RC_PROCESSING_COMPARE_ALWAYS_FALSE, 0); + + TEST_PARAMS1(test_default_comparator, "I:0xH0000_0xH0001=0"); /* indirect cannot be optimized */ + TEST_PARAMS1(test_default_comparator, "fF0001=f2.0"); /* float is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "p0xH0001=0"); /* prior is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "b0xH0001=0"); /* bcd is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "~0xH0001=0"); /* inverted is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "d0xH0001=0x 0001"); /* delta comparison only optimized for same address, same size */ + TEST_PARAMS1(test_default_comparator, "0xH0001=d0x 0001"); /* delta comparison only optimized for same address, same size */ + TEST_PARAMS1(test_default_comparator, "d0xH0001=0xH0002"); /* delta comparison only optimized for same address, same size */ + TEST_PARAMS1(test_default_comparator, "0xH0001=d0xH0002"); /* delta comparison only optimized for same address, same size */ + + /* float evaluations (ram[0] = 2.0, ram[4] = 3.14159 */ + TEST_PARAMS2(test_evaluate_condition_float, "fF0000=f2.0", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=f2.0", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000<=f2.0", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000>=f2.0", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000f2.0", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000f1.999999", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000f2.000001", 0); + + TEST_PARAMS2(test_evaluate_condition_float, "fF0000=2", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=2", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000<=2", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000>=2", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000<2", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000>2", 0); + + TEST_PARAMS2(test_evaluate_condition_float, "fM0004=f6.283185", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004!=f6.283185", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004<=f6.283185", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004>=f6.283185", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283185", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283183", 1); /* binary to float, the last decimal digit may */ + TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283187", 0); /* ensure we cover an epsilon gap */ + + TEST_PARAMS2(test_evaluate_condition_float, "fM0004=6", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004!=6", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004<=6", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004>=6", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004<6", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004>6", 1); + + TEST_PARAMS2(test_evaluate_condition_float, "fF0000==fF0000", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=fF0000", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000==fM0004", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=fM0004", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000fM0004", 0); + + TEST(test_condition_compare_delta); + TEST(test_condition_delta_24bit); + TEST(test_condition_prior_24bit); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_condset.c b/src/rcheevos/test/rcheevos/test_condset.c new file mode 100644 index 000000000..ebe8587b7 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_condset.c @@ -0,0 +1,4103 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +typedef struct rc_condset_memrefs_t +{ + rc_memref_t* memrefs; + rc_value_t* variables; +} rc_condset_memrefs_t; + +static void _assert_parse_condset(rc_condset_t** condset, rc_condset_memrefs_t* memrefs, void* buffer, const char* memaddr) +{ + rc_parse_state_t parse; + int size; + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs->memrefs); + rc_init_parse_state_variables(&parse, &memrefs->variables); + + *condset = rc_parse_condset(&memaddr, &parse, 0); + size = parse.offset; + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(size, 0); + ASSERT_PTR_NOT_NULL(*condset); +} +#define assert_parse_condset(condset, memrefs_out, buffer, memaddr) ASSERT_HELPER(_assert_parse_condset(condset, memrefs_out, buffer, memaddr), "assert_parse_condset") + +static void _assert_evaluate_condset(rc_condset_t* condset, rc_condset_memrefs_t* memrefs, memory_t* memory, int expected_result) { + int result; + rc_eval_state_t eval_state; + + rc_update_memref_values(memrefs->memrefs, peek, memory); + rc_update_variables(memrefs->variables, peek, memory, 0); + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + result = rc_test_condset(condset, &eval_state); + + /* NOTE: reset normally handled by trigger since it's not group specific */ + if (eval_state.was_reset) + rc_reset_condset(condset); + + ASSERT_NUM_EQUALS(result, expected_result); +} +#define assert_evaluate_condset(condset, memrefs, memory, expected_result) ASSERT_HELPER(_assert_evaluate_condset(condset, &memrefs, memory, expected_result), "assert_evaluate_condset") + +static rc_condition_t* condset_get_cond(rc_condset_t* condset, int cond_index) { + rc_condition_t* cond = condset->conditions; + + while (cond_index-- != 0) { + if (cond == NULL) + break; + + cond = cond->next; + } + + return cond; +} + +static void _assert_hit_count(rc_condset_t* condset, int cond_index, unsigned expected_hit_count) { + rc_condition_t* cond = condset_get_cond(condset, cond_index); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hit_count); +} +#define assert_hit_count(condset, cond_index, expected_hit_count) ASSERT_HELPER(_assert_hit_count(condset, cond_index, expected_hit_count), "assert_hit_count") + + +static void test_hitcount_increment_when_true() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18"); /* one condition, true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1U); + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2U); +} + +static void test_hitcount_does_not_increment_when_false() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001!=18"); /* one condition, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0U); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0U); +} + +static void test_hitcount_target() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=20(2)_0xH0002=52"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + + /* hit target met, overall is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); /* hit target met, not incremented */ + assert_hit_count(condset, 1, 4); + + /* first condition no longer true, but hit count was met so it acts true */ + ram[1] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 5); +} + +static void test_hitcount_two_conditions(const char* memaddr, unsigned expected_result, unsigned expected_hitcount1, unsigned expected_hitcount2) { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, memaddr); + assert_evaluate_condset(condset, memrefs, &memory, expected_result); + assert_hit_count(condset, 0, expected_hitcount1); + assert_hit_count(condset, 1, expected_hitcount2); +} + +static void test_hitcount_three_conditions(const char* memaddr, unsigned expected_result, unsigned expected_hitcount1, + unsigned expected_hitcount2, unsigned expected_hitcount3) { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, memaddr); + assert_evaluate_condset(condset, memrefs, &memory, expected_result); + assert_hit_count(condset, 0, expected_hitcount1); + assert_hit_count(condset, 1, expected_hitcount2); + assert_hit_count(condset, 2, expected_hitcount3); +} + +static void test_pauseif() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52_P:0xL0x0004=6"); + + /* first condition true, but ignored because both pause conditions are true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); /* Also true, but processing stops on first PauseIf */ + + /* first pause condition no longer true, but second still is */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); /* PauseIf goes to 0 when false */ + assert_hit_count(condset, 2, 1); + + /* both pause conditions not true, set will trigger */ + ram[4] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_pauseif_hitcount_one() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.1."); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* pause condition no longer true, but hitcount prevents trigger */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); +} + +static void test_pauseif_hitcount_two() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.2."); + + /* pause hit target has not been met, group is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* pause hit target has been met, group is false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + + /* pause condition is no longer true, but hitcount prevents trigger */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); +} + +static void test_pauseif_hitcount_with_reset() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.1._R:0xH0003=1"); + + /* pauseif triggered, non-pauseif conditions ignored */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* pause condition is no longer true, but hitcount prevents trigger */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* pause has precedence over reset, reset in group is ignored */ + ram[3] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); +} + +static void test_pauseif_does_not_increment_hits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_P:0xL0004=4"); + + /* both conditions true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* pause condition is true, other conditions should not tally hits */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* pause condition not true, other conditions should tally hits */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* pause condition is true, other conditions should not tally hits */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + + /* pause condition not true, other conditions should tally hits */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); +} + +static void test_pauseif_delta_updated() { + unsigned char ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_d0xH0002=60"); + + /* upaused, delta = 0, current = 52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* paused, delta = 52, current = 44 */ + ram[1] = 1; + ram[2] = 44; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + /* paused, delta = 44, current = 60 */ + ram[2] = 60; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* unpaused, delta = 60, current = 97 */ + ram[1] = 0; + ram[2] = 97; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); +} + +static void test_pauseif_indirect_delta_updated() { + unsigned char ram[] = {0x00, 0x00, 0x34, 0x3C, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_I:0xH0000_d0xH0002=60"); + + /* upaused, delta = 0, current = 52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 2, 0); + + /* paused, delta = 52, current = 44 */ + ram[1] = 1; + ram[2] = 44; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 2, 0); + + /* paused, delta = 44, current = 60 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 2, 0); + + /* unpaused, delta = 60, current = 97 */ + ram[1] = 0; + ram[3] = 97; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 2, 1); +} + +static void test_pauseif_short_circuit() { + unsigned char ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* evaluation of an achievement stops at the first true pauseif condition + * + * this allows achievements to prevent accumulating hits on a pauselock farther down + * in a group. a better solution would be to use an AndNext on the pauselock, but + * there are achievements in the wild relying on this behavior. + * see https://retroachievements.org/achievement/66804, which has a PauseIf 2040 frames + * pass (condition 5), but don't tally those frames if the game is paused (condition 3). + * similarly, https://retroachievements.org/achievement/154804 has a PauseIf 480 frames + * (condition 5), but don't tally those frames if the map is visible (condition 4). + */ + assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_P:0xH0002=1.3._0xH0003=1.4."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* non-pauseif true */ + ram[3] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* second pauseif tallies a hit, but it's not enough to pause the non-pauseif */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + + /* first pauseif is true, pauses the second pauseif and the non-pauseif */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + + /* first pauseif is false, the second pauseif and the non-pauseif can update */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 3); + + /* second pauseif reaches hitcount, non-pauseif does not update */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); + + /* pauseif hitcount still met, non-pauseif does not update */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); +} + +static void test_resetif() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_R:0xH0002=50_R:0xL0x0004=4"); + + /* first condition true, neither reset true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + + /* first reset true */ + ram[2] = 50; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); /* hitcount reset */ + + /* both resets true */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + + /* only second reset is true */ + ram[2] = 52; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + + /* neither reset true */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_cond_with_hittarget() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4"); + + /* both conditions true, reset not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + + /* reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* reset no longer true, hit target not met */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_hitcount() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4.2."); + + /* hitcounts on conditions 1 and 2 are incremented */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* hitcounts on conditions 1 and 2 are incremented. cond 1 now true, so entire set is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* hitcount on condition 2 is incremented, cond 1 already at its target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + + /* reset condition is true, but its hitcount is not met */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 1); + + /* second hit on reset condition should reset everything */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_hitcount_one() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4.1."); + + /* hitcounts on conditions 1 and 2 are incremented */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* hitcounts on conditions 1 and 2 are incremented. cond 1 now true, so entire set is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* hitcount on condition 2 is incremented, cond 1 already at its target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + + /* reset condition is true, its hitcount is met, so all hitcounts (including the resetif) should be reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_hitcount_addhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* never(repeated(3, byte(1) == 18 || low(4) == 6)) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_R:0xL0004=6(3)"); + + /* result is true, no non-reset conditions */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* total hitcount is met (2 for each condition, need 3 total) , everything resets */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); +} + +static void test_pauseif_resetif_hitcounts() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_R:0xH0002=50_P:0xL0004=4"); + + /* first condition is true, pauseif and resetif are not, so it gets a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* pause is true, hit not incremented or reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* reset if true, but set is still paused */ + ram[2] = 50; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* set no longer paused, reset clears hitcount */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + + /* reset no longer true, hits increment again */ + ram[2] = 52; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* hitcount met, set is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); +} + +static void test_resetnextif() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_0xH0001=18(2)_0xH0002=52.4."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 3); + + /* trigger resetnextif, last condition should not be reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 4); + + /* reset no longer true, hit target not met */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 4); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 4); +} + +static void test_resetnextif_non_hitcount_condition() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* resetnextif for non-hitcount condition will still set the hitcount to 0 and make it false */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_0xH0001=18_0xH0002=52.4."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* conditions continue to tally */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); + + /* target hitcount met, condset true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 4); + + /* trigger resetnextif, last condition should not be reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 4); + + /* reset no longer true (hit count on reset kept), condset is true again */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 4); +} + +static void test_resetnextif_addhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AddHits byte(0x0001)=18 <-- ResetNextIf resets hits on this condition before it's added to the accumulator + * upper4(0x0003)=10 (4) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_C:0xH0001=18_0xU0003=10(4)_0xH0002=52.1."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* total tallies match limit, trigger */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* resetnextif resets hits on condition 2, but not condition 3 - total will be 3/4 - does not trigger */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_addhits_chain() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AddHits byte(0x0001)=18 + * ResetNextIf low4(0x0004)=5 + * AddHits byte(0x0000)=0 + * upper4(0x0003)=10 (6) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_C:0xH0001=18_Z:0xL0004=5_C:0xH0000=0_0xU0003=10(6)_0xH0002=52.1."); + + /* resetnextifs not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 1); /* <- total hits is 3/6 */ + assert_hit_count(condset, 5, 1); + + /* first resetnextif true, only affects first addhits condition */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 2); + assert_hit_count(condset, 4, 2); /* <- total hits is 4/6 */ + assert_hit_count(condset, 5, 1); + + /* second resetnextif true, only affects second addhits condition */ + ram[4] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 3); /* <- total hits is 4/6 */ + assert_hit_count(condset, 5, 1); + + /* total hits reaches hit target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 4); /* <- total hits is 6/6 */ + assert_hit_count(condset, 5, 1); +} + +static void test_resetnextif_addhits_chain_total() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AddHits byte(0x0001)=18 + * AddHits byte(0x0000)=0 + * ResetNextIf low4(0x0004)=4 + * upper4(0x0003)=10 (6) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_C:0xH0000=0_Z:0xL0004=4_0xU0003=10(6)_0xH0002=52.1."); + + /* resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); /* <- total hits is 3/6 */ + assert_hit_count(condset, 4, 1); + + /* resetnextif true, only affects that condition, not total */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); /* <- total hits is 4/6 */ + assert_hit_count(condset, 4, 1); + + /* resetnextif still true, total matches target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 0); /* <- total hits is 6/6 */ + assert_hit_count(condset, 4, 1); +} + +static void test_resetnextif_using_andnext() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext byte(0x0001)=18 + * ResetNextIf low4(0x0004)=4 + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=18_Z:0xL0004=4_0xU0003=10(3)_0xH0002=52.1."); + + /* conditions 1, 3, and 4 true; resetnextif relies on conditions 1 and 2, so it not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* both resetnextif conditions true */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* first part of resetnextif not true */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* both resetnextif conditions true */ + ram[1] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_andnext() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AndNext byte(0x0001)=18 + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_N:0xH0001=18_0xU0003=10(3)_0xH0002=52.1."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* partial andnext not true */ + ram[3] = 0x86; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* andnext true again */ + ram[3] = 0xA0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* resetnextif resets all hits in the andnext chain */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_andnext_hitchain() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AndNext byte(0x0001)=18 (2) + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_N:0xH0001=18.2._0xU0003=10(3)_0xH0002=52.1."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); /* condition 2 must meet hitcount before condition three tallies a hit */ + assert_hit_count(condset, 3, 1); + + /* resetnextif true, should reset conditions 2 and 3 (3 was already 0) */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* resetnextif not true, condition 2 tallies a hit */ + ram[4] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* resetnextif still not true, condition 2 tallies another hit, which allows condition 3 to tally a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* resetnextif true, should reset both conditions 2 and 3 */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_addaddress() { + unsigned char ram[] = {0x00, 0x00, 0x02, 0x03, 0x04}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_Z:0xH0001=1_I:0xH0000_0xH0002=2(3)_0xH0004=4.8."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 1); + + /* resetnextif true */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 2); + + /* pointer changes. resetnextif not true, condition not true */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 3); + + /* condition true, resetnextif not true */ + ram[3] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 4); + + /* resetnextif true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 5); + + /* pointer changes, resetnextif and condition true */ + ram[0] = 0; + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 6); + + /* resetnextif not true */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 7); +} + +static void test_resetnextif_chain() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * ResetNextIf byte(0x0001)=1 + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_Z:0xH0001=1_0xU0003=10(3)_0xH0002=52.1."); + + /* both conditions true, resetnextifs not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* second resetnextif true, resets first hit count */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, disables second, allows hitcount */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* second resetnextif no longer true, first still keeps it disabled */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* first resetnextif no longer true (hit count on resetnextif itself is not reset), second already false */ + ram[4] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_chain_andnext() { + unsigned char ram[] = {0x00, 0x00, 0x01}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext byte(0x0000)=0 + * ResetNextIf byte(0x0001)=1 + * AndNext byte(0x0000)=1 + * ResetNextIf byte(0x0001)=1 + * byte(0x0002)=1 (5) + */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0000=0_Z:0xH0001=1_N:0xH0000=1_Z:0xH0001=1_0xH0002=1.5."); + + /* no ResetNextIf true. hit count incremented */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); /* hit captured on byte(0) == 0 */ + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 1); + + /* second ResetNextIf clause true */ + ram[0] = ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); /* ResetNextIf on condition 3 doesn't affect condition 1 */ + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 0); + + /* no ResetNextIf true, hit count incremented */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 1); + + /* first ResetNextIf clause true */ + ram[0] = 0; + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); /* first ResetNextIf affects second ResetNextIf */ + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 2); /* but not final clause */ +} + +static void test_resetnextif_chain_with_hits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * ResetNextIf byte(0x0001)=1 (2) + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_Z:0xH0001=1(2)_0xU0003=10(8)_0xH0002=52.1."); + + /* both conditions true, resetnextifs not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* second resetnextif true, but hit target not met */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, resets second, allows hitcount */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 1); + + /* second resetnextif no longer true, first still keeps it disabled */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 4); + assert_hit_count(condset, 3, 1); + + /* first resetnextif no longer true (hit count on resetnextif itself is not reset), second already false */ + ram[4] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 5); + assert_hit_count(condset, 3, 1); + + /* second resetnextif true again, but hitcount not met */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 6); + assert_hit_count(condset, 3, 1); + + /* second resetnextif hitcount met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* second resetnextif condition no longer true, but hitcount keeps it active */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, resets second, allows hit on third condition */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_pause_lock() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf byte(0x0002)=1 + * PauseIf byte(0x0001)=1 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xH0002=1_P:0xH0001=1(1)"); + + /* both conditions false */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* reset next true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + /* reset next and pause true */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* only pause true */ + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + + /* both conditions true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + + /* only pause true */ + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); + + /* both conditions false */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); + + /* reset next true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 0); +} + +static void test_addsource() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ + + /* first condition is true, but not sum */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* first condition is true, sum is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_overflow() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* adding two bytes will result in a value larger than 256, don't truncate to a byte */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_0xH0002=22"); + + /* sum is 0x102 (0x12 + 0xF0) */ + ram[2] = 0xF0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* sum is 0x122 (0x32 + 0xF0) */ + ram[1] = 0x32; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* NOTE: SubSource subtracts the first item from the second! */ + /* byte(1) - byte(2) == 14 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0_0xH0001=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ + + /* first condition is true, but not difference */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* first condition is true, difference is negative inverse of expected value */ + ram[2] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct again */ + ram[1] = 28; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_legacy_garbage() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) - byte(2) == 14 */ + /* old serializers would store the comparison and right-side value from the condition before it was converted to SubSource */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0xH0000_0xH0001=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ + + /* first condition is true, but not difference */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* first condition is true, difference is negative inverse of expected value */ + ram[2] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct again */ + ram[1] = 28; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_overflow() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* subtracting two bytes will result in a very large positive number, don't truncate to a byte */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0_0xH0001=14"); + + /* difference is -10 (8 - 18) */ + ram[2] = 8; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* difference is 0xFFFFFF0E (8 - 0xFA) */ + ram[1] = 0xFA; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addsource_subsource() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) - low(2) + low(4) == 14 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_B:0xL0002=0_0xL0004=14"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* sum is correct */ + ram[1] = 12; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* first condition is true, but not sum */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* byte(4) would make sum true, but not low(4) */ + ram[4] = 0x12; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* difference is correct again */ + ram[2] = 1; + ram[4] = 15; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 2); +} + +static void test_addsource_multiply() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) * 3 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*3_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 19; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_multiply() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(2) - byte(1) * 3 == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001*3_0xH0002=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 32; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct */ + ram[2] = 17; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_multiply_fraction() { + unsigned char ram[] = {0x00, 0x08, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) * 0.75 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*f0.75_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 15; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_multiply_address() { + unsigned char ram[] = {0x00, 0x06, 0x04, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) * byte(0) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*0xH00000_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 19; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_divide() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) / 3 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/3_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_compare_percentage() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* (byte(0)/byte(1) > 0.5) => (byte(1) * 0.5) < byte(0) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*f0.5_0<0xH0000"); + + /* 0/6 > 50% = false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + ram[0] = 2; assert_evaluate_condset(condset, memrefs, &memory, 0); /* 2/6 > 50% = false */ + ram[0] = 4; assert_evaluate_condset(condset, memrefs, &memory, 1); /* 4/6 > 50% = true */ + ram[1] = 7; assert_evaluate_condset(condset, memrefs, &memory, 1); /* 4/7 > 50% = true */ + ram[0] = 3; assert_evaluate_condset(condset, memrefs, &memory, 0); /* 3/7 > 50% = false */ +} + +static void test_subsource_divide() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(2) - byte(1) / 3 == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001/3_0xH0002=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is not correct */ + ram[1] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_divide_address() { + unsigned char ram[] = {0x00, 0x06, 0x10, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) / byte(0) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/0xH00000_0xH0002=22"); + + /* sum is not correct (divide by zero) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 21; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_divide_self() { + unsigned char ram[] = {0x00, 0x06, 0x10, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) / byte(1) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/0xH00001_0xH0002=22"); + + /* sum is not correct (1 + 16 != 22) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct (1 + 21 == 22) */ + ram[2] = 21; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct (0 + 21 == 22) */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct (0 + 22 == 22) */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_mask() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) & 0x07 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001&h7_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 0x74; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_xor() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) ^ 0x05 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001^h5_0xH0002=22"); + + /* sum (6 ^ 5 + 52 == 22) is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum ((6 ^ 5 = 3) + 19 = 22) is correct */ + ram[2] = 19; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum ((17 ^ 5 = 20) + 19 = 22) is not correct */ + ram[1] = 0x11; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum ((17 ^ 5 = 20) + 2 = 22) is correct */ + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_float_first() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F}; /* fF0004 = 1.5 */ + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* float(4) + float(4) + float(4) + 1 == 5.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:fF0004_A:fF0004_A:fF0004_1=f5.5"); + + /* sum is correct */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addsource_float_second() { + unsigned char ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F}; /* fF0004 = 1.5 */ + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* 1 + float(4) + float(4) + float(4) == 5.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_fF0004=f5.5"); + + /* the first value defines the type of the accumulator. since it's an integer, each float will be + * truncated when it is added to the accumulator, so 1 + (1.5) + (1.5) is 3. when the accumulator is + * added to the final value, the accumulator is converted to match the final value, so 3.0 + 1.5 = 4.5. */ + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 + float(4) + float(4) + float(4) == 4.5 (note: two intermediary floats are converted to int, but not last) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_fF0004=f4.5"); + /* sum is correct */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 1 + float(4) + float(4) + float(4) + 0 == 4.0 (note: all intermediate floats are converted to int) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_A:fF0004_0=f4.0"); + /* sum is correct */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_mask() { + unsigned char ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(2) - byte(1) & 0x06 == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001&6_0xH0002=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is not correct */ + ram[1] = 10; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_overflow_comparison_equal() { + unsigned char ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "A==B" can be expressed as "-A+B==0" */ + + /* - byte(0) + byte(1) = 0 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_0xH0001=0"); + + /* 1 == 0 = false */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 == 1 = true */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 == 0 = true */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 == 1 = false */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 == 255 = false */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 == 255 = true */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 == 254 = false */ + ram[0] = 255; ram[1] = 254; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 == 0 = false */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_overflow_comparison_greater() { + unsigned char ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "A>B" can be expressed as "-A+B>M" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) + byte(1) > 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_0xH0001>256"); + + /* 1 > 0 = true */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 1 > 1 = false */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 > 0 = false */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 > 1 = false */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 > 255 = false */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 > 255 = false */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 > 254 = true */ + ram[0] = 255; ram[1] = 254; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 > 0 = true */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_overflow_comparison_greater_or_equal() { + unsigned char ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "A>=B" can be expressed as "-A-1+B>=M" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) - 1 + byte(1) > 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_B:1_0xH0001>=256"); + + /* 1 >= 0 = true */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 1 >= 1 = true */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 >= 0 = true */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 >= 1 = false */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 >= 255 = false */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 >= 255 = true */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 >= 254 = true */ + ram[0] = 255; ram[1] = 254; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 >= 0 = true */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_overflow_comparison_lesser() { + unsigned char ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "AM" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) + byte(1) + 256 > 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_A:0xH0001_256>256"); + + /* 1 < 0 = false */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 < 1 = false */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 < 0 = false */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 < 1 = true */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 < 255 = true */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 < 255 = false */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 254 < 255 = true */ + ram[0] = 254; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 < 0 = false */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_overflow_comparison_lesser_or_equal() { + unsigned char ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "AM" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) + byte(1) + 256 >= 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_A:0xH0001_256>=256"); + + /* 1 <= 0 = false */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 <= 1 = true */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 <= 0 = true */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 <= 1 = true */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 <= 255 = true */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 <= 255 = true */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 254 <= 255 = true */ + ram[0] = 254; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 <= 0 = false */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_float() { + unsigned char ram[] = {0x06, 0x00, 0x00, 0x00, 0x92, 0x44, 0x9A, 0x42}; /* fF0004 = 77.133926 */ + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* float(0x0004) - word(0) * 1.666667 > 65.8 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0x 0000*f1.666667_fF0004>f65.8"); + + /* 77.133926 - (6 * 1.666667) = 77.133926 - 10 = 67.133926 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 77.133926 - (7 * 1.666667) = 77.133926 - 11.6667 = 65.4672 */ + ram[0] = 7; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, byte(1) == 18 || low(4) == 6) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18(2)_0xL0004=6(4)"); + + /* both conditions true, total not met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* total hits met (two for each condition) */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + + /* target met for first, it stops incrementing, second continues */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + + /* reset hit counts */ + rc_reset_condset(condset); + + /* both conditions true, total not met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* first condition not true, total not met*/ + ram[1] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + + /* total met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 3); +} + +static void test_addhits_multiple() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, byte(1) == 18 || low(4) == 6) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18(2)_0xL0004=6(4)_0xL0004=6(3)"); + + /* both conditions true, total not met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* total hits met (two for each condition), third condition not yet met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + /* target met for first, it stops incrementing, second and third continue */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); +} + +static void test_addhits_no_target() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AddHits is not a substitution for OrNext */ + /* since the second condition doesn't have a target hit count, the hits tallied by the first condition are ignored */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_0xH0000=1"); + + /* first condition true, but ignored */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + /* second condition true, overall is true */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + + /* second condition no longer true, overall is not true */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); +} + +static void test_addhits_with_addsource() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(2, (byte(1) + byte(2) == 70) || byte(0) == 0) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001_C:0xH0002=70_0xH0000=0(2)"); + + /* addsource (conditions 1 and 2) is true, condition 3 is true, total of two hits, overall is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* repeated(2, byte(0) == 0 || (byte(1) + byte(2) == 70)) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0000=0_A:0xH0001=0_0xH0002=70(2)"); + + /* condition 1 is true, addsource (conditions 2 and 3) is true, total of two hits, overall is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); +} + +static void test_subhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, low(4) == 6, deducting = repeated(2, byte(1) == 16)) */ + /* NOTE: cannot have SubHits without AddHits as there's no way to reach the final hit target + * if hits are subtracted but not added */ + assert_parse_condset(&condset, &memrefs, buffer, "D:0xH0001=16(2)_C:0xL0004=6_0=1(4)"); + + /* second condition true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + + /* both conditions true 1+3 == 4, not -1+3 != 4, no trigger */ + ram[1] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 3); + + /* -2+4 != 4 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 4); + + /* first condition target met, -2+5 != 4 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 5); + + /* total met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 6); +} + +static void test_subhits_below_zero() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, low(4) == 6, deducting = repeated(2, byte(1) == 16)) */ + /* NOTE: cannot have SubHits without AddHits as there's no way to reach the final hit target + * if hits are subtracted but not added */ + assert_parse_condset(&condset, &memrefs, buffer, "D:0xH0001=18(2)_C:0xL0002=6_0=1(4)"); + + /* first condition true. -1 less than 0. target hit count is unsigned. + make sure comparison doesn't treat -1 as unsigned */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* first condition target met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* both conditions true. takes 6 counts on second condition to reach hit target because + first condition is currently -2 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 4); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 5); + + /* total met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 6); +} + +static void test_andnext() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(3, byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=20_N:0xH0002=20_0xH0003=20.3."); + + /* all conditions are false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* first two are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* all three are true, tally hits. hits are tallied for each true statement starting with the first */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* middle condition not true, only first tallies a hit */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* all three conditions are true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + /* third condition not true, first two tally hits */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + + /* all three conditions are true, hit target reached */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 3); + + /* hit target previously reached */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 6); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 3); + + /* second condition no longer true, only first condition tallied, hit target was previously met */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 7); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 3); +} + +static void test_andnext_boundaries() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000) == 0 && once(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) && byte(0x0000) == 0 */ + assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_0xH0003=20.1._0xH0000=0"); + + /* first and last condition are true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 1); + + /* final condition of AndNext chain is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 2); + + /* two conditions of AndNext chain are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 3); + + /* whole AndNext chain is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 4); +} + +static void test_andnext_resetif() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000) == 0 && never(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_R:0xH0003=20"); + + /* tally a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* final condition of AndNext chain is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* two conditions of AndNext chain are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* middle condition not true */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* third condition not true */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); +} + +static void test_andnext_pauseif() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000) == 0 && unless(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_P:0xH0003=20"); + + /* tally a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* final condition of AndNext chain is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* two conditions of AndNext chain are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* middle condition not true */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); /* pauseif goes to 0 when not true */ + + /* whole AndNext chain is true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* third condition not true */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 0); /* pauseif goes to 0 when not true */ + + /* whole AndNext chain is true */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 4); + assert_hit_count(condset, 3, 1); +} + +static void test_andnext_addsource() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* once(byte(0x0001) + byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_N:0xH0002=20_0xH0003=20.1."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition true */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* conditions 2 and 3 true, but AddSource in condition 1 makes condition 2 not true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* AddSource condition true via sum, whole set is true */ + ram[1] = ram[2] = 10; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); +} + +static void test_andnext_addhits() { + unsigned char ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(5, (byte(0) == 1 && byte(0x0001) > prev(byte(0x0001))) || byte(0) == 2 || 0 == 1) */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH00=1_C:0xH01>d0xH01_N:0=1_0=1.2."); + + /* initialize delta */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* second part of AndNext is true, but first is still false */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* both parts of AndNext are true */ + ram[0] = 1; + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* And Next true again, hit count should match target */ + ram[1] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); +} + +static void test_andnext_between_addhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext has higher priority than AddHits + * + * AddHits byte(0x0001) == 20 (2) + * AndNext byte(0x0002) == 20 (2) <-- hit count only applies to line 2, AddHits on line 1 modifies line 3 + * byte(0x0003) == 20 (4) + * + * The AndNext on line 2 will combine with line 3, not line 1, so the overall interpretation is: + * + * repeated(4, repeated(2, byte(0x0001) == 20) || (byte(0x0002) == 20 && byte(0x0003) == 20))) + */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=20.2._N:0xH0002=20.2._0xH0003=20.4."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition is not enough to trigger */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* second condition is true, but only has one hit, so won't increment third */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* first condition true, but not second, only first will increment */ + /* hits from first condition should not cause second condition to act true */ + ram[2] = 0; + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* all three conditions are true. the first has already hit its target hit count, the + * second and third will increment. the total of the first and third is only 3, so no trigger */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + + /* third clause will tally again and set will be true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); +} + +static void test_andnext_with_hits_chain() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext has higher priority than AddHits + * + * AndNext byte(0x0001) == 20 (1) + * AndNext byte(0x0002) == 20 (1) + * byte(0x0003) == 20 (1) + * + * Line 1 must be true before line 2 can be true, which has to be true before line 3 + * + * a = once(byte(0x0001) == 20) + * b = once(a && byte(0x0002) == 20) + * c = once(b && byte(0x0003) == 20) + */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=20.1._N:0xH0002=20.1._0xH0003=20.1."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition is not enough to trigger */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* second condition is true, cut can't tally until the first is true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* first condition is true, but not second, only first will increment */ + ram[2] = 0; + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition cannot tally without the previous items in the chain */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* only second condition true. first is historically true, so second can tally */ + ram[3] = ram[1] = 0; + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* only final condition true, first two historically true, so can tally */ + ram[3] = 20; + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* nothing true, but all historically true, overall still true */ + ram[3] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); +} + +static void test_andnext_changes_to() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0001) ~> 18 */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=18_d0xH0001!=18"); + + /* value already 18, initial delta value is 0, so its considered changed */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* value already 18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value no longer 18 */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value changes to 18 */ + ram[1] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* value already 18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_ornext() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(5, byte(0x0001) == 20 || byte(0x0002) == 20 || byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "O:0xH0001=20_O:0xH0002=20_0xH0003=20.6."); + + /* first condition is true, which chains to make the second and third conditions true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* first and second are true, all but third should update, but only 1 hit each */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + /* all three true, only increment each once */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); + + /* only middle is true, first won't be incremented */ + ram[1] = ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 4); + + /* only last is true, only it will be incremented */ + ram[2] = 30; + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 5); + + /* none are true */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 5); + + /* first is true, hit target met, set is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 6); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 6); + assert_hit_count(condset, 2, 6); +} + +static void test_andnext_ornext_interaction() { + unsigned char ram[] = {0, 0, 0, 0, 0}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext and OrNext are evaluated at each step: (((1 || 2) && 3) || 4) */ + assert_parse_condset(&condset, &memrefs, buffer, "O:0xH0001=1_N:0xH0002=1_O:0xH0003=1_0xH0004=1"); + + ram[3] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((0 || 0) && 0) || 0) = 0 */ + ram[4] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 0) || 1) = 1 */ + ram[3] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 1) || 1) = 1 */ + ram[4] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((0 || 0) && 1) || 0) = 0 */ + ram[2] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 1) || 0) = 1 */ + ram[1] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 0) = 1 */ + ram[2] = 0; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 0) = 1 */ + ram[3] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((1 || 0) && 0) || 0) = 0 */ + ram[4] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 0) || 1) = 1 */ + ram[3] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 1) = 1 */ +} + +static void test_addaddress_direct_pointer() { + unsigned char ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000 + byte(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=22"); + + /* initially, byte(0x0000 + 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value, still correct */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_direct_pointer_delta() { + unsigned char ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* delta(byte(0x0000 + byte(0xh0000))) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_d0xH0000=22"); + + /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + /* byte(0x0000 + 2); value=52, prev=22, prior=22 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 2); value=52, prev=52, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value, still correct */ + /* byte(0x0000 + 1); value=22, prev=22, prior=52 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + /* byte(0x0000 + 1); value=11, prev=22, prior=22 */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 1); value=11, prev=11, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to secondary value, which is correct */ + /* byte(0x0000 + 2); value=22, prev=11, prior=11 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=11 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_direct_pointer_prior() { + unsigned char ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* delta(byte(0x0000 + byte(0xh0000))) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_p0xH0000=22"); + + /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to new value */ + /* byte(0x0000 + 2); value=52, prev=22, prior=22 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 2); value=52, prev=52, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* new pointed-at value is correct */ + /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to original value, still correct */ + /* byte(0x0000 + 1); value=22, prev=22, prior=52 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* original value no longer correct */ + /* byte(0x0000 + 1); value=11, prev=22, prior=22 */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 1); value=11, prev=11, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to secondary value, which is correct */ + /* byte(0x0000 + 2); value=22, prev=11, prior=11 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=11 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=32, prev=22, prior=22 */ + ram[2] = 32; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer() { + unsigned char ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002 + byte(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xH0002=22"); + + /* initially, byte(0x0001 + 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* non-offset value is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer_negative() { + unsigned char ram[] = {0x02, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0xh0000) - 1) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xHFFFFFFFF=22"); + + /* initially, byte(0x0002 - 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* non-offset value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to invalid address */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to already correct value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer_out_of_range() { + unsigned char ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56, 0x16}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram) - 1; /* purposely hide ram[5] */ + + /* byte(0x0002 + byte(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xH0002=22"); + + /* pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* way out of bounds */ + ram[0] = 100; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* boundary condition - ram[5] value is correct, but should be unreachable */ + /* NOTE: address validation must be handled by the registered 'peek' callback */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_indirect_pointer_multiple() { + unsigned char ram[] = {0x01, 0x02, 0x03, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* the expectation is that the AddAddress lines will share rc_memref_value_t's, but the following lines + will generate their own rc_memref_value_t's for indirection. none of that is actually verified. */ + assert_parse_condset(&condset, &memrefs, buffer, + "I:0xH0000=0_0xH0002=22_I:0xH0000=0_0xH0003=23_I:0xH0001=0_0xH0003=24"); + /* $(0002 + $0000) == 22 && $(0003 + $0000) == 23 && $(0003 + $0001) == 24 */ + /* $0003 (0x34) == 22 && $0004 (0xAB) == 23 && $0005 (0x56) == 24 */ + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 5, 0); + + /* first condition is true */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 5, 0); + + /* second condition is true */ + ram[4] = 23; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 5, 0); + + /* third condition is true */ + ram[5] = 24; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 3, 2); + assert_hit_count(condset, 5, 1); +} + +static void test_addaddress_pointer_data_size_differs_from_pointer_size() { + unsigned char ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002 + word(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0x 0002=22"); + + /* 8-bit pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 16-bit pointed-at value is correct */ + ram[4] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is only partially correct */ + ram[3] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_double_indirection() { + unsigned char ram[] = {0x01, 0x02, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000 + byte(0x0000 + byte(0x0000))) == 22 | $($($0000))) == 22*/ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_I:0xH0000=0_0xH0000=22"); + + /* value is correct: $0000=1, $0001=2, $0002 = 22 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* second pointer in chain causes final pointer to point at address 3 */ + ram[1] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* first pointer points at address 2, which is 22, so out-of-bounds */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* second pointer points at address 3, which is correct */ + ram[2] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* first pointer is out of range, so returns 0 for the second pointer, $0 contains the correct value */ + ram[0] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_double_indirection_with_delta() { + unsigned char ram[] = { 0, 2, 4 }; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* prev(byte(0x0000 + byte(0x0000 + byte(0x0000)))) == 22 | prev($($($0000)))) == 22*/ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_I:0xH0000=0_d0xH0000=4"); + + /* NOTE: indirectly calculated memrefs keep their own delta, not the delta of the newly pointed to + * value. using the intermediate deltas to calculate an address for the current frame will + * generate incorrect values. Only the final item in the chain should have the delta. */ + + /* 1st frame: A = Mem[0] = 0 (delta[0] = 0), B = Mem[A] = Mem[0] = 0 (delta B = 0), C = Mem[B] = Mem[0] = 0 (delta C = 0) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 2nd frame: A = Mem[0] = 1 (delta[0] = 0), B = Mem[A] = Mem[1] = 2 (delta B = 0), C = Mem[B] = Mem[2] = 4 (delta C = 0) */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 3nd frame: A = Mem[0] = 1 (delta[0] = 1), B = Mem[A] = Mem[1] = 2 (delta B = 2), C = Mem[B] = Mem[2] = 4 (delta C = 4) */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_double_indirection_with_delta_incorrect() { + unsigned char ram[] = { 0, 2, 4 }; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* prev(byte(0x0000 + prev(byte(0x0000 + prev(byte(0x0000)))))) == 22 | prev($($($0000)))) == 22*/ + assert_parse_condset(&condset, &memrefs, buffer, "I:d0xH0000=0_I:d0xH0000=0_d0xH0000=4"); + + /* putting prevs on each step of the chain results in using old pointers to get to data that may not exist yet, + * but this validates that incorrect behavior */ + + /* 1st frame: Mem[0] = 0 (delta[0] = 0), B = Mem[deltaA] = Mem[0] = 0 (delta B = 0), C = Mem[deltaB] = Mem[0] = 0 (delta C = 0) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 2nd frame: Mem[0] = 1 (delta[0] = 0), B = Mem[deltaA] = Mem[0] = 1 (delta B = 0), C = Mem[deltaB] = Mem[0] = 1 (delta C = 0) */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 3rd frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 1), C = Mem[deltaB] = Mem[1] = 2 (delta C = 1) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 4th frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 2), C = Mem[deltaB] = Mem[2] = 4 (delta C = 2) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 5th frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 2), C = Mem[deltaB] = Mem[2] = 4 (delta C = 4) */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_adjust_both_sides() { + unsigned char ram[] = {0x02, 0x11, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0) > delta $($0) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000>d0xH0000"); + + /* initial delta will be 0, so 2 will be greater */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* delta should be the same as current */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value increased */ + ram[2]++; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* value decreased */ + ram[2]--; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* this is a small hiccup in the AddAddress behavior. when the pointer changes, we + * can't reasonably know the previous value, so delta will be 0 for the first frame. + * 52 is greater than 0 (even though it didn't change), so set will be true. */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_adjust_both_sides_different_bases() { + unsigned char ram[] = {0x02, 0x11, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0) == $($0 + 1) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=0xH0001"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* values are the same */ + ram[2] = ram[3]; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust pointer */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* values are the same */ + ram[1] = ram[2]; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_scaled() { + unsigned char ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0 * 2) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xH0000=22"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust pointer */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new value is correct */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[2] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_scaled_negative() { + unsigned char ram[] = {0x01, 0x12, 0x34, 0xAB, 0x01}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($4 * -1 + 2) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0004*4294967295_0xH0002=22"); /* 4294967295 = 0xFFFFFFFF = -1 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value is correct: $(1 * -1 + 2) = $(1) */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust pointer: $(2 * -1 + 2) = $(0) */ + ram[4] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new value is correct */ + ram[0] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value */ + ram[4] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_prior_sequence() { + unsigned char ram[] = {0x00}; + memory_t memory; + rc_condset_t* condset; + rc_condset_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* prior(bit0(0))==1 && prior(bit1(0))==1 && prior(bit2(0))==1 */ + assert_parse_condset(&condset, &memrefs, buffer, "p0xM0000=1_p0xN0000=1_p0xO0000=1"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value ~> 1 [0001], all priors still 0 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* value ~> 2 [0010], prior(bit0(0)) = 1, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* value ~> 3 [0011], prior(bit0(0)) = 0, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* value ~> 4 [0100], prior(bit0(0)) = 1, prior(bit1(0)) = 1, prior(bit2(0)) = 0 */ + ram[0] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* value ~> 5 [0101], prior(bit0(0)) = 0, prior(bit1(0)) = 1, prior(bit2(0)) = 0 */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* value ~> 6 [0110], prior(bit0(0)) = 1, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 6; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* value ~> 7 [0111], prior(bit0(0)) = 0, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 7; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* value ~> 8 [1000], prior(bit0(0)) = 1, prior(bit1(0)) = 1, prior(bit2(0)) = 1 */ + ram[0] = 8; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 1); +} + +void test_condset(void) { + TEST_SUITE_BEGIN(); + + /* hit counts */ + TEST(test_hitcount_increment_when_true); + TEST(test_hitcount_does_not_increment_when_false); + TEST(test_hitcount_target); + + /* two conditions */ + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001=18_0xH0002=52", 1, 1, 1); + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001=18_0xH0002!=52", 0, 1, 0); + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001>18_0xH0002=52", 0, 0, 1); + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001<18_0xH0002>52", 0, 0, 0); + + /* three conditions */ + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002=52_0xL0004=6", 1, 1, 1, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002=52_0xL0004>6", 0, 1, 1, 0); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002<52_0xL0004=6", 0, 1, 0, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002<52_0xL0004>6", 0, 1, 0, 0); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002=52_0xL0004=6", 0, 0, 1, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002=52_0xL0004>6", 0, 0, 1, 0); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002<52_0xL0004=6", 0, 0, 0, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002<52_0xL0004>6", 0, 0, 0, 0); + + /* pauseif */ + TEST(test_pauseif); + TEST(test_pauseif_hitcount_one); + TEST(test_pauseif_hitcount_two); + TEST(test_pauseif_hitcount_with_reset); + TEST(test_pauseif_does_not_increment_hits); + TEST(test_pauseif_delta_updated); + TEST(test_pauseif_indirect_delta_updated); + TEST(test_pauseif_short_circuit); + + /* resetif */ + TEST(test_resetif); + TEST(test_resetif_cond_with_hittarget); + TEST(test_resetif_hitcount); + TEST(test_resetif_hitcount_one); + TEST(test_resetif_hitcount_addhits); + + TEST(test_pauseif_resetif_hitcounts); + + /* resetnextif */ + TEST(test_resetnextif); + TEST(test_resetnextif_non_hitcount_condition); + TEST(test_resetnextif_addhits); + TEST(test_resetnextif_addhits_chain); + TEST(test_resetnextif_addhits_chain_total); + TEST(test_resetnextif_using_andnext); + TEST(test_resetnextif_andnext); + TEST(test_resetnextif_andnext_hitchain); + TEST(test_resetnextif_addaddress); + TEST(test_resetnextif_chain); + TEST(test_resetnextif_chain_andnext); + TEST(test_resetnextif_chain_with_hits); + TEST(test_resetnextif_pause_lock); + + /* addsource/subsource */ + TEST(test_addsource); + TEST(test_addsource_overflow); + TEST(test_subsource); + TEST(test_subsource_legacy_garbage); + TEST(test_subsource_overflow); + TEST(test_addsource_subsource); + TEST(test_addsource_multiply); + TEST(test_subsource_multiply); + TEST(test_addsource_multiply_fraction); + TEST(test_addsource_multiply_address); + TEST(test_addsource_divide); + TEST(test_addsource_divide_address); + TEST(test_addsource_divide_self); + TEST(test_subsource_divide); + TEST(test_addsource_compare_percentage); + TEST(test_addsource_mask); + TEST(test_addsource_xor); + TEST(test_addsource_float_first); + TEST(test_addsource_float_second); + TEST(test_subsource_mask); + TEST(test_subsource_overflow_comparison_equal); + TEST(test_subsource_overflow_comparison_greater); + TEST(test_subsource_overflow_comparison_greater_or_equal); + TEST(test_subsource_overflow_comparison_lesser); + TEST(test_subsource_overflow_comparison_lesser_or_equal); + TEST(test_subsource_float); + + /* addhits/subhits */ + TEST(test_addhits); + TEST(test_addhits_no_target); + TEST(test_addhits_with_addsource); + TEST(test_addhits_multiple); + TEST(test_subhits); + TEST(test_subhits_below_zero); + + /* andnext */ + TEST(test_andnext); + TEST(test_andnext_boundaries); + TEST(test_andnext_resetif); + TEST(test_andnext_pauseif); + TEST(test_andnext_addsource); + TEST(test_andnext_addhits); + TEST(test_andnext_between_addhits); + TEST(test_andnext_with_hits_chain); + TEST(test_andnext_changes_to); + + /* ornext */ + TEST(test_ornext); + TEST(test_andnext_ornext_interaction); + + /* addaddress */ + TEST(test_addaddress_direct_pointer); + TEST(test_addaddress_direct_pointer_delta); + TEST(test_addaddress_direct_pointer_prior); + TEST(test_addaddress_indirect_pointer); + TEST(test_addaddress_indirect_pointer_negative); + TEST(test_addaddress_indirect_pointer_out_of_range); + TEST(test_addaddress_indirect_pointer_multiple); + TEST(test_addaddress_pointer_data_size_differs_from_pointer_size); + TEST(test_addaddress_double_indirection); + TEST(test_addaddress_double_indirection_with_delta); + TEST(test_addaddress_double_indirection_with_delta_incorrect); + TEST(test_addaddress_adjust_both_sides); + TEST(test_addaddress_adjust_both_sides_different_bases); + TEST(test_addaddress_scaled); + TEST(test_addaddress_scaled_negative); + + /* prior */ + TEST(test_prior_sequence); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_consoleinfo.c b/src/rcheevos/test/rcheevos/test_consoleinfo.c new file mode 100644 index 000000000..f083e7c1e --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_consoleinfo.c @@ -0,0 +1,191 @@ +#include "rc_consoles.h" + +#include "../test_framework.h" + +static void test_name(int console_id, const char* expected_name) +{ + ASSERT_STR_EQUALS(rc_console_name(console_id), expected_name); +} + +static void test_memory(int console_id, unsigned expected_total_memory) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(console_id); + unsigned total_memory = 0; + unsigned max_address = 0; + unsigned i; + ASSERT_PTR_NOT_NULL(regions); + + if (expected_total_memory == 0) + { + ASSERT_NUM_EQUALS(regions->num_regions, 0); + return; + } + + ASSERT_NUM_GREATER(regions->num_regions, 0); + for (i = 0; i < regions->num_regions; ++i) { + total_memory += (regions->region[i].end_address - regions->region[i].start_address + 1); + if (regions->region[i].end_address > max_address) + max_address = regions->region[i].end_address; + + ASSERT_PTR_NOT_NULL(regions->region[i].description); + } + + ASSERT_NUM_EQUALS(total_memory, expected_total_memory); + ASSERT_NUM_EQUALS(max_address, expected_total_memory - 1); +} + +void test_consoleinfo(void) { + TEST_SUITE_BEGIN(); + + /* use raw numbers instead of constants to ensure constants don't change */ + TEST_PARAMS2(test_name, 0, "Unknown"); + TEST_PARAMS2(test_name, 1, "Sega Genesis"); + TEST_PARAMS2(test_name, 2, "Nintendo 64"); + TEST_PARAMS2(test_name, 3, "Super Nintendo Entertainment System"); + TEST_PARAMS2(test_name, 4, "GameBoy"); + TEST_PARAMS2(test_name, 5, "GameBoy Advance"); + TEST_PARAMS2(test_name, 6, "GameBoy Color"); + TEST_PARAMS2(test_name, 7, "Nintendo Entertainment System"); + TEST_PARAMS2(test_name, 8, "PC Engine"); + TEST_PARAMS2(test_name, 9, "Sega CD"); + TEST_PARAMS2(test_name, 10, "Sega 32X"); + TEST_PARAMS2(test_name, 11, "Master System"); + TEST_PARAMS2(test_name, 12, "PlayStation"); + TEST_PARAMS2(test_name, 13, "Atari Lynx"); + TEST_PARAMS2(test_name, 14, "Neo Geo Pocket"); + TEST_PARAMS2(test_name, 15, "Game Gear"); + TEST_PARAMS2(test_name, 16, "GameCube"); + TEST_PARAMS2(test_name, 17, "Atari Jaguar"); + TEST_PARAMS2(test_name, 18, "Nintendo DS"); + TEST_PARAMS2(test_name, 19, "Wii"); + TEST_PARAMS2(test_name, 20, "Wii-U"); + TEST_PARAMS2(test_name, 21, "PlayStation 2"); + TEST_PARAMS2(test_name, 22, "XBOX"); + TEST_PARAMS2(test_name, 23, "Magnavox Odyssey 2"); + TEST_PARAMS2(test_name, 24, "Pokemon Mini"); + TEST_PARAMS2(test_name, 25, "Atari 2600"); + TEST_PARAMS2(test_name, 26, "MS-DOS"); + TEST_PARAMS2(test_name, 27, "Arcade"); + TEST_PARAMS2(test_name, 28, "Virtual Boy"); + TEST_PARAMS2(test_name, 29, "MSX"); + TEST_PARAMS2(test_name, 30, "Commodore 64"); + TEST_PARAMS2(test_name, 31, "ZX-81"); + TEST_PARAMS2(test_name, 32, "Oric"); + TEST_PARAMS2(test_name, 33, "SG-1000"); + TEST_PARAMS2(test_name, 34, "VIC-20"); + TEST_PARAMS2(test_name, 35, "Amiga"); + TEST_PARAMS2(test_name, 36, "Atari ST"); + TEST_PARAMS2(test_name, 37, "Amstrad CPC"); + TEST_PARAMS2(test_name, 38, "Apple II"); + TEST_PARAMS2(test_name, 39, "Sega Saturn"); + TEST_PARAMS2(test_name, 40, "Dreamcast"); + TEST_PARAMS2(test_name, 41, "PlayStation Portable"); + TEST_PARAMS2(test_name, 42, "CD-I"); + TEST_PARAMS2(test_name, 43, "3DO"); + TEST_PARAMS2(test_name, 44, "ColecoVision"); + TEST_PARAMS2(test_name, 45, "Intellivision"); + TEST_PARAMS2(test_name, 46, "Vectrex"); + TEST_PARAMS2(test_name, 47, "PC-8000/8800"); + TEST_PARAMS2(test_name, 48, "PC-9800"); + TEST_PARAMS2(test_name, 49, "PC-FX"); + TEST_PARAMS2(test_name, 50, "Atari 5200"); + TEST_PARAMS2(test_name, 51, "Atari 7800"); + TEST_PARAMS2(test_name, 52, "X68K"); + TEST_PARAMS2(test_name, 53, "WonderSwan"); + TEST_PARAMS2(test_name, 54, "CassetteVision"); + TEST_PARAMS2(test_name, 55, "Super CassetteVision"); + TEST_PARAMS2(test_name, 56, "Neo Geo CD"); + TEST_PARAMS2(test_name, 57, "Fairchild Channel F"); + TEST_PARAMS2(test_name, 58, "FM Towns"); + TEST_PARAMS2(test_name, 59, "ZX Spectrum"); + TEST_PARAMS2(test_name, 60, "Game & Watch"); + TEST_PARAMS2(test_name, 61, "Nokia N-Gage"); + TEST_PARAMS2(test_name, 62, "Nintendo 3DS"); + TEST_PARAMS2(test_name, 63, "Watara Supervision"); + TEST_PARAMS2(test_name, 64, "Sharp X1"); + TEST_PARAMS2(test_name, 65, "TIC-80"); + TEST_PARAMS2(test_name, 66, "Thomson TO8"); + TEST_PARAMS2(test_name, 67, "PC-6000"); + TEST_PARAMS2(test_name, 68, "Sega Pico"); + TEST_PARAMS2(test_name, 69, "Mega Duck"); + TEST_PARAMS2(test_name, 70, "Zeebo"); + TEST_PARAMS2(test_name, 71, "Arduboy"); + TEST_PARAMS2(test_name, 72, "WASM-4"); + TEST_PARAMS2(test_name, 73, "Arcadia 2001"); + TEST_PARAMS2(test_name, 74, "Interton VC 4000"); + TEST_PARAMS2(test_name, 75, "Elektor TV Games Computer"); + TEST_PARAMS2(test_name, 76, "PC Engine CD"); + TEST_PARAMS2(test_name, 77, "Atari Jaguar CD"); + TEST_PARAMS2(test_name, 78, "Nintendo DSi"); + TEST_PARAMS2(test_name, 79, "TI-83"); + TEST_PARAMS2(test_name, 80, "Uzebox"); + TEST_PARAMS2(test_name, 81, "Unknown"); + + TEST_PARAMS2(test_name, 100, "Hubs"); + TEST_PARAMS2(test_name, 101, "Events"); + + /* memory maps */ + TEST_PARAMS2(test_memory, RC_CONSOLE_UNKNOWN, 0x000000); + TEST_PARAMS2(test_memory, RC_CONSOLE_3DO, 0x200000); + TEST_PARAMS2(test_memory, RC_CONSOLE_AMIGA, 0x100000); + TEST_PARAMS2(test_memory, RC_CONSOLE_AMSTRAD_PC, 0x090000); + TEST_PARAMS2(test_memory, RC_CONSOLE_APPLE_II, 0x020000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ARCADE, 0x000000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ARCADIA_2001, 0x000300); + TEST_PARAMS2(test_memory, RC_CONSOLE_ARDUBOY, 0x000F00); + TEST_PARAMS2(test_memory, RC_CONSOLE_ATARI_2600, 0x000080); + TEST_PARAMS2(test_memory, RC_CONSOLE_ATARI_7800, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ATARI_JAGUAR, 0x200000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ATARI_JAGUAR_CD, 0x200000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ATARI_LYNX, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_COLECOVISION, 0x000400); + TEST_PARAMS2(test_memory, RC_CONSOLE_COMMODORE_64, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_DREAMCAST, 0x01000000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, 0x001800); + TEST_PARAMS2(test_memory, RC_CONSOLE_FAIRCHILD_CHANNEL_F, 0x010C40); + TEST_PARAMS2(test_memory, RC_CONSOLE_GAMEBOY, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_GAMEBOY_COLOR, 0x016000); + TEST_PARAMS2(test_memory, RC_CONSOLE_GAMEBOY_ADVANCE, 0x048000); + TEST_PARAMS2(test_memory, RC_CONSOLE_GAMECUBE, 0x01800000); + TEST_PARAMS2(test_memory, RC_CONSOLE_GAME_GEAR, 0x002000); + TEST_PARAMS2(test_memory, RC_CONSOLE_INTELLIVISION, 0x040080); + TEST_PARAMS2(test_memory, RC_CONSOLE_INTERTON_VC_4000, 0x000600); + TEST_PARAMS2(test_memory, RC_CONSOLE_MAGNAVOX_ODYSSEY2, 0x000140); + TEST_PARAMS2(test_memory, RC_CONSOLE_MASTER_SYSTEM, 0x002000); + TEST_PARAMS2(test_memory, RC_CONSOLE_MEGA_DRIVE, 0x020000); + TEST_PARAMS2(test_memory, RC_CONSOLE_MEGADUCK, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_MSX, 0x080000); + TEST_PARAMS2(test_memory, RC_CONSOLE_NEOGEO_POCKET, 0x004000); + TEST_PARAMS2(test_memory, RC_CONSOLE_NEO_GEO_CD, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_NINTENDO, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_NINTENDO_64, 0x800000); + TEST_PARAMS2(test_memory, RC_CONSOLE_NINTENDO_DS, 0x0400000); + TEST_PARAMS2(test_memory, RC_CONSOLE_NINTENDO_DSI, 0x1000000); + TEST_PARAMS2(test_memory, RC_CONSOLE_ORIC, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_PC8800, 0x011000); + TEST_PARAMS2(test_memory, RC_CONSOLE_PC_ENGINE, 0x02000); + TEST_PARAMS2(test_memory, RC_CONSOLE_PC_ENGINE_CD, 0x42800); + TEST_PARAMS2(test_memory, RC_CONSOLE_PCFX, 0x210000); + TEST_PARAMS2(test_memory, RC_CONSOLE_PLAYSTATION, 0x200000); + TEST_PARAMS2(test_memory, RC_CONSOLE_PLAYSTATION_2, 0x02004000); + TEST_PARAMS2(test_memory, RC_CONSOLE_PSP, 0x02000000); + TEST_PARAMS2(test_memory, RC_CONSOLE_POKEMON_MINI, 0x002000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SATURN, 0x200000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SEGA_32X, 0x060000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SEGA_CD, 0x090000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SG1000, 0x006000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SUPER_CASSETTEVISION, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SUPER_NINTENDO, 0x040000); + TEST_PARAMS2(test_memory, RC_CONSOLE_SUPERVISION, 0x006000); + TEST_PARAMS2(test_memory, RC_CONSOLE_THOMSONTO8, 0x080000); + TEST_PARAMS2(test_memory, RC_CONSOLE_TI83, 0x08000); + TEST_PARAMS2(test_memory, RC_CONSOLE_TIC80, 0x018000); + TEST_PARAMS2(test_memory, RC_CONSOLE_UZEBOX, 0x001000); + TEST_PARAMS2(test_memory, RC_CONSOLE_WASM4, 0x010000); + TEST_PARAMS2(test_memory, RC_CONSOLE_WII, 0x05800000); + TEST_PARAMS2(test_memory, RC_CONSOLE_WONDERSWAN, 0x090000); + TEST_PARAMS2(test_memory, RC_CONSOLE_VECTREX, 0x000400); + TEST_PARAMS2(test_memory, RC_CONSOLE_VIRTUAL_BOY, 0x020000); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_format.c b/src/rcheevos/test/rcheevos/test_format.c new file mode 100644 index 000000000..d8bbccf40 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_format.c @@ -0,0 +1,68 @@ +#include "rc_internal.h" + +#include "../test_framework.h" + +static void test_format_value(int format, int value, const char* expected) { + char buffer[64]; + int result; + + result = rc_format_value(buffer, sizeof(buffer), value, format); + ASSERT_STR_EQUALS(buffer, expected); + ASSERT_NUM_EQUALS(result, strlen(expected)); +} + +static void test_parse_format(const char* format, int expected) { + ASSERT_NUM_EQUALS(rc_parse_format(format), expected); +} + +void test_format(void) { + TEST_SUITE_BEGIN(); + + /* rc_format_value */ + TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, 12345, "12345"); + TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, -12345, "-12345"); + TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, 0xFFFFFFFF, "-1"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SCORE, 12345, "012345"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 45, "0:45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 345, "5:45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 12345, "3h25:45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 345, "0:03.45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 12345, "2:03.45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 1234567, "3h25:45.67"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 45, "0h00"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 345, "0h05"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 12345, "3h25"); + TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 45, "0h45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 345, "5h45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 12345, "205h45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 345, "0:05.75"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 12345, "3:25.75"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 1234567, "5h42:56.11"); + + /* rc_parse_format */ + TEST_PARAMS2(test_parse_format, "VALUE", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "SECS", RC_FORMAT_SECONDS); + TEST_PARAMS2(test_parse_format, "TIMESECS", RC_FORMAT_SECONDS); + TEST_PARAMS2(test_parse_format, "TIME", RC_FORMAT_FRAMES); + TEST_PARAMS2(test_parse_format, "MINUTES", RC_FORMAT_MINUTES); + TEST_PARAMS2(test_parse_format, "SECS_AS_MINS", RC_FORMAT_SECONDS_AS_MINUTES); + TEST_PARAMS2(test_parse_format, "FRAMES", RC_FORMAT_FRAMES); + TEST_PARAMS2(test_parse_format, "SCORE", RC_FORMAT_SCORE); + TEST_PARAMS2(test_parse_format, "POINTS", RC_FORMAT_SCORE); + TEST_PARAMS2(test_parse_format, "MILLISECS", RC_FORMAT_CENTISECS); + TEST_PARAMS2(test_parse_format, "OTHER", RC_FORMAT_SCORE); + TEST_PARAMS2(test_parse_format, "INVALID", RC_FORMAT_VALUE); + + TEST_PARAMS2(test_parse_format, "FLOAT", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FLOAT0", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FLOAT1", RC_FORMAT_FLOAT1); + TEST_PARAMS2(test_parse_format, "FLOAT2", RC_FORMAT_FLOAT2); + TEST_PARAMS2(test_parse_format, "FLOAT3", RC_FORMAT_FLOAT3); + TEST_PARAMS2(test_parse_format, "FLOAT4", RC_FORMAT_FLOAT4); + TEST_PARAMS2(test_parse_format, "FLOAT5", RC_FORMAT_FLOAT5); + TEST_PARAMS2(test_parse_format, "FLOAT6", RC_FORMAT_FLOAT6); + TEST_PARAMS2(test_parse_format, "FLOAT7", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FLOAT10", RC_FORMAT_VALUE); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_lboard.c b/src/rcheevos/test/rcheevos/test_lboard.c new file mode 100644 index 000000000..8e3f7b266 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_lboard.c @@ -0,0 +1,655 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_parse_lboard(rc_lboard_t** lboard, void* buffer, const char* memaddr) +{ + int size; + unsigned* overflow; + + size = rc_lboard_size(memaddr); + ASSERT_NUM_GREATER(size, 0); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + *lboard = rc_parse_lboard(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(*lboard); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_parse_lboard(lboard, buffer, memaddr) ASSERT_HELPER(_assert_parse_lboard(lboard, buffer, memaddr), "assert_parse_lboard") + +static int evaluate_lboard(rc_lboard_t* lboard, memory_t* memory, int* value) { + return rc_evaluate_lboard(lboard, value, peek, memory, NULL); +} + +static void test_simple_leaderboard() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02"); + ASSERT_NUM_EQUALS(lboard->state, RC_LBOARD_STATE_WAITING); + + /* submit is true, but leaderboard has not started */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); /* value is only calculated in STARTED and TRIGGERED states */ + + /* cancel is true - still not started */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start is true - will activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x34); + + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x34); + + /* cancel is true - will deactivate */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* submit is true, but leaderboard is not active */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start is true - will activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x34); + + /* submit is true - will submit */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x34); +} + +static void test_start_and_cancel_same_frame() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=18::SUB:0xH00=3::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start and cancel are both true - should not start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* cancel no longer true - should start */ + ram[1] = 0x13; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* start and cancel are both true - should cancel */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + + /* cancel no longer true, but start still is - shouldn't activate */ + ram[1] = 0x13; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_INACTIVE); + + /* start no longer true - can activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* start true - should start */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_and_submit_same_frame() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start and submit are both true - should trigger */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x34); + + /* disable submit - leaderboard should not start */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_INACTIVE); + + /* disable start - leaderboard can activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* enable start - leaderboard should start */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_and_conditions() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0_0xH01=0::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* only first start condition true - should not start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* only second start condition true - should not start */ + ram[0] = 1; + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* both conditions true - should start */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_or_conditions() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:S0xH00=1S0xH01=1::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* neither start condition true - should not start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* only second start condition true - should start */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* reset lboard state */ + ram[1] = 0; + lboard->state = RC_LBOARD_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* only first start condition true - should start */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_resets_value() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH01=0::CAN:0xH01=10::SUB:0xH01=18::VAL:M:0xH02!=d0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* not started */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start condition true - should start */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* tally a couple hits */ + ram[2]++; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + ram[2]++; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 2); + + /* canceled, hitcount kept, but ignored */ + ram[1] = 10; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* must wait one frame to switch back to active */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted, hitcount should be reset */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* tally a hit */ + ram[2]++; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); +} + +static void test_cancel_or_conditions() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:S0xH01=12S0xH02=12::SUB:0xH00=3::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start condition true - should start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* second cancel condition true - should cancel */ + ram[2] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + + /* reset lboard state */ + ram[2] = 0; + lboard->state = RC_LBOARD_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* first cancel condition true - should cancel */ + ram[1] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); +} + +static void test_submit_and_conditions() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:0xH01=18_0xH03=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* only first submit condition is true - should not submit */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* only second submit condition true - should not submit */ + ram[1] = 0; + ram[3] = 18; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* both conditions true - should submit */ + ram[1] = 18; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); +} + +static void test_submit_or_conditions() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:S0xH01=12S0xH03=12::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* neither start condition true - should not submit */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* only second submit condition true - should submit */ + ram[1] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + + /* reset lboard state */ + ram[1] = 0; + lboard->state = RC_LBOARD_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* only first submit condition true - should submit */ + ram[3] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); +} + +static void test_progress() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start true - should start - value from PRO */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x56); + + /* submit true - should trigger - value from VAL */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x34); +} + +static void test_value_from_hitcount() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02!=d0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* not started, value should not be tallied */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + ram[2] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* started, value will not be tallied as it hasn't changed */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* value changed, expect tally */ + ram[2] = 11; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + /* not changed, no tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + /* changed, tally */ + ram[2] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 2); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ram[2] = 13; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted, tally should be reset */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* value changed, expect tally */ + ram[2] = 11; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); +} + +static void test_value_from_addhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:C:0xH03=1_M:0xH02=1"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, nothing to tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* second value tallied*/ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + /* both values tallied */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 3); + + /* only first value tallied */ + ram[2] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 4); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted, tally should be reset, but first is still true, so it'll be tallied */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); +} + +static void test_value_from_float() { + /* bytes 5-8 are the float value for pi */ + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:fF0005"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, nothing to tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 3); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 3); +} + +static void test_value_from_float_scaled() { + /* bytes 5-8 are the float value for pi */ + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:fF0005*100"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, nothing to tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 314); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 314); +} + +static void test_maximum_value_from_conditions() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:Q:0xH01=1_M:0x 02$Q:0xH01=2_M:0x 03"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, neither value is active */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* first value is active */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0xAB34); + + /* second value is active */ + ram[1] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x56AB); + + /* value updated */ + ram[3] = 0x12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x5612); + + /* neither value is active */ + ram[1] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); +} + +static void test_measured_value_and_condition() +{ + rc_lboard_t* lboard; + char buffer[1024]; + + /* a Measured is irrelevant in the STA/CAN/SUB conditions, but if present, allow them to be unique */ + assert_parse_lboard(&lboard, buffer, "STA:M:0xH00=0::CAN:M:0xH00=2::SUB:M:0xH00=3::VAL:M:0xH04"); +} + +static void test_unparsable_lboard(const char* memaddr, int expected_error) { + ASSERT_NUM_EQUALS(rc_lboard_size(memaddr), expected_error); +} + +static void test_unparsable_strings() { + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::GARBAGE", RC_INVALID_LBOARD_FIELD); + TEST_PARAMS2(test_unparsable_lboard, "CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02", RC_MISSING_START); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::SUB:0xH00=3::PRO:0xH04::VAL:0xH02", RC_MISSING_CANCEL); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::PRO:0xH04::VAL:0xH02", RC_MISSING_SUBMIT); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04", RC_MISSING_VALUE); + TEST_PARAMS2(test_unparsable_lboard, "STA:::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_MISSING_START); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:::SUB:0xH00=3::VAL:0xH02", RC_MISSING_CANCEL); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:::VAL:0xH02", RC_MISSING_SUBMIT); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:", RC_MISSING_VALUE); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::STA:0=0", RC_DUPLICATED_START); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::CAN:0=0", RC_DUPLICATED_CANCEL); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::SUB:0=0", RC_DUPLICATED_SUBMIT); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::VAL:0", RC_DUPLICATED_VALUE); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::PRO:0", RC_DUPLICATED_PROGRESS); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH01=1_M:0xH01=2", RC_MULTIPLE_MEASURED); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH01=1_T:0xH01=2", RC_INVALID_VALUE_FLAG); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1_0xH01=2", RC_INVALID_VALUE_FLAG); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1", RC_MISSING_VALUE_MEASURED); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1$M:0xH03", RC_MISSING_VALUE_MEASURED); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02SM:0xH03", RC_INVALID_VALUE_FLAG); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=A::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_MEMORY_OPERAND); + + /* "STA:0xH00=1" is valid, but that leaves the read pointer pointing at the "A", which is not "::", so a generic + * invalid field error is returned. */ + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1A::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_LBOARD_FIELD); + + /* Missing '_' causes "0xH00=10" to be valid, but that leaves the read pointer pointing at the "x", which is not + * "::", so a generic invalid field error is returned. */ + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=10xH01=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_LBOARD_FIELD); + + /* Garbage following value field (legacy format conversion will return invalid comparison) */ + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02=1=2", RC_INVALID_COMPARISON); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02=1=2", RC_INVALID_LBOARD_FIELD); +} + +void test_lboard(void) { + TEST_SUITE_BEGIN(); + + TEST(test_simple_leaderboard); + TEST(test_start_and_cancel_same_frame); + TEST(test_start_and_submit_same_frame); + + TEST(test_start_and_conditions); + TEST(test_start_or_conditions); + + TEST(test_start_resets_value); + + TEST(test_cancel_or_conditions); + + TEST(test_submit_and_conditions); + TEST(test_submit_or_conditions); + + TEST(test_progress); + + TEST(test_value_from_hitcount); + TEST(test_value_from_addhits); + TEST(test_value_from_float); + TEST(test_value_from_float_scaled); + TEST(test_maximum_value_from_conditions); + TEST(test_measured_value_and_condition); + + test_unparsable_strings(); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_memref.c b/src/rcheevos/test/rcheevos/test_memref.c new file mode 100644 index 000000000..c2270e01e --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_memref.c @@ -0,0 +1,407 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +#include + +static void test_mask(char size, unsigned expected) +{ + ASSERT_NUM_EQUALS(rc_memref_mask(size), expected); +} + +static void test_shared_masks(void) +{ + TEST_PARAMS2(test_mask, RC_MEMSIZE_8_BITS, 0x000000ff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_0, 0x00000001); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_1, 0x00000002); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_2, 0x00000004); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_3, 0x00000008); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_4, 0x00000010); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_5, 0x00000020); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_6, 0x00000040); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_7, 0x00000080); + TEST_PARAMS2(test_mask, RC_MEMSIZE_LOW, 0x0000000f); + TEST_PARAMS2(test_mask, RC_MEMSIZE_HIGH, 0x000000f0); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BITCOUNT, 0x000000ff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_16_BITS, 0x0000ffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_16_BITS_BE, 0x0000ffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_24_BITS, 0x00ffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_24_BITS_BE, 0x00ffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_32_BITS, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_32_BITS_BE, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_FLOAT, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_FLOAT_BE, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_MBF32, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_VARIABLE, 0xffffffff); +} + +static void test_shared_size(char size, char expected) +{ + ASSERT_NUM_EQUALS(rc_memref_shared_size(size), expected); +} + +static void test_shared_sizes(void) +{ + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_8_BITS, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_0, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_1, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_2, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_3, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_4, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_5, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_6, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_7, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_LOW, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_HIGH, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BITCOUNT, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_16_BITS, RC_MEMSIZE_16_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_16_BITS_BE, RC_MEMSIZE_16_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_24_BITS, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_24_BITS_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_32_BITS, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_32_BITS_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_FLOAT, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_FLOAT_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_MBF32, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_MBF32_LE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_VARIABLE, RC_MEMSIZE_32_BITS); +} + +static void test_transform(unsigned value, char size, unsigned expected) +{ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + ASSERT_NUM_EQUALS(typed_value.value.u32, expected); +} + +static void test_transform_float(unsigned value, char size, double expected) +{ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + ASSERT_FLOAT_EQUALS(typed_value.value.f32, expected); +} + +static void test_transform_float_inf(unsigned value, char size) +{ + /* C89 does not provide defines for NAN and INFINITY, nor does it provide isnan() or isinf() functions */ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + + if (typed_value.value.f32 < FLT_MAX) { + /* infinity will be greater than max float value */ + ASSERT_FAIL("result of transform is not infinity") + } +} + +static void test_transform_float_nan(unsigned value, char size) +{ + /* C89 does not provide defines for NAN and INFINITY, nor does it provide isnan() or isinf() functions */ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + + if (typed_value.value.f32 == typed_value.value.f32) { + /* NaN cannot be compared, will fail equality check with itself */ + ASSERT_FAIL("result of transform is not NaN") + } +} + +static void test_transforms(void) +{ + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_8_BITS, 0x00000078); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_16_BITS, 0x00005678); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_24_BITS, 0x00345678); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_32_BITS, 0x12345678); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_LOW, 0x00000008); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_HIGH, 0x00000007); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_BITCOUNT, 0x00000004); /* only counts bits in lowest byte */ + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_16_BITS_BE, 0x00007856); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_24_BITS_BE, 0x00785634); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_32_BITS_BE, 0x78563412); + + TEST_PARAMS3(test_transform, 0x00000001, RC_MEMSIZE_BIT_0, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000002, RC_MEMSIZE_BIT_1, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000004, RC_MEMSIZE_BIT_2, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000008, RC_MEMSIZE_BIT_3, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000010, RC_MEMSIZE_BIT_4, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000020, RC_MEMSIZE_BIT_5, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000040, RC_MEMSIZE_BIT_6, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000080, RC_MEMSIZE_BIT_7, 0x00000001); + + TEST_PARAMS3(test_transform, 0x000000FE, RC_MEMSIZE_BIT_0, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000FD, RC_MEMSIZE_BIT_1, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000FB, RC_MEMSIZE_BIT_2, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000F7, RC_MEMSIZE_BIT_3, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000EF, RC_MEMSIZE_BIT_4, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000DF, RC_MEMSIZE_BIT_5, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000BF, RC_MEMSIZE_BIT_6, 0x00000000); + TEST_PARAMS3(test_transform, 0x0000007F, RC_MEMSIZE_BIT_7, 0x00000000); + + TEST_PARAMS3(test_transform_float, 0x3F800000, RC_MEMSIZE_FLOAT, 1.0); + TEST_PARAMS3(test_transform_float, 0x41460000, RC_MEMSIZE_FLOAT, 12.375); + TEST_PARAMS3(test_transform_float, 0x42883EFA, RC_MEMSIZE_FLOAT, 68.123); + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_FLOAT, 0.0); + TEST_PARAMS3(test_transform_float, 0x80000000, RC_MEMSIZE_FLOAT, -0.0); + TEST_PARAMS3(test_transform_float, 0xC0000000, RC_MEMSIZE_FLOAT, -2.0); + TEST_PARAMS3(test_transform_float, 0x40490FDB, RC_MEMSIZE_FLOAT, 3.14159274101257324); + TEST_PARAMS3(test_transform_float, 0x3EAAAAAB, RC_MEMSIZE_FLOAT, 0.333333334326744076); + TEST_PARAMS3(test_transform_float, 0x429A4492, RC_MEMSIZE_FLOAT, 77.133926); + TEST_PARAMS3(test_transform_float, 0x4350370A, RC_MEMSIZE_FLOAT, 208.214996); + TEST_PARAMS3(test_transform_float, 0x45AE36E9, RC_MEMSIZE_FLOAT, 5574.863770); + TEST_PARAMS3(test_transform_float, 0x58635FA9, RC_MEMSIZE_FLOAT, 1000000000000000.0); + TEST_PARAMS3(test_transform_float, 0x24E69595, RC_MEMSIZE_FLOAT, 0.0000000000000001); + TEST_PARAMS3(test_transform_float, 0x000042B4, RC_MEMSIZE_FLOAT, 2.39286e-41); + TEST_PARAMS2(test_transform_float_inf, 0x7F800000, RC_MEMSIZE_FLOAT); + TEST_PARAMS2(test_transform_float_nan, 0x7FFFFFFF, RC_MEMSIZE_FLOAT); + + TEST_PARAMS3(test_transform_float, 0x0000803F, RC_MEMSIZE_FLOAT_BE, 1.0); + TEST_PARAMS3(test_transform_float, 0x00004641, RC_MEMSIZE_FLOAT_BE, 12.375); + TEST_PARAMS3(test_transform_float, 0xFA3E8842, RC_MEMSIZE_FLOAT_BE, 68.123); + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_FLOAT_BE, 0.0); + TEST_PARAMS3(test_transform_float, 0x00000080, RC_MEMSIZE_FLOAT_BE, -0.0); + TEST_PARAMS3(test_transform_float, 0x000000C0, RC_MEMSIZE_FLOAT_BE, -2.0); + TEST_PARAMS3(test_transform_float, 0xDB0F4940, RC_MEMSIZE_FLOAT_BE, 3.14159274101257324); + TEST_PARAMS3(test_transform_float, 0xABAAAA3E, RC_MEMSIZE_FLOAT_BE, 0.333333334326744076); + TEST_PARAMS3(test_transform_float, 0x92449A42, RC_MEMSIZE_FLOAT_BE, 77.133926); + TEST_PARAMS3(test_transform_float, 0x0A375043, RC_MEMSIZE_FLOAT_BE, 208.214996); + TEST_PARAMS3(test_transform_float, 0xE936AE45, RC_MEMSIZE_FLOAT_BE, 5574.863770); + TEST_PARAMS3(test_transform_float, 0xA95F6358, RC_MEMSIZE_FLOAT_BE, 1000000000000000.0); + TEST_PARAMS3(test_transform_float, 0x9595E624, RC_MEMSIZE_FLOAT_BE, 0.0000000000000001); + TEST_PARAMS3(test_transform_float, 0xB4420000, RC_MEMSIZE_FLOAT_BE, 2.39286e-41); + TEST_PARAMS2(test_transform_float_inf, 0x0000807F, RC_MEMSIZE_FLOAT_BE); + TEST_PARAMS2(test_transform_float_nan, 0xFFFFFF7F, RC_MEMSIZE_FLOAT_BE); + + /* MBF values are stored big endian (at least on Apple II), so will be byteswapped + * when passed to rc_transform_memref_value. MBF doesn't support infinity or NaN. */ + TEST_PARAMS3(test_transform_float, 0x00000081, RC_MEMSIZE_MBF32, 1.0); /* 81 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00002084, RC_MEMSIZE_MBF32, 10.0); /* 84 20 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00004687, RC_MEMSIZE_MBF32, 99.0); /* 87 46 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_MBF32, 0.0); /* 00 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00000080, RC_MEMSIZE_MBF32, 0.5); /* 80 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00008082, RC_MEMSIZE_MBF32, -2.0); /* 82 80 00 00 */ + TEST_PARAMS3(test_transform_float, 0xF3043581, RC_MEMSIZE_MBF32, 1.41421354); /* 81 34 04 F3 */ + TEST_PARAMS3(test_transform_float, 0xDA0F4982, RC_MEMSIZE_MBF32, 3.14159256); /* 82 49 0F DA */ + TEST_PARAMS3(test_transform_float, 0xDB0F4983, RC_MEMSIZE_MBF32, 6.28318548); /* 83 49 0F DB */ + + /* Some flavors of BASIC (notably Locomotive BASIC on the Amstrad CPC) use the native endian-ness of + * the system for their MBF values, so we support both MBF32 (big endian) and MBF32_LE (little endian). + * Also note that Amstrad BASIC and Apple II BASIC both use MBF40, but since MBF40 just adds 8 extra bits + * of significance as the end of the MBF32 value, we can discard those as we convert to a 32-bit float. */ + TEST_PARAMS3(test_transform_float, 0x81000000, RC_MEMSIZE_MBF32_LE, 1.0); /* 00 00 00 81 */ + TEST_PARAMS3(test_transform_float, 0x84200000, RC_MEMSIZE_MBF32_LE, 10.0); /* 00 00 20 84 */ + TEST_PARAMS3(test_transform_float, 0x87460000, RC_MEMSIZE_MBF32_LE, 99.0); /* 00 00 46 87 */ + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_MBF32_LE, 0.0); /* 00 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x80000000, RC_MEMSIZE_MBF32_LE, 0.5); /* 00 00 00 80 */ + TEST_PARAMS3(test_transform_float, 0x82800000, RC_MEMSIZE_MBF32_LE, -2.0); /* 00 00 80 82 */ + TEST_PARAMS3(test_transform_float, 0x813504F3, RC_MEMSIZE_MBF32_LE, 1.41421354); /* F3 04 34 81 */ + TEST_PARAMS3(test_transform_float, 0x82490FDA, RC_MEMSIZE_MBF32_LE, 3.14159256); /* DA 0F 49 82 */ + TEST_PARAMS3(test_transform_float, 0x83490FDB, RC_MEMSIZE_MBF32_LE, 6.28318548); /* DB 0F 49 83 */ +} + +static int get_memref_count(rc_parse_state_t* parse) { + int count = 0; + rc_memref_t *memref = *parse->first_memref; + while (memref) { + ++count; + memref = memref->next; + } + + return count; +} + +static void test_allocate_shared_address() { + rc_parse_state_t parse; + rc_memref_t* memrefs; + rc_init_parse_state(&parse, NULL, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 1); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS, 0); /* differing size will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 2); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW, 0); /* differing size will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 3); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2, 0); /* differing size will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 4); + + rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS, 0); /* differing address will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS, 0); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS, 0); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2, 0); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS, 0); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_destroy_parse_state(&parse); +} + +static void test_allocate_shared_address2() { + rc_parse_state_t parse; + rc_memref_t* memrefs; + rc_memref_t* memref1; + rc_memref_t* memref2; + rc_memref_t* memref3; + rc_memref_t* memref4; + rc_memref_t* memref5; + rc_memref_t* memrefX; + rc_init_parse_state(&parse, NULL, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + + memref1 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(memref1->address, 1); + ASSERT_NUM_EQUALS(memref1->value.size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(memref1->value.is_indirect, 0); + ASSERT_NUM_EQUALS(memref1->value.value, 0); + ASSERT_NUM_EQUALS(memref1->value.changed, 0); + ASSERT_NUM_EQUALS(memref1->value.prior, 0); + ASSERT_PTR_EQUALS(memref1->next, 0); + + memref2 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS, 0); /* differing size will not match */ + memref3 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW, 0); /* differing size will not match */ + memref4 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2, 0); /* differing size will not match */ + memref5 = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS, 0); /* differing address will not match */ + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS, 0); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref1); + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS, 0); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref2); + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW, 0); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref3); + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2, 0); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref4); + + memrefX = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS, 0); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref5); + + rc_destroy_parse_state(&parse); +} + +static void test_sizing_mode_grow_buffer() { + int i; + rc_parse_state_t parse; + rc_memref_t* memrefs; + rc_init_parse_state(&parse, NULL, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + + /* memrefs are allocated 16 at a time */ + for (i = 0; i < 100; i++) { + rc_alloc_memref(&parse, i, RC_MEMSIZE_8_BITS, 0); + } + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + /* 100 have been allocated, make sure we can still access items at various addresses without allocating more */ + rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 25, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 50, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 75, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 99, RC_MEMSIZE_8_BITS, 0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_destroy_parse_state(&parse); +} + +static void test_update_memref_values() { + rc_parse_state_t parse; + rc_memref_t* memrefs; + rc_memref_t* memref1; + rc_memref_t* memref2; + + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + memory.ram = ram; + memory.size = sizeof(ram); + + rc_init_parse_state(&parse, NULL, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + + memref1 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS, 0); + memref2 = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS, 0); + + rc_update_memref_values(memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 0x12); + ASSERT_NUM_EQUALS(memref1->value.changed, 1); + ASSERT_NUM_EQUALS(memref1->value.prior, 0); + ASSERT_NUM_EQUALS(memref2->value.value, 0x34); + ASSERT_NUM_EQUALS(memref2->value.changed, 1); + ASSERT_NUM_EQUALS(memref2->value.prior, 0); + + ram[1] = 3; + rc_update_memref_values(memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 3); + ASSERT_NUM_EQUALS(memref1->value.changed, 1); + ASSERT_NUM_EQUALS(memref1->value.prior, 0x12); + ASSERT_NUM_EQUALS(memref2->value.value, 0x34); + ASSERT_NUM_EQUALS(memref2->value.changed, 0); + ASSERT_NUM_EQUALS(memref2->value.prior, 0); + + ram[1] = 5; + rc_update_memref_values(memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 5); + ASSERT_NUM_EQUALS(memref1->value.changed, 1); + ASSERT_NUM_EQUALS(memref1->value.prior, 3); + ASSERT_NUM_EQUALS(memref2->value.value, 0x34); + ASSERT_NUM_EQUALS(memref2->value.changed, 0); + ASSERT_NUM_EQUALS(memref2->value.prior, 0); + + ram[2] = 7; + rc_update_memref_values(memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 5); + ASSERT_NUM_EQUALS(memref1->value.changed, 0); + ASSERT_NUM_EQUALS(memref1->value.prior, 3); + ASSERT_NUM_EQUALS(memref2->value.value, 7); + ASSERT_NUM_EQUALS(memref2->value.changed, 1); + ASSERT_NUM_EQUALS(memref2->value.prior, 0x34); + + rc_destroy_parse_state(&parse); +} + +void test_memref(void) { + TEST_SUITE_BEGIN(); + + test_shared_masks(); + test_shared_sizes(); + test_transforms(); + + TEST(test_allocate_shared_address); + TEST(test_allocate_shared_address2); + + TEST(test_sizing_mode_grow_buffer); + TEST(test_update_memref_values); + + /* rc_parse_memref is thoroughly tested by rc_parse_operand tests */ + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_operand.c b/src/rcheevos/test/rcheevos/test_operand.c new file mode 100644 index 000000000..612113bd6 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_operand.c @@ -0,0 +1,689 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_parse_operand(rc_operand_t* self, char* buffer, const char** memaddr) { + rc_parse_state_t parse; + rc_memref_t* memrefs; + int ret; + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + ret = rc_parse_operand(self, memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER_EQUALS(ret, 0); + ASSERT_NUM_EQUALS(**memaddr, 0); +} +#define assert_parse_operand(operand, buffer, memaddr_out) ASSERT_HELPER(_assert_parse_operand(operand, buffer, memaddr_out), "assert_parse_operand") + +static void _assert_operand(rc_operand_t* self, char expected_type, char expected_size, unsigned expected_address) { + ASSERT_NUM_EQUALS(expected_type, self->type); + switch (expected_type) { + case RC_OPERAND_ADDRESS: + case RC_OPERAND_DELTA: + case RC_OPERAND_PRIOR: + ASSERT_NUM_EQUALS(expected_size, self->size); + ASSERT_UNUM_EQUALS(expected_address, self->value.memref->address); + break; + + case RC_OPERAND_CONST: + ASSERT_UNUM_EQUALS(expected_address, self->value.num); + break; + } +} +#define assert_operand(operand, expected_type, expected_size, expected_address) ASSERT_HELPER(_assert_operand(operand, expected_type, expected_size, expected_address), "assert_operand") + +static void test_parse_operand(const char* memaddr, char expected_type, char expected_size, unsigned expected_value) { + char buffer[256]; + rc_operand_t self; + assert_parse_operand(&self, buffer, &memaddr); + assert_operand(&self, expected_type, expected_size, expected_value); +} + +static void test_parse_operand_fp(const char* memaddr, char expected_type, double expected_value) { + char buffer[256]; + rc_operand_t self; + assert_parse_operand(&self, buffer, &memaddr); + + ASSERT_NUM_EQUALS(expected_type, self.type); + switch (expected_type) { + case RC_OPERAND_CONST: + ASSERT_DBL_EQUALS(expected_value, self.value.num); + break; + case RC_OPERAND_FP: + ASSERT_DBL_EQUALS(expected_value, self.value.dbl); + break; + } +} + +static void test_parse_error_operand(const char* memaddr, int valid_chars, int expected_error) { + rc_operand_t self; + rc_parse_state_t parse; + int ret; + const char* begin = memaddr; + rc_memref_t* memrefs; + + rc_init_parse_state(&parse, 0, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + ret = rc_parse_operand(&self, &memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_EQUALS(expected_error, ret); + ASSERT_NUM_EQUALS(memaddr - begin, valid_chars); +} + +static unsigned evaluate_operand(rc_operand_t* op, memory_t* memory, rc_memref_t* memrefs) +{ + rc_eval_state_t eval_state; + rc_typed_value_t value; + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + rc_update_memref_values(memrefs, peek, memory); + rc_evaluate_operand(&value, op, &eval_state); + return value.value.u32; +} + +static void test_evaluate_operand(const char* memaddr, memory_t* memory, unsigned expected_value) { + rc_operand_t self; + rc_parse_state_t parse; + rc_memref_t* memrefs; + char buffer[512]; + unsigned value; + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&self, &memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + value = evaluate_operand(&self, memory, memrefs); + ASSERT_NUM_EQUALS(value, expected_value); +} + +static float evaluate_operand_float(rc_operand_t* op, memory_t* memory, rc_memref_t* memrefs) { + rc_eval_state_t eval_state; + rc_typed_value_t value; + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + rc_update_memref_values(memrefs, peek, memory); + rc_evaluate_operand(&value, op, &eval_state); + return value.value.f32; +} + +static void test_evaluate_operand_float(const char* memaddr, memory_t* memory, double expected_value) { + rc_operand_t self; + rc_parse_state_t parse; + rc_memref_t* memrefs; + char buffer[512]; + float value; + + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&self, &memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + value = evaluate_operand_float(&self, memory, memrefs); + ASSERT_FLOAT_EQUALS(value, expected_value); +} +static void test_parse_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "0xH1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xH1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0x 1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0x1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xW1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xX1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xL1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xU1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xM1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xN1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xO1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xP1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xQ1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xR1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xS1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xT1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xK1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xI1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xJ1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xG1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fF1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fB1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fM1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fL1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* sizes (ignore case) */ + TEST_PARAMS4(test_parse_operand, "0Xh1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xx1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xl1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xu1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xm1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xn1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xo1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xp1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xq1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xr1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xs1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xt1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xk1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xi1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xj1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xg1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "ff1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fb1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fm1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fl1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* addresses */ + TEST_PARAMS4(test_parse_operand, "0xH0000", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x0000U); + TEST_PARAMS4(test_parse_operand, "0xH12345678", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x12345678U); + TEST_PARAMS4(test_parse_operand, "0xHABCD", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "0xhabcd", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "fFABCD", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0xABCDU); +} + +static void test_parse_delta_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "d0xH1234", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0x 1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0x1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xW1234", RC_OPERAND_DELTA, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xX1234", RC_OPERAND_DELTA, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xL1234", RC_OPERAND_DELTA, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xU1234", RC_OPERAND_DELTA, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xM1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xN1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xO1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xP1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xQ1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xR1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xS1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xT1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xK1234", RC_OPERAND_DELTA, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xI1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xJ1234", RC_OPERAND_DELTA, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xG1234", RC_OPERAND_DELTA, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfF1234", RC_OPERAND_DELTA, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfB1234", RC_OPERAND_DELTA, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfM1234", RC_OPERAND_DELTA, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfL1234", RC_OPERAND_DELTA, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* ignores case */ + TEST_PARAMS4(test_parse_operand, "D0Xh1234", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x1234U); + + /* addresses */ + TEST_PARAMS4(test_parse_operand, "d0xH0000", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x0000U); + TEST_PARAMS4(test_parse_operand, "d0xH12345678", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x12345678U); + TEST_PARAMS4(test_parse_operand, "d0xHABCD", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "d0xhabcd", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0xABCDU); +} + +static void test_parse_prior_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "p0xH1234", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0x 1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0x1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xW1234", RC_OPERAND_PRIOR, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xX1234", RC_OPERAND_PRIOR, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xL1234", RC_OPERAND_PRIOR, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xU1234", RC_OPERAND_PRIOR, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xM1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xN1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xO1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xP1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xQ1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xR1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xS1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xT1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xK1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xI1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xJ1234", RC_OPERAND_PRIOR, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xG1234", RC_OPERAND_PRIOR, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfF1234", RC_OPERAND_PRIOR, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfB1234", RC_OPERAND_PRIOR, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfM1234", RC_OPERAND_PRIOR, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfL1234", RC_OPERAND_PRIOR, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* ignores case */ + TEST_PARAMS4(test_parse_operand, "P0Xh1234", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x1234U); + + /* addresses */ + TEST_PARAMS4(test_parse_operand, "p0xH0000", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x0000U); + TEST_PARAMS4(test_parse_operand, "p0xH12345678", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x12345678U); + TEST_PARAMS4(test_parse_operand, "p0xHABCD", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "p0xhabcd", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0xABCDU); +} + +static void test_parse_bcd_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "b0xH1234", RC_OPERAND_BCD, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0x 1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0x1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xW1234", RC_OPERAND_BCD, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xX1234", RC_OPERAND_BCD, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xI1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xJ1234", RC_OPERAND_BCD, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xG1234", RC_OPERAND_BCD, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfF1234", RC_OPERAND_BCD, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfB1234", RC_OPERAND_BCD, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfM1234", RC_OPERAND_BCD, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfL1234", RC_OPERAND_BCD, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* sizes less than 8-bit technically don't need a BCD conversion */ + TEST_PARAMS4(test_parse_operand, "b0xL1234", RC_OPERAND_BCD, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xU1234", RC_OPERAND_BCD, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xM1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xN1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xO1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xP1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xQ1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xR1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xS1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xT1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_7, 0x1234U); +} + +static void test_parse_inverted_memory_references() { + TEST_PARAMS4(test_parse_operand, "~0xH1234", RC_OPERAND_INVERTED, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0x 1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0x1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xW1234", RC_OPERAND_INVERTED, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xX1234", RC_OPERAND_INVERTED, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xI1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xJ1234", RC_OPERAND_INVERTED, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xG1234", RC_OPERAND_INVERTED, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fF1234", RC_OPERAND_INVERTED, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fB1234", RC_OPERAND_INVERTED, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fM1234", RC_OPERAND_INVERTED, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fL1234", RC_OPERAND_INVERTED, RC_MEMSIZE_MBF32_LE, 0x1234U); + + TEST_PARAMS4(test_parse_operand, "~0xL1234", RC_OPERAND_INVERTED, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xU1234", RC_OPERAND_INVERTED, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xM1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xN1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xO1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xP1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xQ1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xR1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xS1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xT1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_7, 0x1234U); +} + +static void test_parse_unsigned_values() { + /* unsigned integers - no prefix */ + /* values don't actually have size, default is RC_MEMSIZE_8_BITS */ + TEST_PARAMS4(test_parse_operand, "123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123U); + TEST_PARAMS4(test_parse_operand, "123456", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123456U); + TEST_PARAMS4(test_parse_operand, "0", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U); + TEST_PARAMS4(test_parse_operand, "0000000000", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U); + TEST_PARAMS4(test_parse_operand, "0123456", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123456U); + TEST_PARAMS4(test_parse_operand, "4294967295", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); + + /* more than 32-bits (error), will be constrained to 32-bits */ + TEST_PARAMS4(test_parse_operand, "4294967296", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); +} + +static void test_parse_signed_values() { + /* signed integers - 'V' prefix */ + TEST_PARAMS4(test_parse_operand, "v100", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 100); + TEST_PARAMS4(test_parse_operand, "V100", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 100); + TEST_PARAMS4(test_parse_operand, "V+1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1); + TEST_PARAMS4(test_parse_operand, "V-1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xFFFFFFFFU); + TEST_PARAMS4(test_parse_operand, "V-2", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xFFFFFFFEU); + TEST_PARAMS4(test_parse_operand, "V9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x7FFFFFFFU); + TEST_PARAMS4(test_parse_operand, "V-9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x80000001U); + + /* no prefix, sign */ + TEST_PARAMS4(test_parse_operand, "-1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); + TEST_PARAMS4(test_parse_operand, "+1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1); + TEST_PARAMS4(test_parse_operand, "+9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x7FFFFFFFU); + TEST_PARAMS4(test_parse_operand, "-9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x80000001U); + + /* prefix, no value */ + TEST_PARAMS3(test_parse_error_operand, "v", 0, RC_INVALID_CONST_OPERAND); + + /* signed integer prefix, hex value */ + TEST_PARAMS3(test_parse_error_operand, "vabcd", 0, RC_INVALID_CONST_OPERAND); +} + +static void test_parse_hex_values() { + /* hex - 'H' prefix, not '0x'! */ + TEST_PARAMS4(test_parse_operand, "H123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x123U); + TEST_PARAMS4(test_parse_operand, "HABCD", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "h123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x123U); + TEST_PARAMS4(test_parse_operand, "habcd", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "HFFFFFFFF", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); + + /* hex without prefix */ + TEST_PARAMS3(test_parse_error_operand, "ABCD", 0, RC_INVALID_MEMORY_OPERAND); + + /* '0x' is an address */ + TEST_PARAMS4(test_parse_operand, "0x123", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x123U); +} + +static void test_parse_float_values() { + /* floating point - 'F' prefix */ + TEST_PARAMS3(test_parse_operand_fp, "f0.5", RC_OPERAND_FP, 0.5); + TEST_PARAMS3(test_parse_operand_fp, "F0.5", RC_OPERAND_FP, 0.5); + TEST_PARAMS3(test_parse_operand_fp, "f+0.5", RC_OPERAND_FP, 0.5); + TEST_PARAMS3(test_parse_operand_fp, "f-0.5", RC_OPERAND_FP, -0.5); + TEST_PARAMS3(test_parse_operand_fp, "f1.0", RC_OPERAND_FP, 1.0); + TEST_PARAMS3(test_parse_operand_fp, "f1.000000", RC_OPERAND_FP, 1.0); + TEST_PARAMS3(test_parse_operand_fp, "f1.000001", RC_OPERAND_FP, 1.000001); + TEST_PARAMS3(test_parse_operand_fp, "f1", RC_OPERAND_CONST, 1.0); + TEST_PARAMS3(test_parse_operand_fp, "f0.666666", RC_OPERAND_FP, 0.666666); + TEST_PARAMS3(test_parse_operand_fp, "f0.001", RC_OPERAND_FP, 0.001); + TEST_PARAMS3(test_parse_operand_fp, "f0.100", RC_OPERAND_FP, 0.1); + TEST_PARAMS3(test_parse_operand_fp, "f.12345", RC_OPERAND_FP, 0.12345); + + /* prefix, no value */ + TEST_PARAMS3(test_parse_error_operand, "f", 0, RC_INVALID_FP_OPERAND); + + /* float prefix, hex value */ + TEST_PARAMS3(test_parse_error_operand, "fabcd", 0, RC_INVALID_FP_OPERAND); + + /* float prefix, hex value, no period, parser will stop after valid numbers */ + TEST_PARAMS3(test_parse_error_operand, "f1d", 2, RC_OK); + + /* non-numeric decimal part */ + TEST_PARAMS3(test_parse_error_operand, "f1.d", 0, RC_INVALID_FP_OPERAND); + TEST_PARAMS3(test_parse_error_operand, "f1..0", 0, RC_INVALID_FP_OPERAND); + + /* non-C locale - parser will stop at comma */ + TEST_PARAMS3(test_parse_error_operand, "f1,23", 2, RC_OK); + TEST_PARAMS3(test_parse_error_operand, "f-1,23", 3, RC_OK); + + /* no prefix - parser will stop at period */ + TEST_PARAMS3(test_parse_error_operand, "0.5", 1, RC_OK); + TEST_PARAMS3(test_parse_error_operand, "-0.5", 2, RC_OK); +} + +static void test_evaluate_memory_references() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + memory.ram = ram; + memory.size = sizeof(ram); + + /* value */ + TEST_PARAMS3(test_evaluate_operand, "0", &memory, 0x00U); + + /* eight-bit */ + TEST_PARAMS3(test_evaluate_operand, "0xh0", &memory, 0x00U); + TEST_PARAMS3(test_evaluate_operand, "0xh1", &memory, 0x12U); + TEST_PARAMS3(test_evaluate_operand, "0xh4", &memory, 0x56U); + TEST_PARAMS3(test_evaluate_operand, "0xh5", &memory, 0x00U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xh4", &memory, 0xA9U); + + /* sixteen-bit */ + TEST_PARAMS3(test_evaluate_operand, "0x 0", &memory, 0x1200U); + TEST_PARAMS3(test_evaluate_operand, "0x 3", &memory, 0x56ABU); + TEST_PARAMS3(test_evaluate_operand, "0x 4", &memory, 0x0056U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0x 3", &memory, 0xA954U); + + /* twenty-four-bit */ + TEST_PARAMS3(test_evaluate_operand, "0xw0", &memory, 0x341200U); + TEST_PARAMS3(test_evaluate_operand, "0xw2", &memory, 0x56AB34U); + TEST_PARAMS3(test_evaluate_operand, "0xw3", &memory, 0x0056ABU); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xw2", &memory, 0xA954CBU); + + /* thirty-two-bit */ + TEST_PARAMS3(test_evaluate_operand, "0xx0", &memory, 0xAB341200U); + TEST_PARAMS3(test_evaluate_operand, "0xx1", &memory, 0x56AB3412U); + TEST_PARAMS3(test_evaluate_operand, "0xx3", &memory, 0x000056ABU); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xx1", &memory, 0xA954CBEDU); + TEST_PARAMS3(test_evaluate_operand, "~0xx3", &memory, 0xFFFFA954U); /* out of range */ + + /* sixteen-bit big endian*/ + TEST_PARAMS3(test_evaluate_operand, "0xi0", &memory, 0x0012U); + TEST_PARAMS3(test_evaluate_operand, "0xi3", &memory, 0xAB56U); + TEST_PARAMS3(test_evaluate_operand, "0xi4", &memory, 0x5600U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xi3", &memory, 0x54A9U); + + /* twenty-four-bit big endian */ + TEST_PARAMS3(test_evaluate_operand, "0xj0", &memory, 0x001234U); + TEST_PARAMS3(test_evaluate_operand, "0xj1", &memory, 0x1234ABU); + TEST_PARAMS3(test_evaluate_operand, "0xj3", &memory, 0xAB5600U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xj1", &memory, 0xEDCB54U); + TEST_PARAMS3(test_evaluate_operand, "~0xj3", &memory, 0x54A9FFU); /* out of range */ + + /* thirty-two-bit big endian */ + TEST_PARAMS3(test_evaluate_operand, "0xg0", &memory, 0x001234ABU); + TEST_PARAMS3(test_evaluate_operand, "0xg1", &memory, 0x1234AB56U); + TEST_PARAMS3(test_evaluate_operand, "0xg3", &memory, 0xAB560000U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xg1", &memory, 0xEDCB54A9U); + TEST_PARAMS3(test_evaluate_operand, "~0xg3", &memory, 0x54A9FFFFU); /* out of range */ + + /* nibbles */ + TEST_PARAMS3(test_evaluate_operand, "0xu0", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xu1", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xu4", &memory, 0x5U); + TEST_PARAMS3(test_evaluate_operand, "0xu5", &memory, 0x0U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xu4", &memory, 0xAU); + + TEST_PARAMS3(test_evaluate_operand, "0xl0", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xl1", &memory, 0x2U); + TEST_PARAMS3(test_evaluate_operand, "0xl4", &memory, 0x6U); + TEST_PARAMS3(test_evaluate_operand, "0xl5", &memory, 0x0U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xl4", &memory, 0x9U); + + /* bits */ + TEST_PARAMS3(test_evaluate_operand, "0xm0", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xm3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xn3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xo3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xp3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xq3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xr3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xs3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xt3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xm5", &memory, 0x0U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xm0", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xm3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xn3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xo3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xp3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xq3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xr3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xs3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xt3", &memory, 0x0U); + + /* bit count */ + TEST_PARAMS3(test_evaluate_operand, "0xk00", &memory, 0U); /* 0 bits in 0x00 */ + TEST_PARAMS3(test_evaluate_operand, "0xk01", &memory, 2U); /* 2 bits in 0x12 */ + TEST_PARAMS3(test_evaluate_operand, "0xk02", &memory, 3U); /* 3 bits in 0x34 */ + TEST_PARAMS3(test_evaluate_operand, "0xk03", &memory, 5U); /* 5 bits in 0xAB */ + TEST_PARAMS3(test_evaluate_operand, "0xk04", &memory, 4U); /* 4 bits in 0x56 */ + + /* BCD */ + TEST_PARAMS3(test_evaluate_operand, "b0xh3", &memory, 111U); /* 0xAB not technically valid in BCD */ + + ram[3] = 0x56; /* 0xAB not valid in BCD */ + ram[4] = 0x78; + TEST_PARAMS3(test_evaluate_operand, "b0xh0", &memory, 00U); + TEST_PARAMS3(test_evaluate_operand, "b0xh1", &memory, 12U); + TEST_PARAMS3(test_evaluate_operand, "b0x 1", &memory, 3412U); + TEST_PARAMS3(test_evaluate_operand, "b0xw1", &memory, 563412U); + TEST_PARAMS3(test_evaluate_operand, "b0xx1", &memory, 78563412U); + TEST_PARAMS3(test_evaluate_operand, "b0xi1", &memory, 1234U); + TEST_PARAMS3(test_evaluate_operand, "b0xj1", &memory, 123456U); + TEST_PARAMS3(test_evaluate_operand, "b0xg1", &memory, 12345678U); +} + +static void test_evaluate_delta_memory_reference() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_operand_t op; + const char* memaddr; + rc_parse_state_t parse; + char buffer[256]; + rc_memref_t* memrefs; + + memory.ram = ram; + memory.size = sizeof(ram); + + memaddr = "d0xh1"; + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&op, &memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x00); /* first call gets uninitialized value */ + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x12); /* second gets current value */ + + /* RC_OPERAND_DELTA is always one frame behind */ + ram[1] = 0x13; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x12U); + + ram[1] = 0x14; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x13U); + + ram[1] = 0x15; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x14U); + + ram[1] = 0x16; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x15U); + + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x16U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x16U); +} + +void test_evaluate_prior_memory_reference() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_operand_t op; + const char* memaddr; + rc_parse_state_t parse; + char buffer[256]; + rc_memref_t* memrefs; + + memory.ram = ram; + memory.size = sizeof(ram); + + memaddr = "p0xh1"; + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&op, &memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + /* RC_OPERAND_PRIOR only updates when the memory value changes */ + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x00); /* first call gets uninitialized value */ + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x00); /* value only changes when memory changes */ + + ram[1] = 0x13; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x12U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x12U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x12U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x12U); + + ram[1] = 0x14; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x13U); + + ram[1] = 0x15; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x14U); + + ram[1] = 0x16; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x15U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x15U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, memrefs), 0x15U); +} + +static void test_evaluate_memory_references_float() { + unsigned char ram[] = {0x00, 0x00, 0x80, 0x3F, 0x81, 0x00, 0x00, 0x00, 0x00, 0x81}; + memory_t memory; + memory.ram = ram; + memory.size = sizeof(ram); + + TEST_PARAMS3(test_evaluate_operand_float, "fF0", &memory, 1.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fM4", &memory, 1.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fL6", &memory, 1.0); /* MBF32_LE float */ + + /* BCD and inversion are not supported for floats - behaves as if the prefix wasn't present */ + TEST_PARAMS3(test_evaluate_operand_float, "bfF0", &memory, 1.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfM4", &memory, 1.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfL6", &memory, 1.0); /* MBF32_LE float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fF0", &memory, 1.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fM4", &memory, 1.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fL6", &memory, 1.0); /* MBF32_LE float */ + + ram[2] = 0x00; ram[3] = 0x40; /* set IEE754 float to 2.0 */ + ram[4] = 0x83; ram[5] = 0x40; /* set MBF32 float to 6.0 */ + ram[9] = 0x83; ram[8] = 0x00; /* set MBF32_LE float to 4.0 */ + + TEST_PARAMS3(test_evaluate_operand_float, "fF0", &memory, 2.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fM4", &memory, 6.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fL6", &memory, 4.0); /* MBF32_LE float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfF0", &memory, 2.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfM4", &memory, 6.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfL6", &memory, 4.0); /* MBF32_LE float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fF0", &memory, 2.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fM4", &memory, 6.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fL6", &memory, 4.0); /* MBF32_LE float */ +} + +static void test_evaluate_delta_memory_reference_float() { + unsigned char ram[] = {0x00, 0x00, 0x80, 0x3F}; + memory_t memory; + rc_operand_t op; + const char* memaddr; + rc_parse_state_t parse; + char buffer[256]; + rc_memref_t* memrefs; + + memory.ram = ram; + memory.size = sizeof(ram); + + memaddr = "dff0"; + rc_init_parse_state(&parse, buffer, 0, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&op, &memaddr, 0, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 0.0); /* first call gets uninitialized value */ + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 1.0); /* second gets current value */ + + /* RC_OPERAND_DELTA is always one frame behind */ + ram[3] = 0x40; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 1.0); + + ram[3] = 0x41; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 4.0); + + ram[3] = 0x42; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 16.0); + + ram[3] = 0x43; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 64.0); + + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 256.0); + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, memrefs), 256.0); +} + +void test_operand(void) { + TEST_SUITE_BEGIN(); + + test_parse_memory_references(); + test_parse_delta_memory_references(); + test_parse_prior_memory_references(); + test_parse_bcd_memory_references(); + test_parse_inverted_memory_references(); + + test_parse_unsigned_values(); + test_parse_signed_values(); + test_parse_hex_values(); + test_parse_float_values(); + + test_evaluate_memory_references(); + TEST(test_evaluate_delta_memory_reference); + TEST(test_evaluate_prior_memory_reference); + + test_evaluate_memory_references_float(); + TEST(test_evaluate_delta_memory_reference_float); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_rc_client.c b/src/rcheevos/test/rcheevos/test_rc_client.c new file mode 100644 index 000000000..fb036566f --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_rc_client.c @@ -0,0 +1,7473 @@ +#include "rc_client.h" + +#include "rc_consoles.h" +#include "rc_hash.h" +#include "rc_internal.h" +#include "rc_client_internal.h" +#include "rc_version.h" +#include "rc_api_runtime.h" + +#include "../rhash/data.h" +#include "../test_framework.h" + +#if defined(_WIN32) +#include +#elif defined(__unix__) && __STDC_VERSION__ >= 199309L +#include +#else +#define RC_NO_SLEEP +#endif + +static rc_client_t* g_client; +static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ + +#define GENERIC_ACHIEVEMENT_JSON(id, memaddr) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ + "\"Description\":\"Desc " id "\",\"Flags\":3,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ + "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" + +#define GENERIC_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\"}" + +static const char* patchdata_empty = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_2ach_0lbd = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}" + "]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_2ach_1lbd = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}" + "]," + "\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" + "]" + "}}"; + +static const char* patchdata_rich_presence_only = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]," + "\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"" + "}}"; + +static const char* patchdata_leaderboard_only = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}}"; + +static const char* patchdata_leaderboard_immediate_submit = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0=1::SUB:1=1::VAL:0x 000E", "SCORE") + "]" + "}}"; + +static const char* patchdata_bounds_check_system = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("1", "0xH0000=5") "," + GENERIC_ACHIEVEMENT_JSON("2", "0xHFFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("3", "0xH10000=5") "," + GENERIC_ACHIEVEMENT_JSON("4", "0x FFFE=5") "," + GENERIC_ACHIEVEMENT_JSON("5", "0x FFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "0x 10000=5") "," + GENERIC_ACHIEVEMENT_JSON("7", "I:0xH0000_0xHFFFF=5") + "]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_bounds_check_8 = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("408", "0xH0004=5") "," + GENERIC_ACHIEVEMENT_JSON("508", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("608", "0xH0006=5") "," + GENERIC_ACHIEVEMENT_JSON("708", "0xH0007=5") "," + GENERIC_ACHIEVEMENT_JSON("808", "0xH0008=5") "," + GENERIC_ACHIEVEMENT_JSON("416", "0x 0004=5") "," + GENERIC_ACHIEVEMENT_JSON("516", "0x 0005=5") "," + GENERIC_ACHIEVEMENT_JSON("616", "0x 0006=5") "," + GENERIC_ACHIEVEMENT_JSON("716", "0x 0007=5") "," + GENERIC_ACHIEVEMENT_JSON("816", "0x 0008=5") "," + GENERIC_ACHIEVEMENT_JSON("424", "0xW0004=5") "," + GENERIC_ACHIEVEMENT_JSON("524", "0xW0005=5") "," + GENERIC_ACHIEVEMENT_JSON("624", "0xW0006=5") "," + GENERIC_ACHIEVEMENT_JSON("724", "0xW0007=5") "," + GENERIC_ACHIEVEMENT_JSON("824", "0xW0008=5") "," + GENERIC_ACHIEVEMENT_JSON("432", "0xX0004=5") "," + GENERIC_ACHIEVEMENT_JSON("532", "0xX0005=5") "," + GENERIC_ACHIEVEMENT_JSON("632", "0xX0006=5") "," + GENERIC_ACHIEVEMENT_JSON("732", "0xX0007=5") "," + GENERIC_ACHIEVEMENT_JSON("832", "0xX0008=5") + "]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_exhaustive = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," + GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," + GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," + GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," + GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ + GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ + GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ + GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ + "]," + "\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"" + "}}"; + +#define HIDDEN_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"Hidden\":true}" + +static const char* patchdata_leaderboards_hidden = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + HIDDEN_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," + HIDDEN_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]," + "\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"" + "}}"; + +static const char* patchdata_unofficial_unsupported = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=1_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xHFEFEFEFE=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," + "\"Created\":1376971283,\"Modified\":1376971283}" + "]," + "\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xHFEFEFEFE=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" + "]" + "}}"; + +static const char* patchdata_subset = "{\"Success\":true,\"PatchData\":{" + "\"ID\":2345,\"Title\":\"Sample Game [Subset - Bonus]\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112234.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("7", "0xH0007=7") "," + GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("81", "STA:0xH0008=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("82", "STA:0xH0008=2::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}}"; + +static const char* patchdata_subset2 = "{\"Success\":true,\"PatchData\":{" + "\"ID\":2345,\"Title\":\"Sample Game [Subset - Multi]\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112234.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5501", "0xH0017=7") "," + GENERIC_ACHIEVEMENT_JSON("5502", "0xH0018=8") "," + GENERIC_ACHIEVEMENT_JSON("5503", "0xH0019=9") + "]," + "\"Leaderboards\":[" + "]" + "}}"; + +static const char* no_unlocks = "{\"Success\":true,\"Unlocks\":[],\"HardcoreUnlocks\":[]}"; + +/* startsession API only returns HardcoreUnlocks if an achievement has been earned in hardcore, + * even if the softcore unlock has a different timestamp */ +static const char* unlock_5502 = "{\"Success\":true,\"HardcoreUnlocks\":[{\"ID\":5502,\"When\":1234567890}]}"; +static const char* unlock_5501h_and_5502 = "{\"Success\":true,\"Unlocks\":[" + "{\"ID\":5502,\"When\":1234567899}" + "],\"HardcoreUnlocks\":[" + "{\"ID\":5501,\"When\":1234567890}" + "]}"; +static const char* unlock_5501_and_5502 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5501,\"When\":1234567890}," + "{\"ID\":5502,\"When\":1234567899}" + "]}"; +static const char* unlock_5501_5502_and_5503 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5501,\"When\":1234567890}," + "{\"ID\":5502,\"When\":1234567899}," + "{\"ID\":5503,\"When\":1234567999}" + "]}"; +static const char* unlock_8 = "{\"Success\":true,\"HardcoreUnlocks\":[{\"ID\":8,\"When\":1234567890}]}"; +static const char* unlock_6_8h_and_9 = "{\"Success\":true,\"Unlocks\":[" + "{\"ID\":6,\"When\":1234567890}," + "{\"ID\":9,\"When\":1234567899}" + "],\"HardcoreUnlocks\":[" + "{\"ID\":8,\"When\":1234567895}" + "]}"; + +static const char* response_429 = + "\n" + "429 Too Many Requests\n" + "\n" + "

429 Too Many Requests

\n" + "
nginx
\n" + "\n" + ""; + +static const char* response_502 = + "\n" + "502 Bad Gateway\n" + "\n" + "

502 Bad Gateway

\n" + "
nginx
\n" + "\n" + ""; + +static const char* response_503 = + "\n" + "503 Service Temporarily Unavailable\n" + "\n" + "

503 Service Temporarily Unavailable

\n" + "
nginx
\n" + "\n" + ""; + +/* ----- helpers ----- */ + +static void _assert_achievement_state(rc_client_t* client, uint32_t id, int expected_state) +{ + const rc_client_achievement_t* achievement = rc_client_get_achievement_info(client, id); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, expected_state); +} +#define assert_achievement_state(client, id, expected_state) ASSERT_HELPER(_assert_achievement_state(client, id, expected_state), "assert_achievement_state") + +typedef struct rc_client_captured_event_t +{ + rc_client_event_t event; + rc_client_server_error_t server_error; /* server_error goes out of scope, it needs to be copied too */ + uint32_t id; +} rc_client_captured_event_t; + +static rc_client_captured_event_t events[16]; +static int event_count = 0; + +static void rc_client_event_handler(const rc_client_event_t* e, rc_client_t* client) +{ + memcpy(&events[event_count], e, sizeof(rc_client_event_t)); + memset(&events[event_count].server_error, 0, sizeof(events[event_count].server_error)); + + switch (e->type) { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + events[event_count].id = e->achievement->id; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + events[event_count].id = e->leaderboard->id; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + events[event_count].id = e->leaderboard_tracker->id; + break; + + case RC_CLIENT_EVENT_GAME_COMPLETED: + events[event_count].id = rc_client_get_game_info(client)->id; + break; + + case RC_CLIENT_EVENT_SERVER_ERROR: { + static char event_server_error_message[128]; + + /* server error data is not maintained out of scope, copy it */ + memcpy(&events[event_count].server_error, e->server_error, sizeof(events[event_count].server_error)); + strcpy_s(event_server_error_message, sizeof(event_server_error_message), e->server_error->error_message); + events[event_count].server_error.error_message = event_server_error_message; + events[event_count].event.server_error = &events[event_count].server_error; + events[event_count].id = 0; + break; + } + + default: + events[event_count].id = 0; + break; + } + + ++event_count; +} + +static rc_client_event_t* find_event(uint8_t type, uint32_t id) +{ + int i; + + for (i = 0; i < event_count; ++i) { + if (events[i].id == id && events[i].event.type == type) + return &events[i].event; + } + + return NULL; +} + +static uint8_t* g_memory = NULL; +static uint32_t g_memory_size = 0; + +static void mock_memory(uint8_t* memory, uint32_t size) +{ + g_memory = memory; + g_memory_size = size; +} + +static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + if (g_memory_size > 0) { + if (address >= g_memory_size) + return 0; + + uint32_t num_avail = g_memory_size - address; + if (num_avail < num_bytes) + num_bytes = num_avail; + + memcpy(buffer, &g_memory[address], num_bytes); + return num_bytes; + } + + memset(&buffer, 0, num_bytes); + return num_bytes; +} + +static rc_clock_t g_now; + +static rc_clock_t rc_client_get_now_millisecs(const rc_client_t* client) +{ + return g_now; +} + +/* ----- API mocking ----- */ + +typedef struct rc_mock_api_response +{ + const char* request_params; + rc_api_server_response_t server_response; + int seen; + rc_client_server_callback_t async_callback; + void* async_callback_data; +} rc_mock_api_response; + +static rc_mock_api_response g_mock_api_responses[12]; +static int g_num_mock_api_responses = 0; + +static void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + rc_api_server_response_t server_response; + + int i; + for (i = 0; i < g_num_mock_api_responses; i++) { + if (strcmp(g_mock_api_responses[i].request_params, request->post_data) == 0) { + g_mock_api_responses[i].seen++; + callback(&g_mock_api_responses[i].server_response, callback_data); + return; + } + } + + ASSERT_FAIL("No API response for: %s", request->post_data); + + /* still call the callback to prevent memory leak */ + memset(&server_response, 0, sizeof(server_response)); + server_response.body = ""; + server_response.http_status_code = 500; + callback(&server_response, callback_data); +} + +static void rc_client_server_call_async(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = strdup(request->post_data); + g_mock_api_responses[g_num_mock_api_responses].async_callback = callback; + g_mock_api_responses[g_num_mock_api_responses].async_callback_data = callback_data; + g_mock_api_responses[g_num_mock_api_responses].seen = -1; + g_num_mock_api_responses++; +} + +static void _async_api_response(const char* request_params, const char* response_body, int http_status_code) +{ + int i; + for (i = 0; i < g_num_mock_api_responses; i++) + { + if (g_mock_api_responses[i].request_params && strcmp(g_mock_api_responses[i].request_params, request_params) == 0) + { + g_mock_api_responses[i].seen++; + g_mock_api_responses[i].server_response.body = response_body; + g_mock_api_responses[i].server_response.body_length = strlen(response_body); + g_mock_api_responses[i].server_response.http_status_code = http_status_code; + g_mock_api_responses[i].async_callback(&g_mock_api_responses[i].server_response, g_mock_api_responses[i].async_callback_data); + free((void*)g_mock_api_responses[i].request_params); + g_mock_api_responses[i].request_params = NULL; + + while (g_num_mock_api_responses > 0 && g_mock_api_responses[g_num_mock_api_responses - 1].request_params == NULL) + --g_num_mock_api_responses; + return; + } + } + + ASSERT_FAIL("No pending API request for: %s", request_params); +} + +static void async_api_response(const char* request_params, const char* response_body) +{ + _async_api_response(request_params, response_body, 200); +} + +static void async_api_error(const char* request_params, const char* response_body, int http_status_code) +{ + _async_api_response(request_params, response_body, http_status_code); +} + +static void _assert_api_called(const char* request_params, int count) +{ + int i; + for (i = 0; i < g_num_mock_api_responses; i++) { + if (g_mock_api_responses[i].request_params && + strcmp(g_mock_api_responses[i].request_params, request_params) == 0) { + ASSERT_NUM_EQUALS(g_mock_api_responses[i].seen, count); + return; + } + } + + ASSERT_NUM_EQUALS(0, count); +} +#define assert_api_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 1), "assert_api_called") +#define assert_api_not_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_called") +#define assert_api_call_count(request_params, num) ASSERT_HELPER(_assert_api_called(request_params, num), "assert_api_call_count") +#define assert_api_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, -1), "assert_api_pending") +#define assert_api_not_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_pending") + +static void reset_mock_api_handlers(void) +{ + g_num_mock_api_responses = 0; + memset(g_mock_api_responses, 0, sizeof(g_mock_api_responses)); +} + +static void mock_api_response(const char* request_params, const char* response_body) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; + g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; + g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); + g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = 200; + g_num_mock_api_responses++; +} + +static void mock_api_error(const char* request_params, const char* response_body, int http_status_code) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; + g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; + g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); + g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = http_status_code; + g_num_mock_api_responses++; +} + +static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called."); +} + +static rc_client_t* mock_client_not_logged_in(void) +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + rc_client_set_event_handler(client, rc_client_event_handler); + rc_client_set_get_time_millisecs_function(client, rc_client_get_now_millisecs); + + mock_memory(NULL, 0); + rc_api_set_host(NULL); + reset_mock_api_handlers(); + g_now = 100000; + + return client; +} + +static rc_client_t* mock_client_not_logged_in_async(void) +{ + rc_client_t* client = mock_client_not_logged_in(); + client->callbacks.server_call = rc_client_server_call_async; + return client; +} + +static rc_client_t* mock_client_logged_in(void) +{ + rc_client_t* client = mock_client_not_logged_in(); + client->user.username = "Username"; + client->user.display_name = "DisplayName"; + client->user.token = "ApiToken"; + client->user.score = 12345; + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + + return client; +} + +static void mock_client_load_game(const char* patchdata, const char* unlocks) +{ + reset_mock_api_handlers(); + event_count = 0; + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, unlocks); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + if (!g_client->game) + ASSERT_MESSAGE("client->game is NULL"); +} + +static rc_client_t* mock_client_game_loaded(const char* patchdata, const char* unlocks) +{ + g_client = mock_client_logged_in(); + + mock_client_load_game(patchdata, unlocks); + + return g_client; +} + +static void mock_client_load_subset(const char* patchdata, const char* unlocks) +{ + mock_api_response("r=patch&u=Username&t=ApiToken&g=2345", patchdata); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=2345&l=" RCHEEVOS_VERSION_STRING, unlocks); + + rc_client_begin_load_subset(g_client, 2345, rc_client_callback_expect_success, g_callback_userdata); +} + +/* ----- login ----- */ + +static void test_login_with_password(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void test_login_with_token(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login&u=User&t=ApiToken", + "{\"Success\":true,\"User\":\"User\",\"DisplayName\":\"Display\",\"Token\":\"ApiToken\",\"Score\":12345,\"Messages\":2}"); + + rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "Display"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_username_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "username is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_password_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "password is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_token_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "token is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_required_fields(void) +{ + g_client = mock_client_not_logged_in(); + + rc_client_begin_login_with_password(g_client, "User", "", rc_client_callback_expect_password_required, g_callback_userdata); + rc_client_begin_login_with_password(g_client, "", "Pa$$word", rc_client_callback_expect_username_required, g_callback_userdata); + rc_client_begin_login_with_password(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); + + rc_client_begin_login_with_token(g_client, "User", "", rc_client_callback_expect_token_required, g_callback_userdata); + rc_client_begin_login_with_token(g_client, "", "ApiToken", rc_client_callback_expect_username_required, g_callback_userdata); + rc_client_begin_login_with_token(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); + + ASSERT_NUM_EQUALS(g_client->state.user, RC_CLIENT_USER_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_credentials_error(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_API_FAILURE); + ASSERT_STR_EQUALS(error_message, "Invalid User/Password combination. Please try again"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_with_incorrect_password(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_error("r=login&u=User&p=Pa%24%24word", "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"}", 403); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_missing_token(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_STR_EQUALS(error_message, "Token not found in response"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_incomplete_response(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login&u=User&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\"}"); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_missing_token, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + async_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async_aborted(void) +{ + const rc_client_user_t* user; + rc_client_async_handle_t* handle; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", + rc_client_callback_expect_uncalled, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_login_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); + ASSERT_STR_EQUALS(error_message, "Login required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_logged_in(); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + + rc_client_logout(g_client); + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + /* reference pointer should be NULLed out */ + ASSERT_PTR_NULL(user->display_name); + ASSERT_PTR_NULL(user->username); + ASSERT_PTR_NULL(user->token); + + /* attempt to load game should fail */ + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_logout_with_game_loaded(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); + + rc_client_logout(g_client); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_login_aborted(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "Login aborted"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout_during_login(void) +{ + g_client = mock_client_not_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_login_aborted, g_callback_userdata); + rc_client_logout(g_client); + + async_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_no_longer_active(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "The requested game is no longer active"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout_during_fetch_game(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_no_longer_active, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + + rc_client_logout(g_client); + + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void test_user_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_user_get_image_url(rc_client_get_user_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/UserPic/DisplayName.png"); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive, unlock_6_8h_and_9); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_softcore(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive, unlock_6_8h_and_9); + rc_client_set_hardcore_enabled(g_client, 0); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 3); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 15); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_encore_mode(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_exhaustive); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, unlock_6_8h_and_9); + + rc_client_set_encore_mode_enabled(g_client, 1); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_with_unsupported_and_unofficial(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 7); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_with_unsupported_unlocks(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, unlock_5501_5502_and_5503); + + /* unlocked unsupported achievement should be counted in both unlocked and unsuppored buckets */ + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 2); + + ASSERT_NUM_EQUALS(summary.points_core, 7); + ASSERT_NUM_EQUALS(summary.points_unlocked, 7); + + rc_client_destroy(g_client); +} + + +/* ----- load game ----- */ + +static void rc_client_callback_expect_hash_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "hash is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_required_fields(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_load_game(g_client, NULL, rc_client_callback_expect_hash_required, g_callback_userdata); + rc_client_begin_load_game(g_client, "", rc_client_callback_expect_hash_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_unknown_game(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); + ASSERT_STR_EQUALS(error_message, "Unknown game"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_unknown_hash(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":0}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + rc_client_destroy(g_client); +} + +static void test_load_game_not_logged_in(void) +{ + g_client = mock_client_not_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public_.points, 2); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + /* login completion will trigger process to continue */ + async_api_response("r=login&u=Username&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + assert_api_pending("r=patch&u=Username&t=ApiToken&g=1234"); + + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_STR_EQUALS(g_client->user.username, "Username"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login_with_incorrect_password(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + /* login failure will trigger process to continue */ + async_api_error("r=login&u=Username&p=Pa%24%24word", + "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"}", 403); + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + ASSERT_PTR_NULL(g_client->user.username); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_too_many_requests(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + ASSERT_STR_EQUALS(error_message, "429 Too Many Requests"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_gameid_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_error("r=gameid&m=0123456789ABCDEF", response_429, 429); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_patch_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_error("r=patch&u=Username&t=ApiToken&g=1234", response_429, 429); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_startsession_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, response_429, 429); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_gameid_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_patch_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + assert_api_not_called("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_startsession_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_while_spectating(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + rc_client_set_spectator_mode_enabled(g_client, 1); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + /* spectator mode should not start a session or fetch unlocks */ + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public_.points, 2); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + /* spectator mode cannot be disabled if it was enabled before loading the game */ + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + rc_client_unload_game(g_client); + + /* spectator mode can be disabled after unloading game */ + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + + rc_client_destroy(g_client); +} + +static int rc_client_callback_process_game_data_called = 0; +static void rc_client_callback_process_game_data(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata) +{ + ASSERT_STR_EQUALS(server_response->body, patchdata_2ach_1lbd); + ASSERT_NUM_EQUALS(game_data_response->id, 1234); + ASSERT_NUM_EQUALS(game_data_response->num_achievements, 2); + ASSERT_NUM_EQUALS(game_data_response->num_leaderboards, 1); + rc_client_callback_process_game_data_called = 1; +} + +static void test_load_game_process_game_data(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + g_client->callbacks.post_process_game_data_response = rc_client_callback_process_game_data; + rc_client_callback_process_game_data_called = 0; + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public_.points, 2); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + + ASSERT_NUM_NOT_EQUALS(rc_client_callback_process_game_data_called, 0); + + rc_client_destroy(g_client); +} + +/* ----- unload game ----- */ + +static void test_unload_game(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_achievement_info(g_client, 5501)); + ASSERT_PTR_NOT_NULL(rc_client_get_leaderboard_info(g_client, 4401)); + + event_count = 0; + rc_client_unload_game(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + ASSERT_PTR_NULL(g_client->game); + ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NULL(rc_client_get_achievement_info(g_client, 5501)); + ASSERT_PTR_NULL(rc_client_get_leaderboard_info(g_client, 4401)); + + rc_client_destroy(g_client); +} + +static void test_unload_game_hides_ui(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x01] = 1; /* show indicator */ + memory[0x06] = 3; /* progress tracker */ + memory[0x0B] = 1; /* start leaderboard */ + memory[0x0E] = 17; /* leaderboard value */ + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + rc_client_unload_game(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + rc_client_destroy(g_client); + ASSERT_NUM_EQUALS(event_count, 0); +} + +/* ----- identify and load game ----- */ + +static void rc_client_callback_expect_data_or_file_path_required(int result, const char* error_message, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "either data or file_path is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static void test_identify_and_load_game_required_fields(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, NULL, NULL, 0, + rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_identify_and_load_game_console_specified(void) +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_console_not_specified(void) +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multiconsole_first(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* first hash lookup should be pending. inject a secondary console into the iterator */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 1); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ + iterator->consoles[iterator->index + 1] = 0; + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + assert_api_not_pending("r=gameid&m=64b131c5c7fec32985d9c99700babb7e"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* actual console ID returned from server */ + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multiconsole_second(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* first hash lookup should be pending. inject a secondary console into the iterator */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 1); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ + iterator->consoles[iterator->index + 1] = 0; + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + assert_api_pending("r=gameid&m=64b131c5c7fec32985d9c99700babb7e"); + async_api_response("r=gameid&m=64b131c5c7fec32985d9c99700babb7e", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* actual console ID returned from server */ + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "64b131c5c7fec32985d9c99700babb7e"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_unknown_hash(void) +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_NINTENDO); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_unknown_hash_multiconsole(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + /* first hash lookup should be pending. inject a secondary console into the iterator */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 1); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ + iterator->consoles[iterator->index + 1] = 0; + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + assert_api_pending("r=gameid&m=64b131c5c7fec32985d9c99700babb7e"); + async_api_response("r=gameid&m=64b131c5c7fec32985d9c99700babb7e", "{\"Success\":true,\"GameID\":0}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + /* when multiple hashes are tried, console will be unknown and hash will be a CSV */ + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857,64b131c5c7fec32985d9c99700babb7e"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_unknown_hash_console_specified(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* explicitly specify we only want the NES hash processed */ + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + /* first hash lookup should be pending. iterator should not have been initialized */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 0); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_NINTENDO); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash_unknown_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + /* same hash generated for all dsk consoles - only one server call should be made */ + assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash_differ(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* modify the checksum so callback for first lookup will generate a new lookup */ + memset(&image[256], 0, 32); + + /* first lookup fails */ + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + /* second lookup should succeed */ + async_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +/* ----- change media ----- */ + +static void test_change_media_required_fields(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + rc_client_begin_change_media(g_client, NULL, NULL, 0, + rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void rc_client_callback_expect_no_game_loaded(int result, const char* error_message, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); + ASSERT_STR_EQUALS(error_message, "No game loaded"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static void test_change_media_no_game_loaded(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_no_game_loaded, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_same_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + /* changing known discs within a game set is expected to succeed */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* resetting with a disc from the current game is allowed */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_known_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":5555}"); + + /* changing to a known disc from another game is allowed */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* resetting with a disc from another game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void rc_client_callback_expect_hardcore_disabled_undentified_media(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_HARDCORE_DISABLED); + ASSERT_STR_EQUALS(error_message, "Hardcore disabled. Unidentified media inserted."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_change_media_unknown_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + ASSERT_TRUE(rc_client_get_hardcore_enabled(g_client)); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + /* changing to an unknown disc is not allowed - could be a hacked version of one of the game's discs */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_hardcore_disabled_undentified_media, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + ASSERT_FALSE(rc_client_get_hardcore_enabled(g_client)); + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_unhashable(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + /* N64 hash will fail with Not a Nintendo 64 ROM */ + g_client->game->public_.console_id = RC_CONSOLE_NINTENDO_64; + + /* changing to a disc not supported by the system is allowed */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_back_and_forth(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + uint8_t* image2 = generate_generic_file(image_size); + memset(&image2[256], 0, 32); /* force image2 to be different */ + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); + assert_api_call_count("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", 1); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image2); + free(image); +} + +static void test_change_media_while_loading(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + /* load game lookup */ + async_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + + /* media request won't occur until patch data is received */ + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + /* secondary hash resolution does not occur until game is fully loaded or hash can't be compared to loaded game */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_while_loading_later(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + + /* get past fetching the patch data so there's a valid console for the change media call */ + async_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + + /* change_media should immediately attempt to resolve the new hash */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_aborted(void) +{ + rc_client_async_handle_t* handle; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* changing known discs within a game set is expected to succeed */ + handle = rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* hash should still have been captured and lookup should succeed without having to call server again */ + reset_mock_api_handlers(); + + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); + free(image); +} + +/* ----- get game image ----- */ + +static void test_game_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Images/112233.png"); + + rc_client_destroy(g_client); +} + +static void test_game_get_image_url_non_ssl(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + rc_client_set_host(g_client, "http://retroachievements.org"); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "http://media.retroachievements.org/Images/112233.png"); + + rc_client_destroy(g_client); +} + +static void test_game_get_image_url_custom(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + rc_client_set_host(g_client, "localhost"); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "http://localhost/Images/112233.png"); + + rc_client_destroy(g_client); +} + +/* ----- subset ----- */ + +static void test_load_subset(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + rc_client_subset_info_t* subset_info; + const rc_client_subset_t* subset; + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + mock_api_response("r=patch&u=Username&t=ApiToken&g=2345", patchdata_subset); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=2345&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_subset(g_client, 2345, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + subset = rc_client_get_subset_info(g_client, 2345); + ASSERT_PTR_NOT_NULL(subset); + if (subset) { + subset_info = g_client->game->subsets->next; + ASSERT_PTR_EQUALS(subset, &subset_info->public_); + + ASSERT_NUM_EQUALS(subset->id, 2345); + ASSERT_STR_EQUALS(subset->title, "Bonus"); + ASSERT_STR_EQUALS(subset->badge_name, "112234"); + ASSERT_NUM_EQUALS(subset->num_achievements, 3); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 2); + + achievement = &subset_info->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 7); + ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 7"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc 7"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "007"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_NUM_EQUALS(achievement->created_time, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated_time, 1376929305); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &subset_info->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 8); + ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 8"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc 8"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "008"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &subset_info->achievements[2]; + ASSERT_NUM_EQUALS(achievement->public_.id, 9); + ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 9"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc 9"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "009"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &subset_info->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 81); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard 81"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc 81"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + + leaderboard = &subset_info->leaderboards[1]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 82); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard 82"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc 82"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + rc_client_destroy(g_client); +} + +/* ----- achievement list ----- */ + +static void test_achievement_list_simple(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 0); + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unlocks(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in hardcore mode, 5501 should be unlocked, but 5502 will be locked */ + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + rc_client_destroy_achievement_list(list); + } + + rc_client_set_hardcore_enabled(g_client, 0); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in softcore mode, both should be unlocked */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unlocks_encore_mode(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in hardcore mode, 5501 should be unlocked, but both will appear locked due to encore mode */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_set_hardcore_enabled(g_client, 0); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in softcore mode, both should be unlocked, but will appear locked due to encore mode */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + /* unlock 5501, should appear unlocked again */ + mock_memory(memory, sizeof(memory)); + rc_client_do_frame(g_client); + memory[1] = 3; + memory[2] = 7; + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=0&m=0123456789ABCDEF&v=7f066800cd3962efeb1f479e5671b59c", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501)); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + + achievement = list->buckets[0].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + achievement = list->buckets[1].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + + achievement = list->buckets[0].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + achievement = list->buckets[1].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unofficial_and_unsupported(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unofficial_off(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 0); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 0); + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_buckets(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 5); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 9); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 70); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 71); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievement no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); + ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_with_unofficial_and_unsupported(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + mock_client_load_subset(patchdata_subset, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 3); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[2]->id, 9); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 3); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[1]->id, 8); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[2]->id, 9); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_buckets(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); + mock_client_load_subset(patchdata_subset2, unlock_5502); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + memory[0x17] = 7; /* trigger achievement 5501 */ + rc_client_do_frame(g_client); + event_count = 0; + + /* set the unlock time for achievement 5 back one second to ensure consistent sorting */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time--; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 6); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[3].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[5].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[5].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[5].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[5].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[5].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievements no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5501))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 5); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); + ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[1]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_buckets_subset_first(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":2345}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=2345", patchdata_subset2); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=2345&l=" RCHEEVOS_VERSION_STRING, unlock_5502); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_exhaustive); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&l=" RCHEEVOS_VERSION_STRING, unlock_8); + rc_client_begin_load_subset(g_client, 1234, rc_client_callback_expect_success, g_callback_userdata); + + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[0].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 6); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[3].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + rc_client_destroy(g_client); +} + +/* ----- leaderboards ----- */ + +static void test_leaderboard_list_simple(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_simple_with_unsupported(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_buckets(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_buckets_with_unsupported(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0B] = 3; /* start 52 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 1); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 5); + + iter = list->buckets[2].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_subset(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks); + mock_client_load_subset(patchdata_subset, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 2); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 81); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 82); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + memory[0x08] = 2; /* start 82 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 4); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 82); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 1); + + iter = list->buckets[2].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 81); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_hidden(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_leaderboards_hidden, no_unlocks); + + rc_client_do_frame(g_client); + + /* hidden leaderboards (45+48) should not appear in list */ + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static const char* lbinfo_4401_top_10 = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," + "{\"User\":\"DisplayName\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static const char* lbinfo_4401_top_10_no_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," + "{\"User\":\"PlayerJ\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static const char* lbinfo_4401_near_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":17,\"Index\":17,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":18,\"Index\":18,\"DateSubmitted\":1615634566}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":19,\"Index\":19,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":19,\"Index\":20,\"DateSubmitted\":1615623878}," + "{\"User\":\"DisplayName\",\"Score\":3811,\"Rank\":19,\"Index\":21,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":19,\"Index\":22,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":23,\"Index\":23,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":24,\"Index\":24,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":25,\"Index\":25,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":26,\"Index\":26,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static rc_client_leaderboard_entry_list_t* g_leaderboard_entries = NULL; +static void rc_client_callback_expect_leaderboard_entry_list(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + + ASSERT_PTR_NOT_NULL(list); + g_leaderboard_entries = list; +} + +static void test_fetch_leaderboard_entries(void) +{ + rc_client_leaderboard_entry_t* entry; + char url[256]; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); + + rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ASSERT_STR_EQUALS(entry->display, "003524"); + ASSERT_NUM_EQUALS(entry->index, 1); + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerG.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ASSERT_STR_EQUALS(entry->display, "003645"); + ASSERT_NUM_EQUALS(entry->index, 2); + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_NUM_EQUALS(entry->submitted, 1615634566); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "DisplayName"); + ASSERT_STR_EQUALS(entry->display, "003754"); + ASSERT_NUM_EQUALS(entry->index, 3); + ASSERT_NUM_EQUALS(entry->rank, 3); + ASSERT_NUM_EQUALS(entry->submitted, 1615234553); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/DisplayName.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 4); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615653844); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615623878); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615653284); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerA.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ASSERT_STR_EQUALS(entry->display, "003902"); + ASSERT_NUM_EQUALS(entry->index, 7); + ASSERT_NUM_EQUALS(entry->rank, 7); + ASSERT_NUM_EQUALS(entry->submitted, 1615632174); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ASSERT_STR_EQUALS(entry->display, "003956"); + ASSERT_NUM_EQUALS(entry->index, 8); + ASSERT_NUM_EQUALS(entry->rank, 8); + ASSERT_NUM_EQUALS(entry->submitted, 1616384834); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ASSERT_STR_EQUALS(entry->display, "003985"); + ASSERT_NUM_EQUALS(entry->index, 9); + ASSERT_NUM_EQUALS(entry->rank, 9); + ASSERT_NUM_EQUALS(entry->submitted, 1615238383); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + ASSERT_STR_EQUALS(entry->display, "004012"); + ASSERT_NUM_EQUALS(entry->index, 10); + ASSERT_NUM_EQUALS(entry->rank, 10); + ASSERT_NUM_EQUALS(entry->submitted, 1615638984); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 2); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void test_fetch_leaderboard_entries_no_user(void) +{ + rc_client_leaderboard_entry_t* entry; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10_no_user); + + rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerJ"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, -1); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void test_fetch_leaderboard_entries_around_user(void) +{ + rc_client_leaderboard_entry_t* entry; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); + + rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ASSERT_STR_EQUALS(entry->display, "003524"); + ASSERT_NUM_EQUALS(entry->index, 17); + ASSERT_NUM_EQUALS(entry->rank, 17); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ASSERT_STR_EQUALS(entry->display, "003645"); + ASSERT_NUM_EQUALS(entry->index, 18); + ASSERT_NUM_EQUALS(entry->rank, 18); + ASSERT_NUM_EQUALS(entry->submitted, 1615634566); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 19); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615653844); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 20); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615623878); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "DisplayName"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 21); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615234553); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 22); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615653284); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ASSERT_STR_EQUALS(entry->display, "003902"); + ASSERT_NUM_EQUALS(entry->index, 23); + ASSERT_NUM_EQUALS(entry->rank, 23); + ASSERT_NUM_EQUALS(entry->submitted, 1615632174); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ASSERT_STR_EQUALS(entry->display, "003956"); + ASSERT_NUM_EQUALS(entry->index, 24); + ASSERT_NUM_EQUALS(entry->rank, 24); + ASSERT_NUM_EQUALS(entry->submitted, 1616384834); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ASSERT_STR_EQUALS(entry->display, "003985"); + ASSERT_NUM_EQUALS(entry->index, 25); + ASSERT_NUM_EQUALS(entry->rank, 25); + ASSERT_NUM_EQUALS(entry->submitted, 1615238383); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + ASSERT_STR_EQUALS(entry->display, "004012"); + ASSERT_NUM_EQUALS(entry->index, 26); + ASSERT_NUM_EQUALS(entry->rank, 26); + ASSERT_NUM_EQUALS(entry->submitted, 1615638984); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 4); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_entry_list_login_required(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); + ASSERT_STR_EQUALS(error_message, "Login required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + ASSERT_PTR_NULL(list); +} + +static void test_fetch_leaderboard_entries_around_user_not_logged_in(void) +{ + g_client = mock_client_not_logged_in(); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); + + rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, + rc_client_callback_expect_leaderboard_entry_list_login_required, g_callback_userdata); + ASSERT_PTR_NULL(g_leaderboard_entries); + + assert_api_not_called("r=lbinfo&i=4401&u=Username&c=10"); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_uncalled(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called.") +} + +static void test_fetch_leaderboard_entries_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + g_leaderboard_entries = NULL; + + handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); + ASSERT_PTR_NULL(g_leaderboard_entries); + + rc_client_destroy(g_client); +} + +/* ----- do frame ----- */ + +static void test_do_frame_bounds_check_system(void) +{ + const uint32_t memory_size = 0x10010; /* provide more memory than system expects */ + uint8_t* memory = (uint8_t*)calloc(1, memory_size); + ASSERT_PTR_NOT_NULL(memory); + + g_client = mock_client_game_loaded(patchdata_bounds_check_system, no_unlocks); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":7,\"AchievementsRemaining\":4}"); + + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->max_valid_address, 0xFFFF); + + assert_achievement_state(g_client, 1, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 2, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 3, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ + assert_achievement_state(g_client, 4, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* cannot read two bytes from 0xFFFF, but size isn't enforced until do_frame */ + assert_achievement_state(g_client, 6, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* verify that reading at the edge of the memory bounds fails */ + mock_memory(memory, 0x10000); + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* cannot read two bytes from 0xFFFF */ + + /* set up memory so achievement 7 would trigger if the pointed at address were valid */ + /* achievement should not trigger - invalid address should be ignored */ + memory[0x10000] = 5; + memory[0x00000] = 1; /* byte(0xFFFF + byte(0x0000)) == 5 */ + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* even if the extra memory is available, it shouldn't try to read beyond the system defined max address */ + mock_memory(memory, memory_size); + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* change max valid address so memory will be evaluated. achievement should trigger */ + g_client->game->max_valid_address = memory_size - 1; + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + + rc_client_destroy(g_client); + free(memory); +} + +static void test_do_frame_bounds_check_available(void) +{ + uint8_t memory[8] = { 0,0,0,0,0,0,0,0 }; + g_client = mock_client_game_loaded(patchdata_bounds_check_8, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + /* all addresses are valid according to the system, so no achievements should be disabled yet. */ + assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* limit the memory that's actually exposed and try to process a frame */ + mock_memory(memory, sizeof(memory)); + rc_client_do_frame(g_client); + + assert_achievement_state(g_client, 408, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 508, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 608, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 708, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 416, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 516, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 616, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 716, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 816, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 424, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 524, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 24-bit read actually fetches 32-bits */ + assert_achievement_state(g_client, 624, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ + assert_achievement_state(g_client, 724, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 824, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 432, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 532, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only three bytes available */ + assert_achievement_state(g_client, 632, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ + assert_achievement_state(g_client, 732, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 832, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_already_awarded(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_server_error(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"Achievement not found\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + /* achievement still counts as triggered */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ + + /* but an error should have been reported */ + event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->server_error->api, "award_achievement"); + ASSERT_STR_EQUALS(event->server_error->error_message, "Achievement not found"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_while_spectating(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"Achievement should not have been unlocked in spectating mode\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + /* achievement still counts as triggered */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ + + /* expect API not called */ + assert_api_not_called("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + } + + rc_client_destroy(g_client); +} + +static int rc_client_callback_deny_unlock(uint32_t achievement_id, rc_client_t* client) +{ + return 0; +} + +static int rc_client_callback_allow_unlock(uint32_t achievement_id, rc_client_t* client) +{ + return 1; +} + +static void test_do_frame_achievement_trigger_blocked(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + uint32_t num_active; + const char* api_call8 = "r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"; + const char* api_call9 = "r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6"; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.can_submit_achievement_unlock = rc_client_callback_deny_unlock; + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + num_active = g_client->game->runtime.trigger_count; + + mock_api_response(api_call8, + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + mock_api_response(api_call9, + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12350); /* 12345+5 - not updated by server response that didn't happen */ + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + assert_api_not_called(api_call8); + + g_client->callbacks.can_submit_achievement_unlock = rc_client_callback_allow_unlock; + + memory[9] = 9; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + assert_api_called(api_call9); + + rc_client_destroy(g_client); +} + + +static void test_do_frame_achievement_trigger_automatic_retry(void) +{ + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_response(unlock_request_params, ""); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it for one second later */ + async_api_response(unlock_request_params, ""); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 1 * 1000); + g_now += 1 * 1000; + + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it for two seconds later */ + async_api_response(unlock_request_params, ""); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 2 * 1000); + g_now += 2 * 1000; + + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(unlock_request_params, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_automatic_retry_429(void) +{ + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_error(unlock_request_params, response_429, 429); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it */ + async_api_error(unlock_request_params, response_429, 429); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it */ + async_api_error(unlock_request_params, response_429, 429); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(unlock_request_params, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_automatic_retry_502(void) +{ + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_error(unlock_request_params, response_502, 502); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it */ + async_api_error(unlock_request_params, response_502, 502); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it */ + async_api_error(unlock_request_params, response_502, 502); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(unlock_request_params, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_automatic_retry_503(void) +{ + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(unlock_request_params, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_subset(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + mock_client_load_subset(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5437); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + + +static void test_do_frame_achievement_measured(void) +{ + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=70&h=1&m=0123456789ABCDEF&v=61e40027573e2cde88b49d27f6804879", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":70,\"AchievementsRemaining\":11}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=71&h=1&m=0123456789ABCDEF&v=3a8d55b81d391557d5111306599a2b0d", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":71,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x10] = 0x39; memory[0x11] = 0x30; /* 12345 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12345/100000"); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12%"); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increment measured value - raw counter will report progress change, percentage will not */ + memory[0x10] = 0x3A; /* 12346 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12346/100000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increment measured value - raw counter will report progress change, percentage will not */ + memory[0x11] = 0x33; /* 13114 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "13114/100000"); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "13%"); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* trigger measured achievements - progress becomes blank */ + memory[0x10] = 0xA0; memory[0x11] = 0x86; memory[0x12] = 0x01; /* 100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); /* two TRIGGERED events, and no PROGRESS_INDICATOR_SHOW events */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_measured_progress_event(void) +{ + rc_client_event_t* event; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=6&h=1&m=0123456789ABCDEF&v=65206f4290098ecd30c7845e895057d0", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":6,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x06] = 3; /* 3/6 */ + memory[0x11] = 0xC3; memory[0x10] = 0x4F; /* 49999/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + /* 3/6 = 50%, 49999/100000 = 49.999% */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); + + /* both achievements should have been updated, */ + achievement = rc_client_get_achievement_info(g_client, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "3/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 50.0); + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "49999/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 49.999); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* any change will trigger the popup - even dropping */ + memory[0x10] = 0x4E; /* 49998 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 70); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "49998/100000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* don't trigger popup when value changes to 0 as the measured_progress string will be blank */ + memory[0x06] = 0; /* 0 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + /* both at 50%, only report first */ + memory[0x06] = 3; /* 3/6 */ + memory[0x11] = 0xC3; memory[0x10] = 0x50; /* 50000/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second slightly ahead */ + memory[0x6] = 4; /* 4/6 */ + memory[0x12] = 1; memory[0x11] = 0x04; memory[0x10] = 0x6B; /* 66667/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 70); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "66667/100000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* don't show popup on trigger */ + memory[0x06] = 6; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, ""); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + ASSERT_PTR_NOT_NULL(g_client->game->progress_tracker.hide_callback); + g_now += 2 * 1000; + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_challenge_indicator(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 1; /* show indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 0; /* hide indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 1; /* show indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* trigger achievement - expect both hide and trigger events. both should have triggered achievement data */ + memory[7] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); + ASSERT_PTR_NOT_NULL(event); + + memory[9] = 9; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery_encore(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); + ASSERT_PTR_NOT_NULL(event); + + memory[9] = 9; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_started(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_update(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* update the leaderboard */ + memory[0x0E] = 18; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000018"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_failed(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel the leaderboard */ + memory[0x0C] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_server_error(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":false,\"Error\":\"Leaderboard not found\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + /* an error should have also been reported */ + event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->server_error->api, "submit_lboard_entry"); + ASSERT_STR_EQUALS(event->server_error->error_message, "Leaderboard not found"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_while_spectating(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":false,\"Error\":\"Leaderboard entry should not have been submitted in spectating mode\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* expect API not called */ + assert_api_not_called("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_immediate(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_leaderboard_immediate_submit, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard (it will immediately submit) */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* don't expect start or tracker events - only submit */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_hidden(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_leaderboards_hidden, no_unlocks); + + /* hidden leaderboards should still start/track/submit normally. they just don't appear in list */ + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=48&s=17&m=0123456789ABCDEF&v=468a1f9e9475d8c4d862f48cc8806018", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0A] = 2; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_destroy(g_client); +} + +static int rc_client_callback_deny_leaderboard(uint32_t leaderboard_id, rc_client_t* client) +{ + return 0; +} + +static int rc_client_callback_allow_leaderboard(uint32_t leaderboard_id, rc_client_t* client) +{ + return 1; +} + +static void test_do_frame_leaderboard_submit_blocked(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + const char* api_call = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.can_submit_leaderboard_entry = rc_client_callback_deny_leaderboard; + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + + mock_api_response(api_call, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0B] = 0; + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + assert_api_not_called(api_call); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + g_client->callbacks.can_submit_leaderboard_entry = rc_client_callback_allow_leaderboard; + + /* restart the leaderboard - will immediately submit */ + memory[0x0B] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44)); + + assert_api_called(api_call); + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_tracker_sharing(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start one leaderboard (one tracker) */ + memory[0x0B] = 1; + memory[0x0E] = 17; + memory[0x0F] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start additional leaderboards (45,46,47) - 45 and 46 should generate new trackers */ + memory[0x0A] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 5); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 45); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 45 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 46); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 46 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 47); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 44,47 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); /* 45 has different size */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 3); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); /* 46 has different format */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start additional leaderboard (48) - should share tracker with 44 */ + memory[0x0A] = 2; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 3); /* 44,47,48 */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 44 */ + memory[0x0C] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 47,48 */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 45 */ + memory[0x0C] = 2; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 45); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 46 */ + memory[0x0C] = 3; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 46); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 3); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel 47, start 51 */ + memory[0x0A] = 3; + memory[0x0B] = 0; + memory[0x0C] = 4; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 47); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 1); /* 48 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 51 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel 48 */ + memory[0x0C] = 5; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_tracker_sharing_hits(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start leaderboards 51,52 (share tracker) */ + memory[0x0A] = 3; + memory[0x0B] = 3; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 52); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 52)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); + + /* hit count ticks */ + memory[0x09] = 1; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); + + /* cancel leaderboard 51 */ + memory[0x0C] = 6; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "2"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "2"); + + /* hit count ticks */ + memory[0x0A] = 0; + memory[0x0C] = 0; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "3"); + + /* restart leaderboard 51 - hit count differs, can't share */ + memory[0x0A] = 3; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "1"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "4"); /* 52 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); /* 51 */ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_automatic_retry(void) +{ + const char* submit_entry_params = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_response(submit_entry_params, ""); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it for one second later */ + async_api_response(submit_entry_params, ""); + assert_api_call_count(submit_entry_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 1 * 1000); + g_now += 1 * 1000; + + rc_client_idle(g_client); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it for two seconds later */ + async_api_response(submit_entry_params, ""); + assert_api_call_count(submit_entry_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 2 * 1000); + g_now += 2 * 1000; + + rc_client_idle(g_client); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(submit_entry_params, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + } + + rc_client_destroy(g_client); +} + +static void test_clock_get_now_millisecs(void) +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + rc_get_time_millisecs_func_t get_millisecs = client->callbacks.get_time_millisecs; + +#ifdef RC_NO_SLEEP + rc_clock_t time1; + ASSERT_PTR_NOT_NULL(get_millisecs); + time1 = get_millisecs(client); + ASSERT_NUM_NOT_EQUALS(time1, 0); +#else + rc_clock_t time1, time2, diff; + time_t first = time(NULL), now; + + do { + ASSERT_PTR_NOT_NULL(get_millisecs); + time1 = get_millisecs(client); + ASSERT_NUM_NOT_EQUALS(time1, 0); + +#if defined(_WIN32) + Sleep(50); +#else + usleep(50000); +#endif + + time2 = get_millisecs(client); + ASSERT_NUM_NOT_EQUALS(time2, 0); + diff = time2 - time1; + + ASSERT_NUM_GREATER(diff, 49); + if (diff < 100) + break; + + now = time(NULL); + if (now - first >= 3) { + ASSERT_FAIL("could not get a 50ms sleep interval within 3 seconds"); + break; + } + + } while (1); +#endif + + rc_client_destroy(client); +} + +/* ----- ping ----- */ + +static void test_idle_ping(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234", "{\"Success\":true}"); + + rc_client_idle(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + } + + /* unloading game should unschedule ping */ + rc_client_unload_game(g_client); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + rc_client_destroy(g_client); +} + +static void test_do_frame_ping_rich_presence(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_memory(memory, sizeof(memory)); + memory[0x03] = 25; + + /* before rc_client_do_frame, memory will not have been read. all values will be 0 */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a0", "{\"Success\":true}"); + + rc_client_idle(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + /* rc_client_do_frame will update the memory, so the message will contain appropriate data */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25", "{\"Success\":true}"); + + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25"); + + /* change the memory to make sure the rich presence gets updated */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75", "{\"Success\":true}"); + memory[0x03] = 75; + + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75"); + + /* no change to rich presence strings. make sure the callback still gets called again */ + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + assert_api_call_count("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75", 2); + } + + rc_client_destroy(g_client); +} + +static void test_reset_hides_widgets(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x06] = 3; /* progress indicator for achievement 6 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + rc_client_reset(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* non tracked achievements/leaderboards should also be reset to waiting */ + achievement = rc_client_get_achievement_info(g_client, 5); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + + leaderboard = rc_client_get_leaderboard_info(g_client, 46); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + rc_client_destroy(g_client); +} + +/* ----- progress ----- */ + +static void test_deserialize_progress_updates_widgets(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + const rc_client_event_t* event; + uint8_t* serialized1; + uint8_t* serialized2; + size_t serialize_size; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* create an initial checkpoint */ + serialize_size = rc_client_progress_size(g_client); + serialized1 = (uint8_t*)malloc(serialize_size); + serialized2 = (uint8_t*)malloc(serialize_size); + ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized1), RC_OK); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x06] = 4; /* progress indicator for achievement 6*/ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* capture the state with the widgets visible */ + ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized2), RC_OK); + + /* deserialize current state. expect progress tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize original state. expect challenge indicator hide, tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized1), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_ACTIVE); + + /* deserialize second state. expect challenge indicator show, tracker show */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* update tracker value */ + memory[0x0E] = 30; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000030"); + + /* deserialize second state. expect challenge tracker update to old value */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 1); + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + free(serialized2); + free(serialized1); + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_null(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, NULL), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ + ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* advance frame, challenge indicator and leaderboard tracker should reappear */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_invalid(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, memory), RC_INVALID_STATE); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ + ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* advance frame, challenge indicator and leaderboard tracker should reappear */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + rc_client_destroy(g_client); +} + +/* ----- processing required ----- */ + +static void test_processing_required(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_empty_game(void) +{ + g_client = mock_client_game_loaded(patchdata_empty, no_unlocks); + + ASSERT_FALSE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_rich_presence_only(void) +{ + g_client = mock_client_game_loaded(patchdata_rich_presence_only, no_unlocks); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_leaderboard_only(void) +{ + g_client = mock_client_game_loaded(patchdata_leaderboard_only, no_unlocks); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_after_mastery(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501_and_5502); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_after_mastery_no_leaderboards(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_0lbd, unlock_5501_and_5502); + + ASSERT_FALSE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +/* ----- settings ----- */ + +static void test_set_hardcore_disable(void) +{ + const rc_client_achievement_t* achievement; + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* 5502 should be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); + } + + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_disable_active_tracker(void) +{ + const rc_client_leaderboard_t* leaderboard; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x0C] = 1; + memory[0x0E] = 25; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 4401); + ASSERT_PTR_NOT_NULL(event); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + ASSERT_NUM_EQUALS(event_count, 1); + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable(void) +{ + const rc_client_achievement_t* achievement; + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_logged_in(); + rc_client_set_hardcore_enabled(g_client, 0); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); + } + + /* when enabling hardcore, flag waiting_for_reset. this will prevent processing until rc_client_reset is called */ + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RESET, 0)); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* 5502 should be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); + } + + /* resetting clears waiting_for_reset */ + rc_client_reset(g_client); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + /* hardcore already enabled, attempting to set it again shouldn't flag waiting_for_reset */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable_no_game_loaded(void) +{ + g_client = mock_client_logged_in(); + rc_client_set_hardcore_enabled(g_client, 0); + + /* enabling hardcore before a game is loaded just toggles the flag */ + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable_encore_mode(void) +{ + const rc_client_achievement_t* achievement; + rc_client_achievement_info_t* achievement_info; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + g_client->game->runtime.triggers[1].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + /* trigger an achievement */ + achievement_info = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 5501); + achievement_info->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_TRIGGERED; + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* only one active now */ + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_PTR_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger, g_client->game->runtime.triggers[0].trigger); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); + + rc_client_destroy(g_client); +} + +static void test_set_encore_mode_enable(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + /* toggle encore mode with a game loaded has no effect */ + rc_client_set_encore_mode_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + rc_client_destroy(g_client); +} + +static void test_set_encore_mode_disable(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + rc_client_set_encore_mode_enabled(g_client, 0); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + /* toggle encore mode with a game loaded has no effect */ + rc_client_set_encore_mode_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + rc_client_destroy(g_client); +} + +/* ----- harness ----- */ + +void test_client(void) { + TEST_SUITE_BEGIN(); + + /* login */ + TEST(test_login_with_password); + TEST(test_login_with_token); + TEST(test_login_required_fields); + TEST(test_login_with_incorrect_password); + TEST(test_login_incomplete_response); + TEST(test_login_with_password_async); + TEST(test_login_with_password_async_aborted); + + /* logout */ + TEST(test_logout); + TEST(test_logout_with_game_loaded); + TEST(test_logout_during_login); + TEST(test_logout_during_fetch_game); + + /* user */ + TEST(test_user_get_image_url); + + TEST(test_get_user_game_summary); + TEST(test_get_user_game_summary_softcore); + TEST(test_get_user_game_summary_encore_mode); + TEST(test_get_user_game_summary_with_unsupported_and_unofficial); + TEST(test_get_user_game_summary_with_unsupported_unlocks); + + /* load game */ + TEST(test_load_game_required_fields); + TEST(test_load_game_unknown_hash); + TEST(test_load_game_not_logged_in); + TEST(test_load_game); + TEST(test_load_game_async_login); + TEST(test_load_game_async_login_with_incorrect_password); + TEST(test_load_game_gameid_failure); + TEST(test_load_game_patch_failure); + TEST(test_load_game_startsession_failure); + TEST(test_load_game_gameid_aborted); + TEST(test_load_game_patch_aborted); + TEST(test_load_game_startsession_aborted); + TEST(test_load_game_while_spectating); + TEST(test_load_game_process_game_data); + + /* unload game */ + TEST(test_unload_game); + TEST(test_unload_game_hides_ui); + + /* identify and load game */ + TEST(test_identify_and_load_game_required_fields); + TEST(test_identify_and_load_game_console_specified); + TEST(test_identify_and_load_game_console_not_specified); + TEST(test_identify_and_load_game_multiconsole_first); + TEST(test_identify_and_load_game_multiconsole_second); + TEST(test_identify_and_load_game_unknown_hash); + TEST(test_identify_and_load_game_unknown_hash_multiconsole); + TEST(test_identify_and_load_game_unknown_hash_console_specified); + TEST(test_identify_and_load_game_multihash); + TEST(test_identify_and_load_game_multihash_unknown_game); + TEST(test_identify_and_load_game_multihash_differ); + + /* change media */ + TEST(test_change_media_required_fields); + TEST(test_change_media_no_game_loaded); + TEST(test_change_media_same_game); + TEST(test_change_media_known_game); + TEST(test_change_media_unknown_game); + TEST(test_change_media_unhashable); + TEST(test_change_media_back_and_forth); + TEST(test_change_media_while_loading); + TEST(test_change_media_while_loading_later); + TEST(test_change_media_aborted); + + /* game */ + TEST(test_game_get_image_url); + TEST(test_game_get_image_url_non_ssl); + TEST(test_game_get_image_url_custom); + + /* subset */ + TEST(test_load_subset); + + /* achievements */ + TEST(test_achievement_list_simple); + TEST(test_achievement_list_simple_with_unlocks); + TEST(test_achievement_list_simple_with_unlocks_encore_mode); + TEST(test_achievement_list_simple_with_unofficial_and_unsupported); + TEST(test_achievement_list_simple_with_unofficial_off); + TEST(test_achievement_list_buckets); + TEST(test_achievement_list_subset_with_unofficial_and_unsupported); + TEST(test_achievement_list_subset_buckets); + TEST(test_achievement_list_subset_buckets_subset_first); + + TEST(test_achievement_get_image_url); + + /* leaderboards */ + TEST(test_leaderboard_list_simple); + TEST(test_leaderboard_list_simple_with_unsupported); + TEST(test_leaderboard_list_buckets); + TEST(test_leaderboard_list_buckets_with_unsupported); + TEST(test_leaderboard_list_subset); + TEST(test_leaderboard_list_hidden); + + TEST(test_fetch_leaderboard_entries); + TEST(test_fetch_leaderboard_entries_no_user); + TEST(test_fetch_leaderboard_entries_around_user); + TEST(test_fetch_leaderboard_entries_around_user_not_logged_in); + TEST(test_fetch_leaderboard_entries_aborted); + + /* do frame */ + TEST(test_do_frame_bounds_check_system); + TEST(test_do_frame_bounds_check_available); + TEST(test_do_frame_achievement_trigger); + TEST(test_do_frame_achievement_trigger_already_awarded); + TEST(test_do_frame_achievement_trigger_server_error); + TEST(test_do_frame_achievement_trigger_while_spectating); + TEST(test_do_frame_achievement_trigger_blocked); + TEST(test_do_frame_achievement_trigger_automatic_retry); + TEST(test_do_frame_achievement_trigger_automatic_retry_429); + TEST(test_do_frame_achievement_trigger_automatic_retry_502); + TEST(test_do_frame_achievement_trigger_automatic_retry_503); + TEST(test_do_frame_achievement_trigger_subset); + TEST(test_do_frame_achievement_measured); + TEST(test_do_frame_achievement_measured_progress_event); + TEST(test_do_frame_achievement_challenge_indicator); + TEST(test_do_frame_mastery); + TEST(test_do_frame_mastery_encore); + TEST(test_do_frame_leaderboard_started); + TEST(test_do_frame_leaderboard_update); + TEST(test_do_frame_leaderboard_failed); + TEST(test_do_frame_leaderboard_submit); + TEST(test_do_frame_leaderboard_submit_server_error); + TEST(test_do_frame_leaderboard_submit_while_spectating); + TEST(test_do_frame_leaderboard_submit_immediate); + TEST(test_do_frame_leaderboard_submit_hidden); + TEST(test_do_frame_leaderboard_submit_blocked); + TEST(test_do_frame_leaderboard_tracker_sharing); + TEST(test_do_frame_leaderboard_tracker_sharing_hits); + TEST(test_do_frame_leaderboard_submit_automatic_retry); + + TEST(test_clock_get_now_millisecs); + + /* ping */ + TEST(test_idle_ping); + TEST(test_do_frame_ping_rich_presence); + + /* reset */ + TEST(test_reset_hides_widgets); + + /* deserialize_progress */ + TEST(test_deserialize_progress_updates_widgets); + TEST(test_deserialize_progress_null); + TEST(test_deserialize_progress_invalid); + + /* processing required */ + TEST(test_processing_required); + TEST(test_processing_required_empty_game); + TEST(test_processing_required_rich_presence_only); + TEST(test_processing_required_leaderboard_only); + TEST(test_processing_required_after_mastery); + TEST(test_processing_required_after_mastery_no_leaderboards); + + /* settings */ + TEST(test_set_hardcore_disable); + TEST(test_set_hardcore_disable_active_tracker); + TEST(test_set_hardcore_enable); + TEST(test_set_hardcore_enable_no_game_loaded); + TEST(test_set_hardcore_enable_encore_mode); + TEST(test_set_encore_mode_enable); + TEST(test_set_encore_mode_disable); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_rc_libretro.c b/src/rcheevos/test/rcheevos/test_rc_libretro.c new file mode 100644 index 000000000..93dbb6465 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_rc_libretro.c @@ -0,0 +1,785 @@ +#include "rc_libretro.h" + +#include "rc_compat.h" +#include "rc_consoles.h" + +#include "../test_framework.h" +#include "../rhash/mock_filereader.h" + +static void* retro_memory_data[4] = { NULL, NULL, NULL, NULL }; +static size_t retro_memory_size[4] = { 0, 0, 0, 0 }; + +static void libretro_get_core_memory_info(unsigned id, rc_libretro_core_memory_info_t* info) +{ + info->data = retro_memory_data[id]; + info->size = retro_memory_size[id]; +} + +static int libretro_get_image_path(unsigned index, char* buffer, size_t buffer_size) +{ + if (index < 0 || index > 9) + return 0; + + snprintf(buffer, buffer_size, "save%d.dsk", index); + return 1; +} + +static void test_allowed_setting(const char* library_name, const char* setting, const char* value) { + const rc_disallowed_setting_t* settings = rc_libretro_get_disallowed_settings(library_name); + if (!settings) + return; + + ASSERT_TRUE(rc_libretro_is_setting_allowed(settings, setting, value)); +} + +static void test_disallowed_setting(const char* library_name, const char* setting, const char* value) { + const rc_disallowed_setting_t* settings = rc_libretro_get_disallowed_settings(library_name); + ASSERT_PTR_NOT_NULL(settings); + ASSERT_FALSE(rc_libretro_is_setting_allowed(settings, setting, value)); +} + +static void test_allowed_system(const char* library_name, int console_id) { + ASSERT_TRUE(rc_libretro_is_system_allowed(library_name, console_id)); +} + +static void test_disallowed_system(const char* library_name, int console_id) { + ASSERT_FALSE(rc_libretro_is_system_allowed(library_name, console_id)); +} + +static void test_memory_init_without_regions() { + rc_libretro_memory_regions_t regions; + unsigned avail; + unsigned char buffer1[16], buffer2[8], buffer3[4]; + int i; + + for (i = 0; i < sizeof(buffer1); ++i) + buffer1[i] = i; + for (i = 0; i < sizeof(buffer2); ++i) + buffer2[i] = i; + for (i = 0; i < sizeof(buffer3); ++i) + buffer3[i] = i; + + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer1) + sizeof(buffer2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, sizeof(buffer1) + 2), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer1) + sizeof(buffer2) + 2)); + + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 2, &avail), &buffer1[2]); + ASSERT_NUM_EQUALS(avail, sizeof(buffer1) - 2); + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, sizeof(buffer1) - 1, &avail), &buffer1[sizeof(buffer1) - 1]); + ASSERT_NUM_EQUALS(avail, 1); + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, sizeof(buffer1) + 2, &avail), &buffer2[2]); + ASSERT_NUM_EQUALS(avail, sizeof(buffer2) - 2); + ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, sizeof(buffer1) + sizeof(buffer2) + 2, &avail)); + ASSERT_NUM_EQUALS(avail, 0); + + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 2, buffer3, 1), 1); + ASSERT_TRUE(memcmp(buffer3, &buffer1[2], 1) == 0); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 7, buffer3, 4), 4); + ASSERT_TRUE(memcmp(buffer3, &buffer1[7], 4) == 0); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, sizeof(buffer1) - 2, buffer3, 3), 2); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, sizeof(buffer1) + sizeof(buffer2) + 2, buffer3, 1), 0); +} + +static void test_memory_init_without_regions_system_ram_only() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[16]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer1)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer1) + 2)); +} + +static void test_memory_init_without_regions_save_ram_only() { + rc_libretro_memory_regions_t regions; + unsigned char buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer2) + 2)); +} + +static void test_memory_init_without_regions_no_ram() { + rc_libretro_memory_regions_t regions; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 0); + ASSERT_NUM_EQUALS(regions.total_size, 0); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 2)); +} + +static void test_memory_init_from_unmapped_memory() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0x10000; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_unmapped_memory_null_filler() { + rc_libretro_memory_regions_t regions; + unsigned avail; + unsigned char buffer1[16], buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 4); /* two valid regions and two null fillers */ + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00012)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x1000A)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); + + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 0x00002, &avail), &buffer1[2]); + ASSERT_NUM_EQUALS(avail, sizeof(buffer1) - 2); + ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, 0x00012, &avail)); + ASSERT_NUM_EQUALS(avail, 0); + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 0x10002, &avail), &buffer2[2]); + ASSERT_NUM_EQUALS(avail, sizeof(buffer2) - 2); + ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, 0x1000A, &avail)); + ASSERT_NUM_EQUALS(avail, 0); +} + +static void test_memory_init_from_unmapped_memory_no_save_ram() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[16]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_unmapped_memory_merge_neighbors() { + rc_libretro_memory_regions_t regions; + unsigned char* buffer1 = malloc(0x10000); /* have to malloc to prevent array-bounds compiler warnings */ + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_ATARI_LYNX)); + + ASSERT_NUM_EQUALS(regions.count, 1); /* all regions are adjacent, so should be merged */ + ASSERT_NUM_EQUALS(regions.total_size, 0x10000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer1[0x0002]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer1[0x0102]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0xFFFF), &buffer1[0xFFFF]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10000)); + + free(buffer1); +} + +static void test_memory_init_from_unmapped_memory_no_ram() { + rc_libretro_memory_regions_t regions; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + /* init returns false */ + ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + /* but one null-filled region is still generated */ + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_unmapped_memory_save_ram_first() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x40000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0x8000; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_GAMEBOY_ADVANCE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x48000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer2[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x48002)); +} + +static void test_memory_init_from_memory_map() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_null_filler() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_no_save_ram() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_merge_neighbors() { + rc_libretro_memory_regions_t regions; + unsigned char* buffer1 = malloc(0x10000); /* have to malloc to prevent array-bounds compiler warnings */ + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0x0000], 0, 0x0000U, 0, 0, 0xFC00, "RAM" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0xFC00], 0, 0xFC00U, 0, 0, 0x0400, "Hardware controllers" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_ATARI_LYNX)); + + ASSERT_NUM_EQUALS(regions.count, 1); /* all regions are adjacent, so should be merged */ + ASSERT_NUM_EQUALS(regions.total_size, 0x10000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer1[0x0002]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer1[0x0102]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0xFFFF), &buffer1[0xFFFF]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10000)); + + free(buffer1); +} + +static void test_memory_init_from_memory_map_no_ram() { + rc_libretro_memory_regions_t regions; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, NULL, 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, NULL, 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + /* init returns false */ + ASSERT_FALSE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + /* but one null-filled region is still generated */ + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_splice() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8], buffer3[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x08000, "RAM1" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer2[0], 0, 0xFF8000U, 0, 0, 0x08000, "RAM2" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer3[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 3); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer2[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer3[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_mirrored() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0xFF0000U, 0x00C000U, 0x04000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0x000000U, 0x000000U, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + /* select of 0xFF0000 should mirror the 0x4000 bytes at 0xFF0000 into 0xFF4000, 0xFF8000, and 0xFFC000 */ + ASSERT_NUM_EQUALS(regions.count, 5); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x04002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0C002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_out_of_order() { + rc_libretro_memory_regions_t regions; + unsigned char buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_disconnect_gaps() { + rc_libretro_memory_regions_t regions; + unsigned char buffer[256]; + /* the disconnect bit is smaller than the region size, so only parts of the memory map + * will be filled by the region. in this case, 00-1F will be buffer[00-1F], but + * buffer[20-3F] will be associated to addresses 40-5F! */ + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer[0], 0, 0x0000, 0xFC20, 0x0020, sizeof(buffer), "RAM" }, + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MAGNAVOX_ODYSSEY2)); + + ASSERT_NUM_EQUALS(regions.count, 10); + ASSERT_NUM_EQUALS(regions.total_size, 0x140); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer[0x02]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0022)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0042), &buffer[0x22]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0062)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0082), &buffer[0x42]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00A2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00C2), &buffer[0x62]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00E2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer[0x82]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0122)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0142)); +} + +static void test_hash_set_add_single() { + rc_libretro_hash_set_t hash_set; + const char hash[] = "ABCDEF01234567899876543210ABCDEF"; + + rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.rom")); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + + rc_libretro_hash_set_add(&hash_set, "file.rom", 1234, hash); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 1234); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_update_single() { + rc_libretro_hash_set_t hash_set; + const char hash[] = "ABCDEF01234567899876543210ABCDEF"; + const char hash2[] = "0123456789ABCDEF0123456789ABCDEF"; + + rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.rom")); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + + rc_libretro_hash_set_add(&hash_set, "file.rom", 99, hash); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 99); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 0); + + rc_libretro_hash_set_add(&hash_set, "file.rom", 1234, hash2); + ASSERT_NUM_EQUALS(hash_set.entries_count, 1); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash2); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 1234); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_add_many() { + rc_libretro_hash_set_t hash_set; + const char hash1[] = "ABCDEF01234567899876543210ABCDE1"; + const char hash2[] = "ABCDEF01234567899876543210ABCDE2"; + const char hash3[] = "ABCDEF01234567899876543210ABCDE3"; + const char hash4[] = "ABCDEF01234567899876543210ABCDE4"; + const char hash5[] = "ABCDEF01234567899876543210ABCDE5"; + const char hash6[] = "ABCDEF01234567899876543210ABCDE6"; + const char hash7[] = "ABCDEF01234567899876543210ABCDE7"; + const char hash8[] = "ABCDEF01234567899876543210ABCDE8"; + + rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path); + + rc_libretro_hash_set_add(&hash_set, "file1.rom", 1, hash1); + rc_libretro_hash_set_add(&hash_set, "file2.rom", 2, hash2); + rc_libretro_hash_set_add(&hash_set, "file3.rom", 3, hash3); + rc_libretro_hash_set_add(&hash_set, "file4.rom", 4, hash4); + rc_libretro_hash_set_add(&hash_set, "file5.rom", 5, hash5); + rc_libretro_hash_set_add(&hash_set, "file6.rom", 6, hash6); + rc_libretro_hash_set_add(&hash_set, "file7.rom", 7, hash7); + rc_libretro_hash_set_add(&hash_set, "file8.rom", 8, hash8); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file1.rom"), hash1); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash1), 1); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file2.rom"), hash2); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 2); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file3.rom"), hash3); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash3), 3); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file4.rom"), hash4); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash4), 4); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file5.rom"), hash5); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash5), 5); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file6.rom"), hash6); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash6), 6); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file7.rom"), hash7); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash7), 7); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file8.rom"), hash8); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash8), 8); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_single() { + rc_libretro_hash_set_t hash_set; + const char hash[] = "ABCDEF01234567899876543210ABCDEF"; + const char* m3u_contents = "file.dsk"; + + init_mock_filereader(); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk")); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_savedisk() { + rc_libretro_hash_set_t hash_set; + const char* m3u_contents = "file.dsk\n#SAVEDISK:"; + + init_mock_filereader(); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_savedisk_volume_label() { + rc_libretro_hash_set_t hash_set; + const char* m3u_contents = "file.dsk\n#SAVEDISK:DSAVE"; + + init_mock_filereader(); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_savedisk_multiple_with_comments_and_whitespace() { + rc_libretro_hash_set_t hash_set; + const char* m3u_contents = + "#EXTM3U\n" + "file.dsk\n" /* index 0 */ + "\n" + "#Save disk in the middle, because why not?\n" + "#SAVEDISK:\n" /* index 1 */ + " \r\n" + "\tfile2.dsk|File 2\n" /* index 2 */ + "#SAVEDISK:DSAVE\n" /* index 3 */ + "\t\r\n" + "#LABEL:My Custom Disk Label\n" + "file3.dsk" /* index 4 */ + "\r\n" + "#SAVEDISK:|No Custom Label for Save Disk"; /* index 5 */ + + init_mock_filereader(); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file2.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save3.dsk"), "[SAVE DISK]"); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file3.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save5.dsk"), "[SAVE DISK]"); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static int libretro_get_image_path_no_core_support(unsigned index, char* buffer, size_t buffer_size) +{ + if (index < 0 || index > 1) + return 0; + + snprintf(buffer, buffer_size, "file%d.dsk", index); + return 1; +} + +static void test_hash_set_m3u_savedisk_no_core_support() { + rc_libretro_hash_set_t hash_set; + const char* m3u_contents = "file1.dsk\n#SAVEDISK:\nfile2.dsk"; + + init_mock_filereader(); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path_no_core_support); + + ASSERT_NUM_EQUALS(hash_set.entries_count, 0); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file1.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file2.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file3.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save2.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save3.dsk")); + + rc_libretro_hash_set_destroy(&hash_set); +} + + +void test_rc_libretro(void) { + TEST_SUITE_BEGIN(); + + /* rc_libretro_disallowed_settings */ + TEST_PARAMS3(test_allowed_setting, "bsnes-mercury", "bsnes_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "bsnes-mercury", "bsnes_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "bsnes-mercury", "bsnes_region", "PAL"); + + TEST_PARAMS3(test_allowed_setting, "cap32", "cap32_autorun", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "cap32", "cap32_autorun", "disabled"); + + TEST_PARAMS3(test_allowed_setting, "dolphin-emu", "dolphin_cheats_enabled", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "dolphin-emu", "dolphin_cheats_enabled", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "DuckStation", "duckstation_CDROM.LoadImagePatches", "false"); + TEST_PARAMS3(test_disallowed_setting, "DuckStation", "duckstation_CDROM.LoadImagePatches", "true"); + + TEST_PARAMS3(test_allowed_setting, "ecwolf", "ecwolf-invulnerability", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "ecwolf", "ecwolf-invulnerability", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "PAL"); + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "pal"); /* case insensitive */ + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "Dendy"); + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_palette", "default"); /* setting we don't care about */ + + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-allow-patched-romsets", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-allow-patched-romsets", "enabled"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "disabled"); /* wildcard key match */ + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "enabled"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "0 - Disabled"); /* multi-not value match */ + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "1 - Enabled"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-dipswitch-mslug-BIOS", "MVS Asia/Europe ver. 6 (1 slot)"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-dipswitch-mslug-BIOS", "Universe BIOS ver. 2.3 (alt)"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-neogeo-mode", "DIPSWITCH"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-neogeo-mode", "UNIBIOS"); + + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "action replay (pro)"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "game genie"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "sonic & knuckles"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "NTSC-U"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "PAL"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "NTSC-J"); + + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "action replay (pro)"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "game genie"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "sonic & knuckles"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "NTSC-U"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "PAL"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "NTSC-J"); + + TEST_PARAMS3(test_allowed_setting, "Mesen", "mesen_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Mesen", "mesen_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "Mesen", "mesen_region", "PAL"); + TEST_PARAMS3(test_disallowed_setting, "Mesen", "mesen_region", "Dendy"); + + TEST_PARAMS3(test_allowed_setting, "Mesen-S", "mesen-s_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Mesen-S", "mesen-s_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "Mesen-S", "mesen-s_region", "PAL"); + + TEST_PARAMS3(test_allowed_setting, "PPSSPP", "ppsspp_cheats", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "PPSSPP", "ppsspp_cheats", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "PAL"); + + TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "US"); + TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "Japan NTSC"); + TEST_PARAMS3(test_disallowed_setting, "PicoDrive", "picodrive_region", "Europe"); + TEST_PARAMS3(test_disallowed_setting, "PicoDrive", "picodrive_region", "Japan PAL"); + + TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "16"); + TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "8"); + TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "4"); + TEST_PARAMS3(test_disallowed_setting, "QUASI88", "q88_cpu_clock", "2"); + TEST_PARAMS3(test_disallowed_setting, "QUASI88", "q88_cpu_clock", "1"); + + TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "auto"); + TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "ntsc-u"); + TEST_PARAMS3(test_disallowed_setting, "SMS Plus GX", "smsplus_region", "pal"); + TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "ntsc-j"); + + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_region", "PAL"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_gfx_clip", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_gfx_clip", "disabled"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_gfx_transp", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_gfx_transp", "disabled"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_layer_1", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_layer_1", "disabled"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_layer_5", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_layer_5", "disabled"); + + TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_autostart", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_autostart", "disabled"); + TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_autostart", "warp"); + TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_reset", "autostart"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "soft"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "hard"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "freeze"); + + TEST_PARAMS3(test_allowed_setting, "Virtual Jaguar", "virtualjaguar_pal", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "Virtual Jaguar", "virtualjaguar_pal", "enabled"); + + /* rc_libretro_is_system_allowed */ + TEST_PARAMS2(test_allowed_system, "FCEUmm", RC_CONSOLE_NINTENDO); + + TEST_PARAMS2(test_allowed_system, "Mesen-S", RC_CONSOLE_SUPER_NINTENDO); + TEST_PARAMS2(test_disallowed_system, "Mesen-S", RC_CONSOLE_GAMEBOY); + TEST_PARAMS2(test_disallowed_system, "Mesen-S", RC_CONSOLE_GAMEBOY_COLOR); + + /* rc_libretro_memory_init */ + TEST(test_memory_init_without_regions); + TEST(test_memory_init_without_regions_system_ram_only); + TEST(test_memory_init_without_regions_save_ram_only); + TEST(test_memory_init_without_regions_no_ram); + + TEST(test_memory_init_from_unmapped_memory); + TEST(test_memory_init_from_unmapped_memory_null_filler); + TEST(test_memory_init_from_unmapped_memory_no_save_ram); + TEST(test_memory_init_from_unmapped_memory_merge_neighbors); + TEST(test_memory_init_from_unmapped_memory_no_ram); + TEST(test_memory_init_from_unmapped_memory_save_ram_first); + + TEST(test_memory_init_from_memory_map); + TEST(test_memory_init_from_memory_map_null_filler); + TEST(test_memory_init_from_memory_map_no_save_ram); + TEST(test_memory_init_from_memory_map_merge_neighbors); + TEST(test_memory_init_from_memory_map_no_ram); + TEST(test_memory_init_from_memory_map_splice); + TEST(test_memory_init_from_memory_map_mirrored); + TEST(test_memory_init_from_memory_map_out_of_order); + TEST(test_memory_init_from_memory_map_disconnect_gaps); + + /* rc_libretro_hash_set_t */ + TEST(test_hash_set_add_single); + TEST(test_hash_set_update_single); + TEST(test_hash_set_add_many); + + TEST(test_hash_set_m3u_single); + TEST(test_hash_set_m3u_savedisk); + TEST(test_hash_set_m3u_savedisk_volume_label); + TEST(test_hash_set_m3u_savedisk_multiple_with_comments_and_whitespace); + TEST(test_hash_set_m3u_savedisk_no_core_support); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_rc_validate.c b/src/rcheevos/test/rcheevos/test_rc_validate.c new file mode 100644 index 000000000..d09ba88ac --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_rc_validate.c @@ -0,0 +1,369 @@ +#include "rc_validate.h" + +#include "rc_compat.h" +#include "rc_consoles.h" + +#include "../test_framework.h" + +#include +#include + +int validate_trigger(const char* trigger, char result[], const size_t result_size, unsigned max_address) { + char* buffer; + rc_trigger_t* compiled; + int success = 0; + + int ret = rc_trigger_size(trigger); + if (ret < 0) { + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_trigger(buffer, trigger, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else if (rc_validate_trigger(compiled, result, result_size, max_address)) { + success = 1; + } + + free(buffer); + return success; +} + +static void test_validate_trigger_max_address(const char* trigger, const char* expected_error, unsigned max_address) { + char buffer[512]; + int valid = validate_trigger(trigger, buffer, sizeof(buffer), max_address); + + if (*expected_error) { + ASSERT_STR_EQUALS(buffer, expected_error); + ASSERT_NUM_EQUALS(valid, 0); + } + else { + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_NUM_EQUALS(valid, 1); + } +} + +static void test_validate_trigger(const char* trigger, const char* expected_error) { + test_validate_trigger_max_address(trigger, expected_error, 0xFFFFFFFF); +} + +static void test_validate_trigger_64k(const char* trigger, const char* expected_error) { + test_validate_trigger_max_address(trigger, expected_error, 0xFFFF); +} + +static void test_validate_trigger_128k(const char* trigger, const char* expected_error) { + test_validate_trigger_max_address(trigger, expected_error, 0x1FFFF); +} + +int validate_trigger_for_console(const char* trigger, char result[], const size_t result_size, int console_id) { + char* buffer; + rc_trigger_t* compiled; + int success = 0; + + int ret = rc_trigger_size(trigger); + if (ret < 0) { + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_trigger(buffer, trigger, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { + success = 1; + } + + free(buffer); + return success; +} + +static void test_validate_trigger_console(const char* trigger, const char* expected_error, int console_id) { + char buffer[512]; + int valid = validate_trigger_for_console(trigger, buffer, sizeof(buffer), console_id); + + if (*expected_error) { + ASSERT_STR_EQUALS(buffer, expected_error); + ASSERT_NUM_EQUALS(valid, 0); + } + else { + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_NUM_EQUALS(valid, 1); + } +} + +static void test_combining_conditions_at_end_of_definition() { + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_A:0xH2345=2", "Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_B:0xH2345=2", "Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_C:0xH2345=2", "Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_D:0xH2345=2", "Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_N:0xH2345=2", "Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_O:0xH2345=2", "Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_Z:0xH2345=2", "Final condition type expects another condition to follow"); + + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_A:0xH2345=2S0x3456=1", "Core Final condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0x3456=1S0xH1234=1_A:0xH2345=2", "Alt1 Final condition type expects another condition to follow"); + + /* combining conditions not at end of definition */ + TEST_PARAMS2(test_validate_trigger, "A:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "B:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "N:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "O:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "Z:0xH1234=1_0xH2345=2", ""); +} + +static void test_addhits_chain_without_target() { + TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_0xH2345=2", "Condition 2: Final condition in AddHits chain must have a hit target"); + TEST_PARAMS2(test_validate_trigger, "D:0xH1234=1_0xH2345=2", "Condition 2: Final condition in AddHits chain must have a hit target"); + TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_0xH2345=2.1.", ""); + TEST_PARAMS2(test_validate_trigger, "D:0xH1234=1_0xH2345=2.1.", ""); +} + +static void test_range_comparisons() { + TEST_PARAMS2(test_validate_trigger, "0xH1234>1", ""); + + TEST_PARAMS2(test_validate_trigger, "0xH1234=255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234!=255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234>255", "Condition 1: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234>=255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234<255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234<=255", "Condition 1: Comparison is always true"); + + /* while a BCD value shouldn't exceed 99, it can reach 165: 0xFF => 15*10+15 */ + TEST_PARAMS2(test_validate_trigger, "b0xH1234<165", ""); + TEST_PARAMS2(test_validate_trigger, "b0xH1234<=165", "Condition 1: Comparison is always true"); + + TEST_PARAMS2(test_validate_trigger, "R:0xH1234<255", ""); + TEST_PARAMS2(test_validate_trigger, "R:0xH1234<=255", "Condition 1: Comparison is always true"); + + TEST_PARAMS2(test_validate_trigger, "0xH1234=256", "Condition 1: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234!=256", "Condition 1: Comparison is always true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234>256", "Condition 1: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234>=256", "Condition 1: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234<256", "Condition 1: Comparison is always true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234<=256", "Condition 1: Comparison is always true"); + + TEST_PARAMS2(test_validate_trigger, "0x 1234>=65535", ""); + TEST_PARAMS2(test_validate_trigger, "0x 1234>=65536", "Condition 1: Comparison is never true"); + + TEST_PARAMS2(test_validate_trigger, "b0x 1234>=16665", ""); + TEST_PARAMS2(test_validate_trigger, "b0x 1234>=16666", "Condition 1: Comparison is never true"); + + TEST_PARAMS2(test_validate_trigger, "0xW1234>=16777215", ""); + TEST_PARAMS2(test_validate_trigger, "0xW1234>=16777216", "Condition 1: Comparison is never true"); + + TEST_PARAMS2(test_validate_trigger, "b0xW1234>=1666665", ""); + TEST_PARAMS2(test_validate_trigger, "b0xW1234>=1666666", "Condition 1: Comparison is never true"); + + TEST_PARAMS2(test_validate_trigger, "0xX1234>=4294967295", ""); + TEST_PARAMS2(test_validate_trigger, "0xX1234>4294967295", "Condition 1: Comparison is never true"); + + TEST_PARAMS2(test_validate_trigger, "b0xX1234>=166666665", ""); + TEST_PARAMS2(test_validate_trigger, "b0xX1234>=166666666", "Condition 1: Comparison is never true"); + + TEST_PARAMS2(test_validate_trigger, "0xT1234>=1", ""); + TEST_PARAMS2(test_validate_trigger, "0xT1234>1", "Condition 1: Comparison is never true"); + + /* max for AddSource is the sum of all parts (255+255=510) */ + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0<255", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0<=255", "Condition 2: Comparison is always true"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235<510", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235<=510", "Condition 2: Comparison is always true"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234*10_0xH1235>=2805", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234*10_0xH1235>2805", "Condition 2: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234/10_0xH1235>=280", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234/10_0xH1235>280", "Condition 2: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234&10_0xH1235>=265", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234&10_0xH1235>265", "Condition 2: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "A:b0xH1234*100_b0xH1235>=16665", ""); + TEST_PARAMS2(test_validate_trigger, "A:b0xH1234*100_b0xH1235>16665", "Condition 2: Comparison is never true"); + + /* max for SubSource is always 0xFFFFFFFF */ + TEST_PARAMS2(test_validate_trigger, "B:0xH1234_0xH1235<510", ""); + TEST_PARAMS2(test_validate_trigger, "B:0xH1234_0xH1235<=510", ""); +} + +void test_size_comparisons() { + TEST_PARAMS2(test_validate_trigger, "0xH1234>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234>0x 1235", "Condition 1: Comparing different memory sizes"); + + /* AddSource chain may compare different sizes without warning as the chain changes the + * size of the final result. */ + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235=0xH2345", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235=0x 2345", ""); +} + +void test_address_range() { + /* basic checks for each side */ + TEST_PARAMS2(test_validate_trigger_64k, "0xH1234>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger_64k, "0xH12345>0xH1235", "Condition 1: Address 12345 out of range (max FFFF)"); + TEST_PARAMS2(test_validate_trigger_64k, "0xH1234>0xH12345", "Condition 1: Address 12345 out of range (max FFFF)"); + TEST_PARAMS2(test_validate_trigger_64k, "0xH12345>0xH12345", "Condition 1: Address 12345 out of range (max FFFF)"); + TEST_PARAMS2(test_validate_trigger_64k, "0xX1234>h12345", ""); + + /* support for multiple memory blocks and edge addresses */ + TEST_PARAMS2(test_validate_trigger_128k, "0xH1234>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH12345>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH0000>5", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH1FFFF>5", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH20000>5", "Condition 1: Address 20000 out of range (max 1FFFF)"); + + /* AddAddress can use really big values for negative offsets, don't flag them. */ + TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xHFFFFFF00>5", ""); + TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xH1234>5_0xHFFFFFF00>5", "Condition 3: Address FFFFFF00 out of range (max 1FFFF)"); + + /* console-specific warnings */ + TEST_PARAMS3(test_validate_trigger_console, "0xH0123>23", "", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH07FF>23", "", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH0800>23", "Condition 1: Mirror RAM may not be exposed by emulator (address 0800)", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH1FFF>23", "Condition 1: Mirror RAM may not be exposed by emulator (address 1FFF)", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH2000>23", "", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH0123>0xH1000", "Condition 1: Mirror RAM may not be exposed by emulator (address 1000)", RC_CONSOLE_NINTENDO); + + TEST_PARAMS3(test_validate_trigger_console, "0xHC123>23", "", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHDFFF>23", "", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHE000>23", "Condition 1: Echo RAM may not be exposed by emulator (address E000)", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHFDFF>23", "Condition 1: Echo RAM may not be exposed by emulator (address FDFF)", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHFE00>23", "", RC_CONSOLE_GAMEBOY); + + TEST_PARAMS3(test_validate_trigger_console, "0xHC123>23", "", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHDFFF>23", "", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHE000>23", "Condition 1: Echo RAM may not be exposed by emulator (address E000)", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHFDFF>23", "Condition 1: Echo RAM may not be exposed by emulator (address FDFF)", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHFE00>23", "", RC_CONSOLE_GAMEBOY_COLOR); + + TEST_PARAMS3(test_validate_trigger_console, "0xH9E20=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address 9E20)", RC_CONSOLE_PLAYSTATION); + TEST_PARAMS3(test_validate_trigger_console, "0xHB8BE=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address B8BE)", RC_CONSOLE_PLAYSTATION); + TEST_PARAMS3(test_validate_trigger_console, "0xHFFFF=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address FFFF)", RC_CONSOLE_PLAYSTATION); + TEST_PARAMS3(test_validate_trigger_console, "0xH10000=68", "", RC_CONSOLE_PLAYSTATION); +} + +void test_delta_pointers() { + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_0xH0000=1", ""); + TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:p0xX1234_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_d0xH0000=1", ""); + TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_I:d0xH0010_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_I:0xH0010_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_I:d0xH0010_0xH0000=1", "Condition 2: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_I:0xH0010_0xH0000=1", ""); +} + +void test_float_comparisons() { + TEST_PARAMS2(test_validate_trigger, "fF1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fM1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fF1234=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xX1234=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xX1234=f2.3", "Condition 1: Comparison is never true"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "0xX1234!=f2.3", "Condition 1: Comparison is never true"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "0xX1234f2.3", "Condition 1: Comparison is never true"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "0xX1234>=f2.3", "Condition 1: Comparison is never true"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "0xX1234=f2.0", ""); /* float can be converted to int without loss of data*/ + TEST_PARAMS2(test_validate_trigger, "0xH1234=f2.3", "Condition 1: Comparison is never true"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=f300.0", "Condition 1: Comparison is never true"); /* value out of range */ + TEST_PARAMS2(test_validate_trigger, "f2.3=fF1234", ""); + TEST_PARAMS2(test_validate_trigger, "f2.3=0xX1234", "Condition 1: Comparison is never true"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "f2.0=0xX1234", ""); + TEST_PARAMS2(test_validate_trigger, "A:Ff2345_fF1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xX2345_fF1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "A:Ff2345_0x1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fM1234>f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fM1234>f-2.3", ""); + TEST_PARAMS2(test_validate_trigger, "I:0xX2345_fM1234>f1.0", ""); + TEST_PARAMS2(test_validate_trigger, "I:0xX2345_fM1234>f-1.0", ""); +} + +void test_conflicting_conditions() { + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_0xH0000=2", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000>5", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000=5", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0001=5", ""); /* ignore differing address */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0x 0000=5", ""); /* ignore differing size */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_d0xH0000=5", ""); /* ignore differing type */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000=5.1.", ""); /* ignore anything with a hit target */ + TEST_PARAMS2(test_validate_trigger, "O:0xH0000<5_0xH0000=5", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "A:0xH0000<5_0xH0000=5", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "N:0xH0000<5_R:0xH0001=8_T:0xH0000=0", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<=5_0xH0000>=5", ""); + TEST_PARAMS2(test_validate_trigger, "0xH0000>1_0xH0000<3", ""); + TEST_PARAMS2(test_validate_trigger, "1=1S0xH0000=1S0xH0000=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=2", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1S0xH0000=1", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1SR:0xH0000=1", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_0xH0000=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1S0xH0000=1", ""); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SP:0xH0000!=1", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_P:0xH0000!=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1SP:0xH0000=5", ""); + TEST_PARAMS2(test_validate_trigger, "M:0xH0000=5_Q:0xH0000=255", "Condition 2: Conflicts with Condition 1"); + + /* PauseIf prevents hits from incrementing. ResetIf clears all hits. If both exist and are conflicting, the group + * will only ever be paused or reset, and therefore will never be true */ + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000!=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=1_P:0xH0000=1", "Condition 2: Conflicts with Condition 1"); + /* if the PauseIf is less restrictive than the ResetIf, it's just a guard. ignore it*/ + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000!=6", ""); + /* PauseIf in alternate group does not affect the ResetIf*/ + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SR:0xH0000!=1", ""); +} + +void test_redundant_conditions() { + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_0xH0000=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<3_0xH0000<5", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000<3", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=1", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1_0xH0000!=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1_0xH0000=2", ""); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1_T:0xH0000=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_R:0xH0000!=1", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1S0xH0000!=1", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1SR:0xH0000!=1", "Core Condition 1: Redundant with Alt1 Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SP:0xH0000=1", ""); /* same pauseif can appear in different groups */ + TEST_PARAMS2(test_validate_trigger, "0xH0000=4.1._0xH0000=5_P:0xH0000<4", ""); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=5_Q:0xH0000!=255", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "M:0xH0000=5_Q:0xH0000!=255", ""); /* measuredif not redundant measured */ + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_0xH0000=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_Q:0xH0000=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1SQ:0xH0000=1", ""); /* same measuredif can appear in different groups */ + TEST_PARAMS2(test_validate_trigger, "T:0xH0000!=0_T:0xH0000=6", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000!=0_T:0xH0000=6", ""); /* trigger not redundant with non-trigger */ + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_Q:0xH0000=1", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000!=0S0xH0001=2", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000!=0S0xH0000=1S0xH0001=2", ""); /* more restrictive alt 1 is not redundant with core */ + TEST_PARAMS2(test_validate_trigger, "0xH0000!=0S0xH0000!=0S0xH0001=2", "Alt1 Condition 1: Redundant with Core Condition 1"); +} + +void test_rc_validate(void) { + TEST_SUITE_BEGIN(); + + /* positive baseline test cases */ + TEST_PARAMS2(test_validate_trigger, "", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_0xH2345=2S0xH3456=1S0xH3456=2", ""); + + test_combining_conditions_at_end_of_definition(); + test_addhits_chain_without_target(); + test_range_comparisons(); + test_size_comparisons(); + test_address_range(); + test_delta_pointers(); + test_float_comparisons(); + test_conflicting_conditions(); + test_redundant_conditions(); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_richpresence.c b/src/rcheevos/test/rcheevos/test_richpresence.c new file mode 100644 index 000000000..759a8544b --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_richpresence.c @@ -0,0 +1,1344 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +#include "../src/rcheevos/rc_compat.h" + +static void _assert_parse_richpresence(rc_richpresence_t** richpresence, void* buffer, const char* script) { + int size; + unsigned* overflow; + *richpresence = NULL; + + size = rc_richpresence_size(script); + ASSERT_NUM_GREATER(size, 0); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + *richpresence = rc_parse_richpresence(buffer, script, NULL, 0); + ASSERT_PTR_NOT_NULL(*richpresence); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_parse_richpresence(richpresence_out, buffer, script) ASSERT_HELPER(_assert_parse_richpresence(richpresence_out, buffer, script), "assert_parse_richpresence") + +static void _assert_richpresence_output(rc_richpresence_t* richpresence, memory_t* memory, const char* expected_display_string) { + char output[256]; + int result; + + result = rc_evaluate_richpresence(richpresence, output, sizeof(output), peek, memory, NULL); + ASSERT_STR_EQUALS(output, expected_display_string); + ASSERT_NUM_EQUALS(result, strlen(expected_display_string)); +} +#define assert_richpresence_output(richpresence, memory, expected_display_string) ASSERT_HELPER(_assert_richpresence_output(richpresence, memory, expected_display_string), "assert_richpresence_output") + +static void test_empty_script() { + int lines; + int result = rc_richpresence_size_lines("", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 1); +} + +static void test_simple_richpresence(const char* script, const char* expected_display_string) { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected_display_string); +} + +static void assert_buffer_boundary(rc_richpresence_t* richpresence, memory_t* memory, int buffersize, int expected_result, const char* expected_display_string) { + char output[256]; + int result; + unsigned* overflow = (unsigned*)(&output[buffersize]); + *overflow = 0xCDCDCDCD; + + result = rc_evaluate_richpresence(richpresence, output, buffersize, peek, memory, NULL); + ASSERT_NUM_EQUALS(result, expected_result); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + ASSERT_STR_EQUALS(output, expected_display_string); +} + +static void test_buffer_boundary() { + unsigned char ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* static strings */ + assert_parse_richpresence(&richpresence, &buffer[32], "Display:\nABCDEFGH"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "ABCDEF"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "ABCDEFG"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "ABCDEFGH"); /* all 8 chars written */ + + /* number formatting */ + assert_parse_richpresence(&richpresence, &buffer[32], "Format:V\nFormatType=VALUE\n\nDisplay:\n@V(0xX0000)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "167772"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "1677721"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "16777216"); /* all 8 chars written */ + + /* lookup */ + assert_parse_richpresence(&richpresence, &buffer[32], "Lookup:L\n1=ABCDEFGH\n\nDisplay:\n@L(0xH0003)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "ABCDEF"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "ABCDEFG"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "ABCDEFGH"); /* all 8 chars written */ + + /* unknown macro - "[Unknown macro]L(0xH0003)" = 25 chars */ + assert_parse_richpresence(&richpresence, &buffer[32], "Display:\n@L(0xH0003)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 25, "[Unkno"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 25, 25, "[Unknown macro]L(0xH0003"); /* only 24 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 26, 25, "[Unknown macro]L(0xH0003)"); /* all 25 chars written */ + + /* multipart */ + assert_parse_richpresence(&richpresence, &buffer[32], "Lookup:L\n0=\n1=A\n4=ABCD\n8=ABCDEFGH\n\nFormat:V\nFormatType=VALUE\n\nDisplay:\n@L(0xH0000)--@L(0xH0001)--@V(0xH0002)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 5, "----0"); /* initial value fits */ + ram[1] = 4; + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 9, "--ABCD-"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 9, "--ABCD--"); /* only 8 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 9, "--ABCD--0"); /* all 9 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 5, 9, "--AB"); /* only 7 chars written */ + ram[2] = 123; + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 11, "--ABCD--1"); /* only 9 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 11, 11, "--ABCD--12"); /* only 10 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 12, 11, "--ABCD--123"); /* all 11 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 2, 11, "-"); /* only 1 char written */ +} + +static void test_conditional_display_simple() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?Zero\n?0xH0000=1?One\nOther"); + assert_richpresence_output(richpresence, &memory, "Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_conditional_display_after_default() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\nOther\n?0xH0000=0?Zero\n?0xH0000=1?One"); + assert_richpresence_output(richpresence, &memory, "Other"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_conditional_display_no_default() { + int lines; + int result = rc_richpresence_size_lines("Display:\n?0xH0000=0?Zero", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 3); +} + +static void test_conditional_display_common_condition() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* condition for Second is a sub-clause of First */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0_0xH0001=18?First\n?0xH0000=0?Second\nThird"); + assert_richpresence_output(richpresence, &memory, "First"); + + /* secondary part of first condition is false, will match second condition */ + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "Second"); + + /* common condition is false, will use default */ + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Third"); + + /* ================================================================ */ + /* == reverse the conditions so the First is a sub-clause of Second */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?First\n?0xH0000=0_0xH0001=18?Second\nThird"); + + /* reset the memory so it matches the first test, First clause will be matched before even looking at Second */ + ram[0] = 0; + ram[1] = 18; + assert_richpresence_output(richpresence, &memory, "First"); + + /* secondary part of second condition is false, will still match first condition */ + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "First"); + + /* common condition is false, will use default */ + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Third"); +} + +static void test_conditional_display_duplicated_condition() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?First\n?0xH0000=0?Second\nThird"); + assert_richpresence_output(richpresence, &memory, "First"); + + /* cannot activate Second */ + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Third"); +} + +static void test_conditional_display_invalid_condition_logic() { + int lines; + int result = rc_richpresence_size_lines("Display:\n?BANANA?Zero\nDefault", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); +} + +static void test_conditional_display_whitespace_text() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0? \n?0xH0000=1?One\nOther"); + assert_richpresence_output(richpresence, &memory, " "); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_macro_value() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "13330 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "13332 Points"); +} + +static void test_macro_value_nibble() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* nibble first, see if byte overwrites */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xL0001)@Points(0xH0001) Points"); + assert_richpresence_output(richpresence, &memory, "218 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "420 Points"); + + /* put byte first, see if nibble overwrites */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH0001)@Points(0xL0001) Points"); + assert_richpresence_output(richpresence, &memory, "204 Points"); +} + +static void test_macro_value_bcd() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(b0xH0001) Points"); + assert_richpresence_output(richpresence, &memory, "12 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "14 Points"); +} + +static void test_macro_value_bitcount() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Bits\nFormatType=VALUE\n\nDisplay:\n@Bits(0xK0001) Bits"); + assert_richpresence_output(richpresence, &memory, "2 Bits"); + + ram[1] = 0x76; + assert_richpresence_output(richpresence, &memory, "5 Bits"); +} + +static void test_conditional_display_indirect() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?I:0xH0000_0xH0002=h01?True\nFalse\n"); + assert_richpresence_output(richpresence, &memory, "False"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "False"); + + ram[3] = 1; + assert_richpresence_output(richpresence, &memory, "True"); +} + +static void test_conditional_display_unnecessary_measured() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?M:0xH0000=0?Zero\n?0xH0000=1?One\nOther"); + assert_richpresence_output(richpresence, &memory, "Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_conditional_display_unnecessary_measured_indirect() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?I:0xH0000_M:0xH0002=h01?True\nFalse\n"); + assert_richpresence_output(richpresence, &memory, "False"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "False"); + + ram[3] = 1; + assert_richpresence_output(richpresence, &memory, "True"); +} + +static void test_macro_value_adjusted_negative() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001_V-10000) Points"); + assert_richpresence_output(richpresence, &memory, "3330 Points"); + + ram[2] = 7; + assert_richpresence_output(richpresence, &memory, "-8190 Points"); +} + +static void test_macro_value_from_formula() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH0001*100_0xH0002) Points"); + assert_richpresence_output(richpresence, &memory, "1852 Points"); + + ram[1] = 32; + assert_richpresence_output(richpresence, &memory, "3252 Points"); +} + +static void test_macro_value_from_hits() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Hits\nFormatType=VALUE\n\nDisplay:\n@Hits(M:0xH01=1) Hits"); + assert_richpresence_output(richpresence, &memory, "0 Hits"); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "1 Hits"); + assert_richpresence_output(richpresence, &memory, "2 Hits"); + assert_richpresence_output(richpresence, &memory, "3 Hits"); +} + +static void test_macro_value_from_indirect() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nPointing at @Value(I:0xH00_M:0xH01)"); + assert_richpresence_output(richpresence, &memory, "Pointing at 18"); + + /* pointed at data changes */ + ram[1] = 99; + assert_richpresence_output(richpresence, &memory, "Pointing at 99"); + + /* pointer changes */ + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Pointing at 52"); +} + +static void test_macro_value_divide_by_zero() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(0xH02/0xH00)"); + assert_richpresence_output(richpresence, &memory, "Result is 0"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Result is 52"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Result is 26"); +} + +static void test_macro_value_divide_by_self() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* sneaky trick to turn any non-zero value into 1 */ + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(0xH02/0xH02)"); + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 1; + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 32; + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 255; + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 0; + assert_richpresence_output(richpresence, &memory, "Result is 0"); +} + +static void test_macro_frames() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Frames\nFormatType=FRAMES\n\nDisplay:\n@Frames(0x 0001)"); + assert_richpresence_output(richpresence, &memory, "3:42.16"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "3:42.20"); +} + +static void test_macro_float(const char* format, unsigned value, const char* expected) { + unsigned char ram[4]; + memory_t memory; + rc_richpresence_t* richpresence; + char script[128]; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + ram[0] = (value & 0xFF); + ram[1] = (value >> 8) & 0xFF; + ram[2] = (value >> 16) & 0xFF; + ram[3] = (value >> 24) & 0xFF; + + snprintf(script, sizeof(script), "Format:N\nFormatType=%s\n\nDisplay:\n@N(fF0000)", format); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_macro_lookup_simple() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_with_inline_comment() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n// Zero\n0=Zero\n// One\n1=One\n//2=Two\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_hex_keys() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0x00=Zero\n0x01=One\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_default() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n*=Star\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At Star"); +} + +static void test_macro_lookup_crlf() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\r\n0=Zero\r\n1=One\r\n\r\nDisplay:\r\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_after_display() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\nAt @Location(0xH0000)\n\nLookup:Location\n0=Zero\n1=One"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_from_formula() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000*0.5)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At One"); +} + +static void test_macro_lookup_from_indirect() { + unsigned char ram[] = { 0x00, 0x00, 0x01, 0x00, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(I:0xH0000=0_M:0xH0001)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At Zero"); +} + +static void test_macro_lookup_repeated() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000), Near @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At , Near "); +} + +static void test_macro_lookup_shared() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for multiple addresses */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000), Near @Location(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near "); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "At Zero, Near One"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near One"); +} + +static void test_macro_lookup_multiple() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple lookups can be used for same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nLookup:Location2\n0=zero\n1=one\n\nDisplay:\nAt @Location(0xH0000), Near @Location2(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near one"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At , Near "); +} + +static void test_macro_lookup_and_value() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nFormat:Location2\nFormatType=VALUE\n\nDisplay:\nAt @Location(0xH0000), Near @Location2(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near 18"); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "At Zero, Near 1"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near 1"); +} + +static void test_macro_lookup_negative_value() { + unsigned char ram[] = { 0x00, 0x00, 0x00, 0x00, 0x00 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* lookup keys are signed 32-bit values. the -1 will become 0xFFFFFFFF */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Diff\n0=Zero\n1=One\n-1=Negative One\n\nDisplay:\nDiff=@Diff(B:0xH0000_M:0xH0001)"); + assert_richpresence_output(richpresence, &memory, "Diff=Zero"); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "Diff=One"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Diff=Zero"); + + ram[1] = 0; + assert_richpresence_output(richpresence, &memory, "Diff=Negative One"); +} + +static void test_macro_lookup_value_with_whitespace() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0= Zero \n1= One \n\nDisplay:\nAt '@Location(0xH0000)' "); + assert_richpresence_output(richpresence, &memory, "At ' Zero ' "); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At ' One ' "); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At '' "); +} + +static void test_macro_lookup_mapping_repeated() { + unsigned char ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:OddOrEven\n0=Even\n1=Odd\n2=Even\n3=Odd\n4=Even\n5=Odd\n\nDisplay:\nFirst:@OddOrEven(0xH0000), Second:@OddOrEven(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Even"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:Odd, Second:Even"); + + ram[0] = 2; + ram[1] = 3; + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Odd"); +} + +static void test_macro_lookup_mapping_repeated_csv() { + unsigned char ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:OddOrEven\n0,2,4=Even\n1,3,5=Odd\n\nDisplay:\nFirst:@OddOrEven(0xH0000), Second:@OddOrEven(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Even"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:Odd, Second:Even"); + + ram[0] = 2; + ram[1] = 3; + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Odd"); +} + +static void test_macro_lookup_mapping_merged() { + unsigned char ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Place\n0=First\n1=First\n2=First\n3=Second\n4=Second\n5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 5; + ram[1] = 2; + assert_richpresence_output(richpresence, &memory, "First:Second, Second:First"); + + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->first, 0); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->last, 2); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->first, 3); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->last, 5); + ASSERT_PTR_NULL(richpresence->first_lookup->root->right->right); +} + +static void test_macro_lookup_mapping_range() { + unsigned char ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Place\n0-2=First\n5,3-4=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 5; + ram[1] = 2; + assert_richpresence_output(richpresence, &memory, "First:Second, Second:First"); + + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->first, 0); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->last, 2); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->first, 3); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->last, 5); + ASSERT_PTR_NULL(richpresence->first_lookup->root->right->right); +} + +static void test_macro_lookup_invalid() { + int result; + int lines; + + /* lookup value starts with Ox instead of 0x */ + result = rc_richpresence_size_lines("Lookup:Location\nOx0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); + + /* lookup value contains invalid hex character */ + result = rc_richpresence_size_lines("Lookup:Location\n0xO=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); + + /* lookup value is not numeric */ + result = rc_richpresence_size_lines("Lookup:Location\nZero=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); +} + +static void test_macro_escaped() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ensures @ can be used in the display string by escaping it */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n\\@Points(0x 0001) \\@@Points(0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "@Points(0x 0001) @13330 Points"); +} + +static void test_macro_undefined() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "[Unknown macro]Points(0x 0001) Points"); +} + +static void test_macro_undefined_at_end_of_line() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* adding [Unknown macro] to the output effectively makes the script larger than it started. + * since we don't detect unknown macros in `rc_richpresence_size`, this was causing a + * write-past-end-of-buffer memory corruption error. this test recreated that error. */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001)"); + assert_richpresence_output(richpresence, &memory, "[Unknown macro]Points(0x 0001)"); +} + +static void test_macro_unterminated() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* valid macro with no closing parenthesis should just be dumped as-is */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001"); + assert_richpresence_output(richpresence, &memory, "@Points(0x 0001"); + + /* adding [Unknown macro] to the output effectively makes the script larger than it started. + * since we don't detect unknown macros in `rc_richpresence_size`, this was causing a + * write-past-end-of-buffer memory corruption error. this test recreated that error. */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001"); + assert_richpresence_output(richpresence, &memory, "@Points(0x 0001"); +} + +static void test_macro_without_parameter() { + int result; + int lines; + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points Points", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(lines, 5); + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points() Points", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_macro_without_parameter_conditional_display() { + int result; + int lines; + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n?0x0h0001=1?@Points Points\nDefault", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(lines, 5); + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n?0x0h0001=1?@Points() Points\nDefault", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_macro_non_numeric_parameter() { + int lines; + int result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(Zero) Points", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_builtin_macro(const char* macro, const char* expected) { + unsigned char ram[] = { 0x39, 0x30 }; + memory_t memory; + rc_richpresence_t* richpresence; + char script[128]; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + snprintf(script, sizeof(script), "Display:\n@%s(0x 0)", macro); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_builtin_macro_float(const char* macro, const char* expected) { + unsigned char ram[] = { 0x92, 0x44, 0x9A, 0x42 }; /* 77.133926 */ + memory_t memory; + rc_richpresence_t* richpresence; + char script[128]; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + snprintf(script, sizeof(script), "Display:\n@%s(fF0000)", macro); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_builtin_macro_override() { + unsigned char ram[] = { 0x39, 0x30 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Number\nFormatType=SECS\n\nDisplay:\n@Number(0x 0)"); + assert_richpresence_output(richpresence, &memory, "3h25:45"); +} + +static void test_asciichar() { + unsigned char ram[] = { 'K', 'e', 'n', '\0', 'V', 'e', 'g', 'a', 1 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Round\n0= (Round 1)\n1= (Round 2)\n\n" + "Display:\n@ASCIIChar(0xH0)@ASCIIChar(0xH1)@ASCIIChar(0xH2)@ASCIIChar(0xH3) vs @ASCIIChar(0xH4)@ASCIIChar(0xH5)@ASCIIChar(0xH6)@ASCIIChar(0xH7)@Round(0xH8)"); + assert_richpresence_output(richpresence, &memory, "Ken vs Vega (Round 2)"); + + ram[0] = 'R'; ram[1] = 'o'; ram[2] = 's'; ram[3] = 'e'; + ram[4] = 'K'; ram[5] = 'e'; ram[6] = 'n'; ram[7] = '\0'; + ram[8] = 0; + assert_richpresence_output(richpresence, &memory, "Rose vs Ken (Round 1)"); +} + +static void test_ascii8(unsigned char c1, unsigned char c2, unsigned char c3, unsigned char c4, + unsigned char c5, unsigned char c6, unsigned char c7, unsigned char c8, char* expected) { + unsigned char ram[9]; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + ram[0] = c1; ram[1] = c2; ram[2] = c3; ram[3] = c4; + ram[4] = c5; ram[5] = c6; ram[6] = c7; ram[7] = c8; + ram[8] = '~'; + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@ASCIIChar(0xH0)@ASCIIChar(0xH1)@ASCIIChar(0xH2)@ASCIIChar(0xH3)@ASCIIChar(0xH4)@ASCIIChar(0xH5)@ASCIIChar(0xH6)@ASCIIChar(0xH7)"); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_unicode4(unsigned short c1, unsigned short c2, unsigned short c3, unsigned short c4, char* expected) { + unsigned char ram[10]; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + ram[0] = c1 & 0xFF; ram[1] = (c1 >> 8) & 0xFF; + ram[2] = c2 & 0xFF; ram[3] = (c2 >> 8) & 0xFF; + ram[4] = c3 & 0xFF; ram[5] = (c3 >> 8) & 0xFF; + ram[6] = c4 & 0xFF; ram[7] = (c4 >> 8) & 0xFF; + ram[8] = '~'; ram[9] = '\0'; + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@UnicodeChar(0x 0)@UnicodeChar(0x 2)@UnicodeChar(0x 4)@UnicodeChar(0x 6)"); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_random_text_between_sections() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Locations are fun!\nLookup:Location\n0=Zero\n1=One\n\nDisplay goes here\nDisplay:\nAt @Location(0xH0000)\n\nWritten by User3"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_comments() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "// Locations are fun!\nLookup:Location // lookup\n0=Zero // 0\n1=One // 1\n\n//Display goes here\nDisplay: // display\nAt @Location(0xH0000) // text\n\n//Written by User3"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_comments_between_lines() { + unsigned char ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "// Locations are fun!\nLookup:Location\n// lookup\n0=Zero\n// 0\n1=One\n// 1\n\n//Display goes here\nDisplay:\n// display\nAt @Location(0xH0000)\n// text\n\n//Written by User3"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_display_string_comment_only() { + int lines; + int result = rc_richpresence_size_lines("Display:\n// This is a comment\n// And another\n// And some whitespace", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 5); /* end of file reached */ +} + +static void test_display_string_comment_with_blank_line() { + int lines; + int result = rc_richpresence_size_lines("Display:\n// This is a comment\n// And another\n\n// And some whitespace", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 4); /* line 4 was blank */ +} + +void test_richpresence(void) { + TEST_SUITE_BEGIN(); + + TEST(test_empty_script); + + /* static display string */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nHello, world!", "Hello, world!"); + + /* static display string with trailing whitespace */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat ", "What "); + + /* static display string whitespace only*/ + TEST_PARAMS2(test_simple_richpresence, "Display:\n ", " "); + + /* static display string with comment (trailing whitespace will be trimmed) */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat // Where", "What"); + + /* static display string with escaped comment */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\// Where", "What // Where"); + + /* static display string with escaped backslash */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\\\ Where", "What \\ Where"); + + /* static display string with partially escaped comment */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\/// Where", "What /"); + + /* static display string with trailing backslash (backslash will be ignored) */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\", "What "); + + /* static display string with trailing text */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat\n\nWhere", "What"); + + /* buffer boundary */ + test_buffer_boundary(); + + /* condition display */ + TEST(test_conditional_display_simple); + TEST(test_conditional_display_after_default); + TEST(test_conditional_display_no_default); + TEST(test_conditional_display_common_condition); + TEST(test_conditional_display_duplicated_condition); + TEST(test_conditional_display_invalid_condition_logic); + TEST(test_conditional_display_whitespace_text); + TEST(test_conditional_display_indirect); + TEST(test_conditional_display_unnecessary_measured); + TEST(test_conditional_display_unnecessary_measured_indirect); + + /* value macros */ + TEST(test_macro_value); + TEST(test_macro_value_nibble); + TEST(test_macro_value_bcd); + TEST(test_macro_value_bitcount); + TEST(test_macro_value_adjusted_negative); + TEST(test_macro_value_from_formula); + TEST(test_macro_value_from_hits); + TEST(test_macro_value_from_indirect); + TEST(test_macro_value_divide_by_zero); + TEST(test_macro_value_divide_by_self); + + /* frames macros */ + TEST(test_macro_frames); + + /* float macros */ + TEST_PARAMS3(test_macro_float, "VALUE", 0x429A4492, "77"); /* 77.133926 */ + TEST_PARAMS3(test_macro_float, "FLOAT1", 0x429A4492, "77.1"); + TEST_PARAMS3(test_macro_float, "FLOAT2", 0x429A4492, "77.13"); + TEST_PARAMS3(test_macro_float, "FLOAT3", 0x429A4492, "77.134"); /* rounded up */ + TEST_PARAMS3(test_macro_float, "FLOAT4", 0x429A4492, "77.1339"); + TEST_PARAMS3(test_macro_float, "FLOAT5", 0x429A4492, "77.13393"); /* rounded up */ + TEST_PARAMS3(test_macro_float, "FLOAT6", 0x429A4492, "77.133926"); + TEST_PARAMS3(test_macro_float, "VALUE", 0xC0000000, "-2"); /* -2.0 */ + TEST_PARAMS3(test_macro_float, "FLOAT1", 0xC0000000, "-2.0"); + TEST_PARAMS3(test_macro_float, "FLOAT6", 0xC0000000, "-2.000000"); + TEST_PARAMS3(test_macro_float, "SECS", 0x429A4492, "1:17"); /* 77.133926 */ + + /* lookup macros */ + TEST(test_macro_lookup_simple); + TEST(test_macro_lookup_with_inline_comment); + TEST(test_macro_lookup_hex_keys); + TEST(test_macro_lookup_default); + TEST(test_macro_lookup_crlf); + TEST(test_macro_lookup_after_display); + TEST(test_macro_lookup_from_formula); + TEST(test_macro_lookup_from_indirect); + TEST(test_macro_lookup_repeated); + TEST(test_macro_lookup_shared); + TEST(test_macro_lookup_multiple); + TEST(test_macro_lookup_and_value); + TEST(test_macro_lookup_negative_value); + TEST(test_macro_lookup_value_with_whitespace); + TEST(test_macro_lookup_mapping_repeated); + TEST(test_macro_lookup_mapping_repeated_csv); + TEST(test_macro_lookup_mapping_merged); + TEST(test_macro_lookup_mapping_range); + TEST(test_macro_lookup_invalid); + + /* escaped macro */ + TEST(test_macro_escaped); + + /* macro errors */ + TEST(test_macro_undefined); + TEST(test_macro_undefined_at_end_of_line); + TEST(test_macro_unterminated); + TEST(test_macro_without_parameter); + TEST(test_macro_without_parameter_conditional_display); + TEST(test_macro_non_numeric_parameter); + + /* builtin macros */ + TEST_PARAMS2(test_builtin_macro, "Number", "12345"); + TEST_PARAMS2(test_builtin_macro, "Score", "012345"); + TEST_PARAMS2(test_builtin_macro, "Centiseconds", "2:03.45"); + TEST_PARAMS2(test_builtin_macro, "Seconds", "3h25:45"); + TEST_PARAMS2(test_builtin_macro, "Minutes", "205h45"); + TEST_PARAMS2(test_builtin_macro, "SecondsAsMinutes", "3h25"); + TEST_PARAMS2(test_builtin_macro, "ASCIIChar", "?"); /* 0x3039 is not a single ASCII char */ + TEST_PARAMS2(test_builtin_macro, "UnicodeChar", "\xe3\x80\xb9"); + TEST_PARAMS2(test_builtin_macro_float, "Float1", "77.1"); + TEST_PARAMS2(test_builtin_macro_float, "Float2", "77.13"); + TEST_PARAMS2(test_builtin_macro_float, "Float3", "77.134"); + TEST_PARAMS2(test_builtin_macro_float, "Float4", "77.1339"); + TEST_PARAMS2(test_builtin_macro_float, "Float5", "77.13393"); + TEST_PARAMS2(test_builtin_macro_float, "Float6", "77.133926"); + TEST(test_builtin_macro_override); + + /* asciichar */ + TEST(test_asciichar); + TEST_PARAMS9(test_ascii8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ""); + TEST_PARAMS9(test_ascii8, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, "ABCDEFGH"); + TEST_PARAMS9(test_ascii8, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, "Test"); + TEST_PARAMS9(test_ascii8, 0x54, 0x65, 0x73, 0x74, 0x00, 0x46, 0x6F, 0x6F, "Test"); + TEST_PARAMS9(test_ascii8, 0x00, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, ""); + TEST_PARAMS9(test_ascii8, 0x31, 0x7E, 0x32, 0x7F, 0x33, 0x20, 0x34, 0x5E, "1~2?3 4^"); /* 7F out of range */ + TEST_PARAMS9(test_ascii8, 0x54, 0x61, 0x62, 0x09, 0x31, 0x0D, 0x0E, 0x00, "Tab?1??"); /* control characters */ + + /* unicodechar */ + TEST_PARAMS5(test_unicode4, 0x0000, 0x0000, 0x0000, 0x0000, ""); + TEST_PARAMS5(test_unicode4, 0x0054, 0x0065, 0x0073, 0x0074, "Test"); + TEST_PARAMS5(test_unicode4, 0x0000, 0x0065, 0x0073, 0x0074, ""); + TEST_PARAMS5(test_unicode4, 0x00A9, 0x0031, 0x0032, 0x0033, "\xc2\xa9\x31\x32\x33"); /* two-byte unicode char */ + TEST_PARAMS5(test_unicode4, 0x2260, 0x0020, 0x0040, 0x0040, "\xe2\x89\xa0 @@"); /* three-byte unicode char */ + TEST_PARAMS5(test_unicode4, 0xD83D, 0xDEB6, 0x005F, 0x007A, "\xef\xbf\xbd\xef\xbf\xbd_z"); /* four-byte unicode pair */ + TEST_PARAMS5(test_unicode4, 0x0009, 0x003E, 0x000D, 0x000A, "\xef\xbf\xbd>\xef\xbf\xbd\xef\xbf\xbd"); /* control characters */ + + /* comments */ + TEST(test_random_text_between_sections); /* before official comments extra text was ignored, so was occassionally used to comment */ + TEST(test_comments); + TEST(test_comments_between_lines); + TEST(test_display_string_comment_only); + TEST(test_display_string_comment_with_blank_line); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_runtime.c b/src/rcheevos/test/rcheevos/test_runtime.c new file mode 100644 index 000000000..3fc0a79de --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_runtime.c @@ -0,0 +1,1663 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "mock_memory.h" + +#include "../test_framework.h" + +static rc_runtime_event_t events[16]; +static int event_count = 0; + +static void event_handler(const rc_runtime_event_t* e) +{ + memcpy(&events[event_count++], e, sizeof(rc_runtime_event_t)); +} + +static void _assert_event(char type, unsigned id, int value) +{ + int i; + + for (i = 0; i < event_count; ++i) { + if (events[i].id == id && events[i].type == type && events[i].value == value) + return; + } + + ASSERT_FAIL("expected event not found"); +} +#define assert_event(type, id, value) ASSERT_HELPER(_assert_event(type, id, value), "assert_event") + +static void _assert_activate_achievement(rc_runtime_t* runtime, unsigned int id, const char* memaddr) +{ + int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") + +static void _assert_activate_lboard(rc_runtime_t* runtime, unsigned int id, const char* memaddr) +{ + int result = rc_runtime_activate_lboard(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_lboard(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_lboard(runtime, id, memaddr), "assert_activate_lboard") + +static void _assert_activate_richpresence(rc_runtime_t* runtime, const char* script) +{ + int result = rc_runtime_activate_richpresence(runtime, script, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_richpresence(runtime, script) ASSERT_HELPER(_assert_activate_richpresence(runtime, script), "assert_activate_richpresence") + +static void assert_do_frame(rc_runtime_t* runtime, memory_t* memory) +{ + event_count = 0; + rc_runtime_do_frame(runtime, event_handler, peek, memory, NULL); +} + +static void assert_richpresence_display_string(rc_runtime_t* runtime, memory_t* memory, const char* expected) +{ + char buffer[512]; + const int expected_len = (int)strlen(expected); + const int result = rc_runtime_get_richpresence(runtime, buffer, sizeof(buffer), peek, memory, NULL); + ASSERT_STR_EQUALS(buffer, expected); + ASSERT_NUM_EQUALS(result, expected_len); +} + +static void test_two_achievements_activate_and_trigger(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + + /* both achievements are true, should remain in waiting state */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both achievements are false, should activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* second achievement is true, should trigger */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + /* first achievement is true, should trigger. second is already triggered */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* reset second achievement, should go back to WAITING and stay there */ + rc_reset_trigger(runtime.triggers[1].trigger); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both achievements are false again. second should active, first should be ignored */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_deactivate_achievements(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + + /* both achievements are true, should remain in waiting state */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* deactivate the first. it owns shared memrefs, so can't be deallocated */ + rc_runtime_deactivate_achievement(&runtime, 1); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_PTR_NULL(runtime.triggers[0].trigger); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* both achievements are false, deactivated one should not activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* both achievements are true, deactivated one should not trigger */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + /* reactivate achievement. definition didn't change, should reactivate in-place */ + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_PTR_NOT_NULL(runtime.triggers[0].trigger); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + + /* reactivated achievement is waiting and should not trigger */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both achievements are false. first should activate, second should be ignored */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_achievement_measured(void) +{ + /* bytes 3-7 are the float value for 16*pi */ + unsigned char ram[] = { 0, 10, 10, 0xDB, 0x0F, 0x49, 0x41 }; + char buffer[32]; + memory_t memory; + rc_runtime_t runtime; + unsigned value, target; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* use equality so we can test values greater than the target */ + assert_activate_achievement(&runtime, 1, "0xH0002==10"); + assert_activate_achievement(&runtime, 2, "M:0xH0002==10"); + assert_activate_achievement(&runtime, 3, "G:0xH0002==10"); + assert_activate_achievement(&runtime, 4, "M:fF0003==f12.56637"); + + /* achievements are true, should remain in waiting state with no measured value */ + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0/10"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0%"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 12); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0/12"); + ASSERT_FALSE(rc_runtime_get_achievement_measured(&runtime, 5, &value, &target)); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 5, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + + /* achievements are false, should activate */ + ram[2] = 9; + ram[6] = 0x40; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 9); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "9/10"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 9); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "90%"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 3); + ASSERT_NUM_EQUALS(target, 12); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "3/12"); /* captured measured value and target are integers, so 3.14/12.56 -> 3/12 */ + + /* value greater than target (i.e. "6 >= 5" should report maximum "5/5" or "100%" */ + ram[2] = 12; + ram[6] = 0x42; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 12); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "10/10"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 12); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "100%"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 50); + ASSERT_NUM_EQUALS(target, 12); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "12/12"); + + /* achievements are true, should trigger - triggered achievement is not measurable */ + ram[2] = 10; + ram[6] = 0x41; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + + rc_runtime_destroy(&runtime); +} + +static void test_achievement_measured_maxint(void) +{ + unsigned char ram[] = { 0xFF, 0xFF, 0xFF, 0xFF }; + char buffer[32]; + memory_t memory; + rc_runtime_t runtime; + unsigned value, target; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 2, "M:0xX0000==hFFFFFFFF"); + assert_activate_achievement(&runtime, 3, "G:0xX0000==hFFFFFFFF"); + + /* achievements are true, should remain in waiting state */ + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0/4294967295"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0%"); + + /* achievements are false (value fits in 31-bits), should activate */ + ram[1] = ram[3] = 0x7F; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0x7FFF7FFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "2147450879/4294967295"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0x7FFF7FFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "49%"); + + /* achievements are false (value requires 32-bits) */ + ram[1] = ram[3] = 0xFE; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0xFEFFFEFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "4278189823/4294967295"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0xFEFFFEFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "99%"); + + /* achievements are true, should trigger - triggered achievement is not measurable */ + ram[1] = ram[3] = 0xFF; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + + rc_runtime_destroy(&runtime); +} + +static void test_two_achievements_differing_resets_in_alts(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10S1=1SR:0xH0000!=0"); + assert_activate_achievement(&runtime, 2, "0xH0001=10S1=1SR:0xH0000!=1"); + + /* first achievement true (stays waiting), second not true because of reset */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* both achievements are false, should activate */ + ram[1] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* first should fire, second prevented by reset */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* second can fire, reset first which will be activated due to reset */ + ram[0] = 1; + rc_reset_trigger(runtime.triggers[0].trigger); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + /* both achievements are false again. second should active, first should be ignored */ + ram[0] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_shared_memref(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + rc_memref_t* memref1; + rc_memref_t* memref2; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0001=12"); + + memref1 = runtime.triggers[0].trigger->requirement->conditions->operand1.value.memref; + memref2 = runtime.triggers[1].trigger->requirement->conditions->operand1.value.memref; + ASSERT_PTR_EQUALS(memref1, memref2); + + /* first is true, should remain waiting. second should activate */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* deactivate second one. it doesn't have any unique memrefs, so can be free'd */ + rc_runtime_deactivate_achievement(&runtime, 2); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_PTR_NOT_NULL(runtime.triggers[0].trigger); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* second is true, but no longer in runtime. first should activate, expect nothing from second */ + ram[1] = 12; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* first is true and should trigger */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* reactivate achievement. old definition was free'd so should be recreated */ + assert_activate_achievement(&runtime, 2, "0xH0001=12"); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* reactivated achievement is waiting and false. should activate */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* deactivate first achievement. memrefs used by second - cannot be free'd */ + rc_runtime_deactivate_achievement(&runtime, 1); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_PTR_NULL(runtime.triggers[0].trigger); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* second achievement is true. should trigger using memrefs from first */ + ram[1] = 12; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_replace_active_trigger(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 1, "0xH0002=10"); + + /* both are true, but first should have been overwritten by second */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_PTR_NULL(runtime.triggers[0].trigger); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both are false. only second should be getting processed, expect single event */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* first is true, but should not trigger */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second is true and should trigger */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* switch back to original definition. since the memrefs kept the buffer alive, it should be recycled */ + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_PTR_NULL(runtime.triggers[1].trigger); + + rc_runtime_destroy(&runtime); +} + +static rc_runtime_t* discarding_event_handler_runtime = NULL; +static void discarding_event_handler(const rc_runtime_event_t* e) +{ + event_handler(e); + rc_runtime_deactivate_achievement(discarding_event_handler_runtime, e->id); +} + +static void test_trigger_deactivation(void) +{ + unsigned char ram[] = { 0, 9, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* three identical achievements that should trigger when $1 changes from 9 to 10 */ + assert_activate_achievement(&runtime, 1, "0xH0001=10_d0xH0001=9"); + assert_activate_achievement(&runtime, 2, "0xH0001=10_d0xH0001=9"); + assert_activate_achievement(&runtime, 3, "0xH0001=10_d0xH0001=9"); + + /* prep the delta and make sure the achievements are active */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* trigger all three */ + ram[1] = 10; + event_count = 0; + discarding_event_handler_runtime = &runtime; + rc_runtime_do_frame(&runtime, discarding_event_handler, peek, &memory, NULL); + discarding_event_handler_runtime = NULL; + + ASSERT_NUM_EQUALS(event_count, 3); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 3, 0); + + /* triggers 2 and 3 should have been removed from the runtime since they don't have memrefs, + * and trigger 1 should be nulled out. */ + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_PTR_NULL(runtime.triggers[0].trigger); + + rc_runtime_destroy(&runtime); +} + +static void test_trigger_with_resetif() { + unsigned char ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* never(byte(3)==1) && once(byte(4)==1) && trigger_when(byte(0)==1) */ + assert_activate_achievement(&runtime, 1, "R:0xH0003=1_0xH0004=1.1._T:0xH0000=1"); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* non-trigger condition is true */ + ram[4] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* ResetIf is true */ + ram[3] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + + /* ResetIf no longer true */ + ram[3] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_trigger_with_resetnextif() { + unsigned char ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* once(byte(4)==1 && never(repeated(2, byte(3)==1 && never(byte(1)==1 || byte(2)==1))) && trigger_when(byte(0)==1) */ + assert_activate_achievement(&runtime, 1, "O:0xH0001=1_Z:0xH0002=1_Z:0xH0003=1.2._0xH0004=1.1._T:0xH0000=1"); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* non-trigger condition is true */ + ram[4] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* second ResetNextIf is true */ + ram[3] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* OrNext resets second ResetNextIf */ + ram[1] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + + /* OrNext no longer true */ + ram[1] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second ResetNextIf fires */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_reset_event(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + rc_condition_t* cond; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10.2._R:0xH0002=10"); + cond = runtime.triggers[0].trigger->requirement->conditions; + + /* reset is true, so achievement is false and should activate, but not notify reset */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 0); + + /* reset is still true, but since no hits were accumulated there shouldn't be a reset event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* reset is not true, hits should increment */ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 1); + + /* reset is true. hits will reset. expect event */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 0); + + /* reset is still true, but since hits were previously reset there shouldn't be a reset event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* reset is not true, hits should increment */ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 1); + + /* reset is not true, hits should increment, causing achievement to trigger */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 2); + + /* reset is true, but hits shouldn't reset as achievement is no longer active */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 2); + + rc_runtime_destroy(&runtime); +} + +static void test_paused_event(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10.2._P:0xH0002=10"); + + /* pause is true, so achievement is false and should activate, but only notify pause */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); + + /* pause is still true, but previously paused, so no event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* pause is not true, expect activate event */ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* pause is true. expect event */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); + + /* pause is still true, but previously paused, so no event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* pause is not true, expect trigger*/ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* pause is true, but shouldn't notify as achievement is no longer active */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_primed_event(void) +{ + unsigned char ram[] = { 0, 1, 0, 1, 0, 0 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(0)==1 && trigger(byte(1)==1) && byte(2)==1 && trigger(byte(3)==1) && unless(byte(4)==1) && never(byte(5) == 1) */ + assert_activate_achievement(&runtime, 1, "0xH0000=1_T:0xH0001=1_0xH0002=1_T:0xH0003=1_P:0xH0004=1_R:0xH0005=1"); + + /* trigger conditions are true, but nothing else */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* primed */ + ram[1] = ram[3] = 0; + ram[0] = ram[2] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* no longer primed */ + ram[0] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + + /* primed */ + ram[0] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* paused */ + ram[4] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); + + /* unpaused */ + ram[4] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* reset */ + ram[5] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + + /* not reset */ + ram[5] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* all conditions are true */ + ram[1] = ram[3] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_progress_event(void) +{ + unsigned char ram[] = { 0, 1 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* measured(byte(0x0001) >= 10) */ + assert_activate_achievement(&runtime, 1, "M:0xH0001>=10"); + runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* should not receive notification when initialized first measured value */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased */ + ram[1] = 2; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 2); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* decreased */ + ram[1] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 0); + + /* increased */ + ram[1] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 9); + + /* triggered. should not receive change event */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* no longer active */ + ram[1] = 11; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_progress_event_as_percent(void) +{ + unsigned char ram[] = { 0, 1 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* measured(byte(0x0001) >= 200, format='percent') */ + assert_activate_achievement(&runtime, 1, "G:0xH0001>=200"); + runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* should not receive notification when initialized first measured value */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased (0% -> 1%) */ + ram[1] = 2; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 1); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased (1% -> 1%, no event) */ + ram[1] = 3; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased (1% -> 2%) */ + ram[1] = 4; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 2); + + /* decreased */ + ram[1] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 0); + + /* increased */ + ram[1] = 199; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 99); + + /* triggered. should not receive change event */ + ram[1] = 200; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* no longer active */ + ram[1] = 40; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_lboard(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0000"); + assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0000*2"); + + /* both start conditions are true, leaderboards will not be active */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are false, leaderboards will activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are true, leaderboards will start */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 1, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 2, 4); + + /* start condition no longer true, leaderboard should continue processing */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* value changed */ + ram[0] = 3; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_UPDATED, 1, 3); + assert_event(RC_RUNTIME_EVENT_LBOARD_UPDATED, 2, 6); + + /* value changed; first leaderboard submit, second canceled - expect events for submit and cancel, none for update */ + ram[0] = 4; + ram[1] = 11; + ram[2] = 12; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_TRIGGERED, 1, 4); + assert_event(RC_RUNTIME_EVENT_LBOARD_CANCELED, 2, 0); + + /* both start conditions are true, leaderboards will not be active */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are false, leaderboards will re-activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are true, leaderboards will start */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 1, 4); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 2, 8); + + rc_runtime_destroy(&runtime); +} + +static void test_format_lboard_value(int format, int value, const char* expected) { + char buffer[64]; + int result; + + result = rc_runtime_format_lboard_value(buffer, sizeof(buffer), value, format); + ASSERT_STR_EQUALS(buffer, expected); + ASSERT_NUM_EQUALS(result, strlen(expected)); +} + +static void test_richpresence(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* initial value */ + assert_richpresence_display_string(&runtime, &memory, ""); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\nScore is @Points(0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "Score is 0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2570 Points"); + + /* calling rc_runtime_get_richpresence without calling rc_runtime_do_frame should return the same string as memrefs aren't updated */ + ram[1] = 20; + assert_richpresence_display_string(&runtime, &memory, "Score is 2570 Points"); + + /* call rc_runtime_do_frame to update memrefs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2580 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_starts_with_macro(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2570 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_macro_only(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001)"); + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2570"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_conditional(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0000=2?@Points(0x 0001) points\nScore is @Points(0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "Score is 0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2570 points"); + + /* update display string */ + ram[0] = 0; + ram[1] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2580 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_conditional_with_hits(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0000=1.2.?Score is @Points(0x 0001) Points\n@Points(0x 0001) points"); + assert_richpresence_display_string(&runtime, &memory, "0 points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2570 points"); + + /* one hit is not enough to switch display strings, but the memref does get updated */ + ram[0] = 1; + ram[1] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2580 points"); + + /* second hit is enough */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2580 Points"); + + /* no more hits are accumulated */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2580 Points"); + + /* same test without intermediary evaluation of display string */ + rc_runtime_reset(&runtime); + ram[0] = 2; + ram[1] = 30; + assert_do_frame(&runtime, &memory); /* no hits */ + + ram[0] = 1; + assert_do_frame(&runtime, &memory); /* one hit */ + assert_do_frame(&runtime, &memory); /* two hits */ + assert_richpresence_display_string(&runtime, &memory, "Score is 2590 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_conditional_with_hits_after_match(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0002=10?It's @Points(0x 0001)\n?0xH0000=1.2.?Score is @Points(0x 0001) Points\n@Points(0x 0001) points"); + assert_richpresence_display_string(&runtime, &memory, "0 points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "It's 2570"); + + /* first condition is true, but one hit should still be tallied on the second conditional */ + ram[0] = 1; + ram[1] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "It's 2580"); + + /* first conditio no longer true, second condtion will get it's second hit, which is enough */ + ram[2] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 5140 Points"); + + /* same test without intermediary evaluation of display string */ + rc_runtime_reset(&runtime); + ram[0] = 2; + ram[1] = 10; + ram[2] = 10; + assert_do_frame(&runtime, &memory); /* no hits */ + + ram[0] = 1; + ram[1] = 20; + assert_do_frame(&runtime, &memory); /* one hit */ + ram[2] = 20; + assert_do_frame(&runtime, &memory); /* two hits */ + assert_richpresence_display_string(&runtime, &memory, "Score is 5140 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_reload(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2570 Points"); + + /* reloading should generate display string with current memrefs */ + ram[1] = 20; + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Bananas"); + assert_richpresence_display_string(&runtime, &memory, "2570 Bananas"); + + /* memrefs should be reused from first script */ + ASSERT_NUM_EQUALS(runtime.richpresence->owns_memrefs, 0); + ASSERT_PTR_NOT_NULL(runtime.richpresence->previous); + + /* first frame after reloading should update display string */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2580 Bananas"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_reload_addaddress(void) +{ + /* ram[1] must be non-zero */ + unsigned char ram[] = { 1, 10, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(I:0xH0000_M:0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2570 Points"); + + /* reloading should generate display string with current memrefs */ + /* the entire AddAddress expression will be a single variable, which will have a current value. */ + ram[2] = 20; + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(I:0xH0000_M:0x 0001) Bananas"); + assert_richpresence_display_string(&runtime, &memory, "2570 Bananas"); + + /* the AddAddress expression will be owned by the previous script. */ + ASSERT_NUM_EQUALS(runtime.richpresence->owns_memrefs, 0); + ASSERT_PTR_NOT_NULL(runtime.richpresence->previous); + + /* first frame after reloading should update display string */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2580 Bananas"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_static(void) +{ + unsigned char ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_richpresence(&runtime, "Display:\nHello, world!"); + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Hello, world!"); + + /* first frame won't affect the display string */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Hello, world!"); + + rc_runtime_destroy(&runtime); +} + +typedef struct { + memory_t memory; + rc_runtime_t* runtime; + unsigned invalid_address; +} +memory_invalid_t; + +static unsigned peek_invalid(unsigned address, unsigned num_bytes, void* ud) +{ + memory_invalid_t* memory = (memory_invalid_t*)ud; + if (memory->invalid_address != address) + return peek(address, num_bytes, &memory->memory); + + rc_runtime_invalidate_address(memory->runtime, address); + return 0; +} + +static void assert_do_frame_invalid(rc_runtime_t* runtime, memory_invalid_t* memory, unsigned invalid_address) +{ + event_count = 0; + memory->runtime = runtime; + memory->invalid_address = invalid_address; + rc_runtime_do_frame(runtime, event_handler, peek_invalid, memory, NULL); +} + +static void test_invalidate_address(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_invalid_t memory; + rc_runtime_t runtime; + + memory.memory.ram = ram; + memory.memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + + /* achievements should start in waiting state */ + assert_do_frame_invalid(&runtime, &memory, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* nothing depends on address 3 */ + assert_do_frame_invalid(&runtime, &memory, 3); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* second achievement depends on address 2 */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 2); + + /* second achievement already disabled, don't raise event again */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + + rc_runtime_destroy(&runtime); +} + +static void test_invalidate_address_no_memrefs(void) +{ + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + /* simple test to ensure a null reference doesn't occur when no memrefs are present */ + rc_runtime_invalidate_address(&runtime, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_invalidate_address_shared_memref(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_invalid_t memory; + rc_runtime_t runtime; + + memory.memory.ram = ram; + memory.memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + assert_activate_achievement(&runtime, 3, "0xH0001=10S0xH0002=10S0xH0003=10"); + + /* achievements should start in waiting state */ + assert_do_frame_invalid(&runtime, &memory, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* second and third achievements depend on address 2 */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_DISABLED); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 3, 2); + + rc_runtime_destroy(&runtime); +} + +static void test_invalidate_address_leaderboard(void) +{ + unsigned char ram[] = { 0, 10, 10 }; + memory_invalid_t memory; + rc_runtime_t runtime; + + memory.memory.ram = ram; + memory.memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0001"); + assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0002*2"); + + /* leaderboards should start in waiting state */ + assert_do_frame_invalid(&runtime, &memory, 0); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second leaderboard depends on address 2 */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_DISABLED); + assert_event(RC_RUNTIME_EVENT_LBOARD_DISABLED, 2, 2); + + rc_runtime_destroy(&runtime); +} + +static int validate_address_handler(unsigned address) +{ + return (address & 1) == 0; /* all even addresses are valid */ +} + +static void test_validate_addresses(void) +{ + rc_runtime_t runtime; + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0003=10"); /* put two invalid memrefs next to each other */ + assert_activate_achievement(&runtime, 3, "0xH0002=10"); + assert_activate_achievement(&runtime, 4, "0xH0001=10"); /* shared reference to invalid memref */ + assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0001"); + assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0002*2"); + + /* everything should start in waiting state */ + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[3].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + + /* validate_addresses should immediately disable the achievements and raise the event */ + rc_runtime_validate_addresses(&runtime, event_handler, validate_address_handler); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[3].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + + ASSERT_NUM_EQUALS(event_count, 4); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 1, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 3); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 4, 1); + assert_event(RC_RUNTIME_EVENT_LBOARD_DISABLED, 1, 1); + + rc_runtime_destroy(&runtime); +} + +void test_runtime(void) { + TEST_SUITE_BEGIN(); + + /* achievements */ + TEST(test_two_achievements_activate_and_trigger); + TEST(test_deactivate_achievements); + TEST(test_achievement_measured); + TEST(test_achievement_measured_maxint); + TEST(test_two_achievements_differing_resets_in_alts); + + TEST(test_shared_memref); + TEST(test_replace_active_trigger); + TEST(test_trigger_deactivation); + TEST(test_trigger_with_resetif); + TEST(test_trigger_with_resetnextif); + + /* achievement events */ + TEST(test_reset_event); + TEST(test_paused_event); + TEST(test_primed_event); + TEST(test_progress_event); + TEST(test_progress_event_as_percent); + + /* leaderboards */ + TEST(test_lboard); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, 12345, "12345"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, -12345, "-12345"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, 0xFFFFFFFF, "-1"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_SCORE, 12345, "012345"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_SECONDS, 345, "5:45"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_CENTISECS, 12345, "2:03.45"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_FRAMES, 12345, "3:25.75"); + + /* rich presence */ + TEST(test_richpresence); + TEST(test_richpresence_starts_with_macro); + TEST(test_richpresence_macro_only); + TEST(test_richpresence_conditional); + TEST(test_richpresence_conditional_with_hits); + TEST(test_richpresence_conditional_with_hits_after_match); + TEST(test_richpresence_reload); + TEST(test_richpresence_reload_addaddress); + TEST(test_richpresence_static); + + /* invalidate address */ + TEST(test_invalidate_address); + TEST(test_invalidate_address_no_memrefs); + TEST(test_invalidate_address_shared_memref); + TEST(test_invalidate_address_leaderboard); + + TEST(test_validate_addresses); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_runtime_progress.c b/src/rcheevos/test/rcheevos/test_runtime_progress.c new file mode 100644 index 000000000..eb84f2ae1 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_runtime_progress.c @@ -0,0 +1,1612 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "../test_framework.h" +#include "../rhash/md5.h" +#include "mock_memory.h" + +static void _assert_activate_achievement(rc_runtime_t* runtime, unsigned int id, const char* memaddr) +{ + int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") + +static void _assert_activate_leaderboard(rc_runtime_t* runtime, unsigned int id, const char* script) +{ + int result = rc_runtime_activate_lboard(runtime, id, script, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_leaderboard(runtime, id, script) ASSERT_HELPER(_assert_activate_leaderboard(runtime, id, script), "assert_activate_leaderboard") + +static void _assert_activate_rich_presence(rc_runtime_t* runtime, const char* script) +{ + int result = rc_runtime_activate_richpresence(runtime, script, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_rich_presence(runtime, script) ASSERT_HELPER(_assert_activate_rich_presence(runtime, script), "assert_activate_rich_presence") + +static void _assert_richpresence_output(rc_runtime_t* runtime, memory_t* memory, const char* expected_display_string) { + char output[256]; + int result; + + result = rc_runtime_get_richpresence(runtime, output, sizeof(output), peek, memory, NULL); + ASSERT_STR_EQUALS(output, expected_display_string); + ASSERT_NUM_EQUALS(result, strlen(expected_display_string)); +} +#define assert_richpresence_output(runtime, memory, expected_display_string) ASSERT_HELPER(_assert_richpresence_output(runtime, memory, expected_display_string), "assert_richpresence_output") + +static void event_handler(const rc_runtime_event_t* e) +{ + (void)e; +} + +static void assert_do_frame(rc_runtime_t* runtime, memory_t* memory) +{ + rc_runtime_do_frame(runtime, event_handler, peek, memory, NULL); +} + +static void _assert_serialize(rc_runtime_t* runtime, unsigned char* buffer, unsigned buffer_size) +{ + int result; + unsigned* overflow; + + unsigned size = rc_runtime_progress_size(runtime, NULL); + ASSERT_NUM_LESS(size, buffer_size); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + result = rc_runtime_serialize_progress(buffer, runtime, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_serialize(runtime, buffer, buffer_size) ASSERT_HELPER(_assert_serialize(runtime, buffer, buffer_size), "assert_serialize") + +static void _assert_deserialize(rc_runtime_t* runtime, unsigned char* buffer) +{ + int result = rc_runtime_deserialize_progress(runtime, buffer, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_deserialize(runtime, buffer) ASSERT_HELPER(_assert_deserialize(runtime, buffer), "assert_deserialize") + +static void _assert_sized_memref(rc_runtime_t* runtime, unsigned address, char size, unsigned value, unsigned prev, unsigned prior) +{ + rc_memref_t* memref = runtime->memrefs; + while (memref) + { + if (memref->address == address && memref->value.size == size) + { + ASSERT_NUM_EQUALS(memref->value.value, value); + ASSERT_NUM_EQUALS(memref->value.prior, prior); + + if (value == prior) + { + ASSERT_NUM_EQUALS(memref->value.changed, 0); + } + else + { + ASSERT_NUM_EQUALS(memref->value.changed, (prev == prior) ? 1 : 0); + } + return; + } + + memref = memref->next; + } + + ASSERT_FAIL("could not find memref for address %u", address); +} +#define assert_sized_memref(runtime, address, size, value, prev, prior) ASSERT_HELPER(_assert_sized_memref(runtime, address, size, value, prev, prior), "assert_sized_memref") +#define assert_memref(runtime, address, value, prev, prior) ASSERT_HELPER(_assert_sized_memref(runtime, address, RC_MEMSIZE_8_BITS, value, prev, prior), "assert_memref") + +static rc_trigger_t* find_trigger(rc_runtime_t* runtime, unsigned ach_id) +{ + unsigned i; + for (i = 0; i < runtime->trigger_count; ++i) { + if (runtime->triggers[i].id == ach_id && runtime->triggers[i].trigger) + return runtime->triggers[i].trigger; + } + + ASSERT_MESSAGE("could not find trigger for achievement %u", ach_id); + return NULL; +} + +static rc_condition_t* find_trigger_cond(rc_trigger_t* trigger, unsigned group_idx, unsigned cond_idx) +{ + rc_condset_t* condset; + rc_condition_t* cond; + + if (!trigger) + return NULL; + + condset = trigger->requirement; + if (group_idx > 0) { + condset = trigger->alternative; + while (condset && --group_idx != 0) + condset = condset->next; + } + + if (!condset) + return NULL; + + cond = condset->conditions; + while (cond && cond_idx > 0) { + --cond_idx; + cond = cond->next; + } + + return cond; +} + +static void _assert_hitcount(rc_runtime_t* runtime, unsigned ach_id, unsigned group_idx, unsigned cond_idx, unsigned expected_hits) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_hitcount(runtime, ach_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_hitcount(runtime, ach_id, group_idx, cond_idx, expected_hits), "assert_hitcount") + +static void _assert_cond_memref(rc_runtime_t* runtime, unsigned ach_id, unsigned group_idx, unsigned cond_idx, unsigned expected_value, unsigned expected_prior, char expected_changed) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.value, expected_value); + ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.prior, expected_prior); + ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.changed, expected_changed); +} +#define assert_cond_memref(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed) \ + ASSERT_HELPER(_assert_cond_memref(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed), "assert_cond_memref") + +static void _assert_cond_memref2(rc_runtime_t* runtime, unsigned ach_id, unsigned group_idx, unsigned cond_idx, unsigned expected_value, unsigned expected_prior, char expected_changed) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.value, expected_value); + ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.prior, expected_prior); + ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.changed, expected_changed); +} +#define assert_cond_memref2(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed) \ + ASSERT_HELPER(_assert_cond_memref2(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed), "assert_cond_memref2") + +static void _assert_achievement_state(rc_runtime_t* runtime, unsigned ach_id, int state) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + ASSERT_PTR_NOT_NULL(trigger); + + ASSERT_NUM_EQUALS(trigger->state, state); +} +#define assert_achievement_state(runtime, ach_id, state) ASSERT_HELPER(_assert_achievement_state(runtime, ach_id, state), "assert_achievement_state") + +static rc_lboard_t* find_lboard(rc_runtime_t* runtime, unsigned lboard_id) +{ + unsigned i; + for (i = 0; i < runtime->lboard_count; ++i) { + if (runtime->lboards[i].id == lboard_id && runtime->lboards[i].lboard) + return runtime->lboards[i].lboard; + } + + ASSERT_MESSAGE("could not find leaderboard %u", lboard_id); + return NULL; +} + +static void _assert_sta_hitcount(rc_runtime_t* runtime, unsigned lboard_id, unsigned group_idx, unsigned cond_idx, unsigned expected_hits) +{ + rc_lboard_t* lboard = find_lboard(runtime, lboard_id); + rc_condition_t* cond = find_trigger_cond(&lboard->start, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_sta_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_sta_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_sta_hitcount") + +static void _assert_sub_hitcount(rc_runtime_t* runtime, unsigned lboard_id, unsigned group_idx, unsigned cond_idx, unsigned expected_hits) +{ + rc_lboard_t* lboard = find_lboard(runtime, lboard_id); + rc_condition_t* cond = find_trigger_cond(&lboard->submit, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_sub_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_sub_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_sub_hitcount") + +static void _assert_can_hitcount(rc_runtime_t* runtime, unsigned lboard_id, unsigned group_idx, unsigned cond_idx, unsigned expected_hits) +{ + rc_lboard_t* lboard = find_lboard(runtime, lboard_id); + rc_condition_t* cond = find_trigger_cond(&lboard->cancel, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_can_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_can_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_can_hitcount") + +static void update_md5(unsigned char* buffer) +{ + md5_state_t state; + + unsigned char* ptr = buffer; + while (ptr[0] != 'D' || ptr[1] != 'O' || ptr[2] != 'N' || ptr[3] != 'E') + ++ptr; + + ptr += 8; + + md5_init(&state); + md5_append(&state, buffer, (int)(ptr - buffer)); + md5_finish(&state, ptr); +} + +static void reset_runtime(rc_runtime_t* runtime) +{ + rc_memref_t* memref; + rc_trigger_t* trigger; + rc_condition_t* cond; + rc_condset_t* condset; + unsigned i; + + memref = runtime->memrefs; + while (memref) + { + memref->value.value = 0xFF; + memref->value.changed = 0; + memref->value.prior = 0xFF; + + memref = memref->next; + } + + for (i = 0; i < runtime->trigger_count; ++i) + { + trigger = runtime->triggers[i].trigger; + if (trigger) + { + trigger->measured_value = 0xFF; + trigger->measured_target = 0xFF; + + if (trigger->requirement) + { + cond = trigger->requirement->conditions; + while (cond) + { + cond->current_hits = 0xFF; + cond = cond->next; + } + } + + condset = trigger->alternative; + while (condset) + { + cond = condset->conditions; + while (cond) + { + cond->current_hits = 0xFF; + cond = cond->next; + } + + condset = condset->next; + } + } + } +} + +static void test_empty() +{ + unsigned char buffer[2048]; + rc_runtime_t runtime; + + rc_runtime_init(&runtime); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + ASSERT_PTR_NULL(runtime.memrefs); + ASSERT_NUM_EQUALS(runtime.trigger_count, 0); + ASSERT_NUM_EQUALS(runtime.lboard_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_single_achievement() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_invalid_marker() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* invalid header prevents anything from being deserialized */ + buffer[0] = 0x40; + update_md5(buffer); + + reset_runtime(&runtime); + ASSERT_NUM_EQUALS(rc_runtime_deserialize_progress(&runtime, buffer, NULL), RC_INVALID_STATE); + + assert_memref(&runtime, 1, 0xFF, 0xFF, 0xFF); + assert_memref(&runtime, 2, 0xFF, 0xFF, 0xFF); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_invalid_memref_chunk_id() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* invalid chunk is ignored, achievement hits will still be read */ + buffer[5] = 0x40; + update_md5(buffer); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 0xFF, 0xFF, 0xFF); + assert_memref(&runtime, 2, 0xFF, 0xFF, 0xFF); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_modified_data() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* this changes the current hits for the test condition to 6, but doesn't update the checksum, so should be ignored */ + ASSERT_NUM_EQUALS(buffer[84], 3); + buffer[84] = 6; + + reset_runtime(&runtime); + ASSERT_NUM_EQUALS(rc_runtime_deserialize_progress(&runtime, buffer, NULL), RC_INVALID_STATE); + + /* memrefs will have been processed and cannot be "reset" */ + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + + /* deserialization failure causes all hits to be reset */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_single_achievement_deactivated() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + + /* disabled achievement */ + rc_runtime_deactivate_achievement(&runtime, 1); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + + /* reactivate */ + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_WAITING); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_single_achievement_md5_changed() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + + /* new achievement definition - rack up a couple hits */ + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5.1."); + ram[1] = 3; + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_hitcount(&runtime, 1, 0, 0, 2); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_memref(&runtime, 1, 4, 4, 3); + + assert_deserialize(&runtime, buffer); + + /* memrefs should be restored */ + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + + /* achievement definition changed, achievement should be reset */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void setup_multiple_achievements(rc_runtime_t* runtime, memory_t* memory) +{ + rc_runtime_init(runtime); + + assert_activate_achievement(runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(runtime, 2, "0xH0002=7_0xH0000=2"); + assert_activate_achievement(runtime, 3, "0xH0003=9_0xH0000=3"); + assert_activate_achievement(runtime, 4, "0xH0004=1_0xH0000=4"); + assert_do_frame(runtime, memory); + memory->ram[1] = 4; + assert_do_frame(runtime, memory); + memory->ram[2] = 7; + assert_do_frame(runtime, memory); + memory->ram[3] = 9; + assert_do_frame(runtime, memory); + memory->ram[4] = 1; + assert_do_frame(runtime, memory); + + assert_memref(runtime, 0, 0, 0, 0); + assert_memref(runtime, 1, 4, 4, 1); + assert_memref(runtime, 2, 7, 7, 2); + assert_memref(runtime, 3, 9, 9, 3); + assert_memref(runtime, 4, 1, 4, 4); + assert_hitcount(runtime, 1, 0, 0, 4); + assert_hitcount(runtime, 2, 0, 0, 3); + assert_hitcount(runtime, 3, 0, 0, 2); + assert_hitcount(runtime, 4, 0, 0, 1); +} + +static void test_no_core_group() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "S0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 1, 0, 3); + assert_hitcount(&runtime, 1, 1, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 1, 0, 3); + assert_hitcount(&runtime, 1, 1, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_shared_address() +{ + unsigned char ram[] = { 2, 3, 0, 0, 0 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0x 0001=5_0xX0001=6"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 6; + assert_do_frame(&runtime, &memory); + + assert_sized_memref(&runtime, 1, RC_MEMSIZE_8_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_16_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_32_BITS, 6, 5, 5); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 2); + assert_hitcount(&runtime, 1, 0, 2, 1); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_sized_memref(&runtime, 1, RC_MEMSIZE_8_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_16_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_32_BITS, 6, 5, 5); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 2); + assert_hitcount(&runtime, 1, 0, 2, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_indirect() +{ + unsigned char ram[] = { 1, 2, 3, 4, 5 }; + unsigned char buffer1[512]; + unsigned char buffer2[512]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(byte(2) + 1) == 5 - third condition just prevents the achievement from triggering*/ + assert_activate_achievement(&runtime, 1, "I:0xH0002_0xH0001=3_0xH0004=99"); + assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5 */ + ram[1] = 3; + ram[2] = 0; + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3 */ + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + assert_serialize(&runtime, buffer1, sizeof(buffer1)); + + assert_do_frame(&runtime, &memory); + ram[1] = 6; + ram[2] = 1; + assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1 */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer1); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_double_indirect() +{ + unsigned char ram[] = { 1, 2, 3, 4, 5 }; + unsigned char buffer1[512]; + unsigned char buffer2[512]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(byte(2) + 1) == byte(byte(2)) - third condition just prevents the achievement from triggering*/ + assert_activate_achievement(&runtime, 1, "I:0xH0002_0xH0001=0xH0000_0xH0004=99"); + assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5, $(3+0) = 4 */ + ram[0] = 3; + ram[1] = 3; + ram[2] = 0; + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, $(0+0) = 3 */ + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + assert_cond_memref2(&runtime, 1, 0, 1, 3, 4, 0); + + assert_serialize(&runtime, buffer1, sizeof(buffer1)); + + assert_do_frame(&runtime, &memory); + ram[1] = 6; + ram[2] = 1; + assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, $(1+0) = 6 */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + assert_cond_memref2(&runtime, 1, 0, 1, 6, 3, 1); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer1); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + assert_cond_memref2(&runtime, 1, 0, 1, 3, 4, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + assert_cond_memref2(&runtime, 1, 0, 1, 6, 3, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 0, 0, 0, 0); + assert_memref(&runtime, 1, 4, 4, 1); + assert_memref(&runtime, 2, 7, 7, 2); + assert_memref(&runtime, 3, 9, 9, 3); + assert_memref(&runtime, 4, 1, 4, 4); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 1, 0); + assert_hitcount(&runtime, 3, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 1, 0); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_ignore_triggered_and_inactive() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* trigger achievement 3 */ + ram[0] = 3; + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_TRIGGERED); + + /* reset achievement 2 to inactive */ + find_trigger(&runtime, 2)->state = RC_TRIGGER_STATE_INACTIVE; + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 0, 0, 0, 0); + assert_memref(&runtime, 1, 4, 4, 1); + assert_memref(&runtime, 2, 7, 7, 2); + assert_memref(&runtime, 3, 9, 9, 3); + assert_memref(&runtime, 4, 1, 4, 4); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_INACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_TRIGGERED); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 0xFF); /* inactive achievement should be ignored */ + assert_hitcount(&runtime, 2, 0, 1, 0xFF); + assert_hitcount(&runtime, 3, 0, 0, 0xFF); /* triggered achievement should be ignored */ + assert_hitcount(&runtime, 3, 0, 1, 0xFF); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_overwrite_waiting() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reset achievement 2 to waiting */ + rc_reset_trigger(find_trigger(&runtime, 2)); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 3); /* waiting achievement should be set back to active */ + assert_hitcount(&runtime, 2, 0, 1, 0); + assert_hitcount(&runtime, 3, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 1, 0); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_reactivate_waiting() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + /* reset achievement 2 to waiting */ + rc_reset_trigger(find_trigger(&runtime, 2)); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reactivate achievement 2 */ + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 0); /* active achievement should be set back to waiting */ + assert_hitcount(&runtime, 2, 0, 1, 0); + assert_hitcount(&runtime, 3, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 1, 0); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_paused_and_primed() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + unsigned char buffer2[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0002=7_0xH0000=2_P:0xH0005=4"); + assert_activate_achievement(&runtime, 3, "0xH0003=9_0xH0000=3"); + assert_activate_achievement(&runtime, 4, "0xH0004=1_T:0xH0000=4"); + + assert_do_frame(&runtime, &memory); + ram[1] = 4; + ram[2] = 7; + ram[3] = 9; + ram[4] = 1; + ram[5] = 4; + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_PAUSED); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_PRIMED); + ASSERT_TRUE(find_trigger(&runtime, 2)->requirement->is_paused); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* unpause achievement 2 and unprime achievement 4 */ + ram[5] = 2; + ram[4] = 2; + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(find_trigger(&runtime, 2)->requirement->is_paused); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_PAUSED); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_PRIMED); + ASSERT_TRUE(find_trigger(&runtime, 2)->requirement->is_paused); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(find_trigger(&runtime, 2)->requirement->is_paused); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_deactivated_memrefs() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); + assert_activate_achievement(&runtime, 3, "0xH0001=6_0xH0000=3"); + + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 6; + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 0, 1); + + /* deactivate an achievement with memrefs - trigger should be nulled */ + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + ASSERT_TRUE(runtime.triggers[0].owns_memrefs); + rc_runtime_deactivate_achievement(&runtime, 1); + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reactivate achievement 1 */ + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=2"); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_WAITING); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 2, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 0, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_deactivated_no_memrefs() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); + assert_activate_achievement(&runtime, 3, "0xH0001=6_0xH0000=3"); + + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 6; + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 0, 1); + + /* deactivate an achievement without memrefs - trigger should be removed */ + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + ASSERT_FALSE(runtime.triggers[1].owns_memrefs); + rc_runtime_deactivate_achievement(&runtime, 2); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reactivate achievement 2 */ + assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 0, 0); + assert_hitcount(&runtime, 3, 0, 0, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_single_leaderboard() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_can_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_can_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_leaderboards() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_deserialize(&runtime, buffer); + + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_leaderboards_ignore_inactive() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + find_lboard(&runtime, 1)->state = RC_LBOARD_STATE_DISABLED; + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_DISABLED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + find_lboard(&runtime, 1)->state = RC_LBOARD_STATE_ACTIVE; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_deserialize(&runtime, buffer); + + /* non-serialized leaderboard should be reset */ + assert_sta_hitcount(&runtime, 1, 0, 0, 0); + assert_sub_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + /* serialized leaderboard should be restored */ + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_leaderboards_ignore_modified() +{ + unsigned char ram[] = { 2, 3, 6 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.3.::VAL:0xH0002"); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_deserialize(&runtime, buffer); + + /* modified leaderboard should be reset */ + assert_sta_hitcount(&runtime, 1, 0, 0, 0); + assert_sub_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + /* serialized leaderboard should be restored */ + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_none() +{ + unsigned char buffer[2048]; + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_static() +{ + unsigned char buffer[2048]; + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\nTest"); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_simple_lookup() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n@Number(p0xH02)"); + assert_do_frame(&runtime, &memory); /* prev[2] = 0 */ + + ram[2] = 4; + assert_do_frame(&runtime, &memory); /* prev[2] = 2 */ + + ram[2] = 8; + assert_do_frame(&runtime, &memory); /* prev[2] = 4 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + ram[2] = 12; + assert_do_frame(&runtime, &memory); /* prev[2] = 8 */ + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* deserialized should remember prev[2] = 4 */ + assert_richpresence_output(&runtime, &memory, "4"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_tracked_hits() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + assert_do_frame(&runtime, &memory); /* count = 3 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 4 */ + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* deserialized should remember count = 3 */ + assert_richpresence_output(&runtime, &memory, "3"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_tracked_hits_md5_changed() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + assert_do_frame(&runtime, &memory); /* count = 3 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 4 */ + + assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)!"); + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* md5 changed, but variable is stored external to RP, 3 should be remembered */ + assert_richpresence_output(&runtime, &memory, "3!"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_conditional_display() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three\nLess"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 3 */ + assert_do_frame(&runtime, &memory); /* count = 4 */ + assert_richpresence_output(&runtime, &memory, "Three"); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* deserialized should remember count = 2 */ + assert_richpresence_output(&runtime, &memory, "Less"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_conditional_display_md5_changed() +{ + unsigned char ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + unsigned char buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three\nLess"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 3 */ + assert_do_frame(&runtime, &memory); /* count = 4 */ + assert_richpresence_output(&runtime, &memory, "Three"); + + reset_runtime(&runtime); + assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three!\nLess"); + assert_deserialize(&runtime, buffer); + + /* md5 changed, hit count should be discarded */ + assert_richpresence_output(&runtime, &memory, "Less"); + + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + assert_richpresence_output(&runtime, &memory, "Less"); + + assert_do_frame(&runtime, &memory); /* count = 3 */ + assert_richpresence_output(&runtime, &memory, "Three!"); + + rc_runtime_destroy(&runtime); +} + +/* ======================================================== */ + +void test_runtime_progress(void) { + TEST_SUITE_BEGIN(); + + TEST(test_empty); + TEST(test_single_achievement); + TEST(test_invalid_marker); + TEST(test_invalid_memref_chunk_id); + TEST(test_modified_data); + TEST(test_single_achievement_deactivated); + TEST(test_single_achievement_md5_changed); + + TEST(test_no_core_group); + TEST(test_memref_shared_address); + TEST(test_memref_indirect); + TEST(test_memref_double_indirect); + + TEST(test_multiple_achievements); + TEST(test_multiple_achievements_ignore_triggered_and_inactive); + TEST(test_multiple_achievements_overwrite_waiting); + TEST(test_multiple_achievements_reactivate_waiting); + TEST(test_multiple_achievements_paused_and_primed); + TEST(test_multiple_achievements_deactivated_memrefs); + TEST(test_multiple_achievements_deactivated_no_memrefs); + + TEST(test_single_leaderboard); + TEST(test_multiple_leaderboards); + TEST(test_multiple_leaderboards_ignore_inactive); + TEST(test_multiple_leaderboards_ignore_modified); + + TEST(test_rich_presence_none); + TEST(test_rich_presence_static); + TEST(test_rich_presence_simple_lookup); + TEST(test_rich_presence_tracked_hits); + TEST(test_rich_presence_tracked_hits_md5_changed); + TEST(test_rich_presence_conditional_display); + TEST(test_rich_presence_conditional_display_md5_changed); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_timing.c b/src/rcheevos/test/rcheevos/test_timing.c new file mode 100644 index 000000000..5490824b9 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_timing.c @@ -0,0 +1,166 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "mock_memory.h" + +#include "../test_framework.h" + +static rc_runtime_t runtime; +static int trigger_count[128]; + +static void event_handler(const rc_runtime_event_t* e) +{ + if (e->type == RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED) + { + rc_trigger_t* trigger = rc_runtime_get_achievement(&runtime, e->id); + trigger_count[e->id]++; + + rc_reset_trigger(trigger); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + } +} + +static void _assert_activate_achievement(rc_runtime_t* runtime, unsigned int id, const char* memaddr) +{ + int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") + +static void do_timing(void) +{ + unsigned char ram[256], last, next, bitcount; + memory_t memory; + int i, j, mask; + clock_t total_clocks = 0, start, end; + double elapsed, average; + + memory.ram = ram; + memory.size = sizeof(ram); + memset(&ram[0], 0, sizeof(ram)); + for (i = 0; i < 0x3F; i++) + ram[0xC0 + i] = i; + + memset(&trigger_count, 0, sizeof(trigger_count)); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0001=1"); + assert_activate_achievement(&runtime, 3, "0xH0001=0"); + assert_activate_achievement(&runtime, 4, "0xH0002!=d0xH0002"); + assert_activate_achievement(&runtime, 5, + "A:0xH0000_A:0xH0001_A:0xH0002_A:0xH0003_A:0xH0004_A:0xH0005_A:0xH0006_A:0xH0007_" + "A:0xH0008_A:0xH0009_A:0xH000A_A:0xH000B_A:0xH000C_A:0xH000D_A:0xH000E_0xH000F=0xH0084"); + assert_activate_achievement(&runtime, 6, + "A:0xH0000_A:0xH0001_A:0xH0002_A:0xH0003_A:0xH0004_A:0xH0005_A:0xH0006_0xH0007=0xK0080_" + "A:0xH0008_A:0xH0009_A:0xH000A_A:0xH000B_A:0xH000C_A:0xH000D_A:0xH000E_0xH000F=0xK0081"); + assert_activate_achievement(&runtime, 7, "I:0xH0024_0xL00C0>8"); + assert_activate_achievement(&runtime, 8, "O:0xH0000=1_O:0xH0001=1_O:0xH0002=1_O:0xH0003=1_O:0xH0004=1_O:0xH0005=1_O:0xH0006=1_0xH0007=1"); + assert_activate_achievement(&runtime, 9, "N:0xH0000=1_N:0xH0001=1_N:0xH0002=1_N:0xH0003=1_N:0xH0004=1_N:0xH0005=1_N:0xH0006=1_0xH0007=1"); + assert_activate_achievement(&runtime, 10, "0xH008A>0xH008B_0xH008A<0xH0089"); + assert_activate_achievement(&runtime, 11, + "0xH0040=0xH0060_0xH0041=0xH0061_0xH0042=0xH0062_0xH0043=0xH0063_" + "0xH0044=0xH0064_0xH0045=0xH0065_0xH0046=0xH0066_0xH0047=0xH0067_" + "0xH0048=0xH0068_0xH0049=0xH0069_0xH004A=0xH006A_0xH004B=0xH006B_" + "0xH004C=0xH006C_0xH004D=0xH006D_0xH004E=0xH006E_0xH004F=0xH006F"); + assert_activate_achievement(&runtime, 12, "0xH0003=1.3._R:0xH0004=1_P:0xH0005=1"); + assert_activate_achievement(&runtime, 13, "0xH0003=1.3.SR:0xH0004=1_P:0xH0005=1"); + assert_activate_achievement(&runtime, 14, "I:0xH0024_0xH0040>d0xH0040"); + assert_activate_achievement(&runtime, 15, "b0xH0085=0xH0080"); + assert_activate_achievement(&runtime, 16, "0xH0026=0xH0028S0xH0023=0xH0027S0xH0025=0xH0022"); + assert_activate_achievement(&runtime, 17, + "C:0xH0000!=0_C:0xH0001!=0_C:0xH0002!=0_C:0xH0003!=0_C:0xH0004!=0_C:0xH0005!=0_C:0xH0006!=0_C:0xH0007!=0_" + "C:0xH0008!=0_C:0xH0009!=0_C:0xH000A!=0_C:0xH000B!=0_C:0xH000C!=0_C:0xH000D!=0_C:0xH000E!=0_0xH000F!=0.20."); + assert_activate_achievement(&runtime, 18, "A:0xL0087*10000_A:0xU0086*1000_A:0xL0086*100_A:0xU0085*10_0xL0x0085=0x 0080"); + + /* we expect these to only be true initially, so forcibly change them from WAITING to ACTIVE */ + rc_runtime_get_achievement(&runtime, 5)->state = RC_TRIGGER_STATE_ACTIVE; + rc_runtime_get_achievement(&runtime, 6)->state = RC_TRIGGER_STATE_ACTIVE; + rc_runtime_get_achievement(&runtime, 15)->state = RC_TRIGGER_STATE_ACTIVE; + rc_runtime_get_achievement(&runtime, 18)->state = RC_TRIGGER_STATE_ACTIVE; + + for (i = 0; i < 65536; i++) + { + /* + * $00-$1F = individual bits of i + * $20-$3F = number of times individual bits changed + * $40-$5F = number of times individual bits were 0 + * $60-$7F = number of times individual bits were 1 + * $80-$83 = i + * $84 = bitcount i + * $85-$87 = bcd i (LE) + * $88-$8C = ASCII i + * $C0-$FF = 0-63 + */ + mask = 1; + bitcount = 0; + for (j = 0; j < 32; j++) + { + next = ((i & mask) != 0) * 1; + mask <<= 1; + last = ram[j]; + + ram[j] = next; + ram[j + 0x60] += next; + bitcount += next; + ram[j + 0x40] += (1 - next); + ram[j + 0x20] += (next != last) * 1; + } + ram[0x80] = i & 0xFF; + ram[0x81] = (i >> 8) & 0xFF; + ram[0x82] = (i >> 16) & 0xFF; + ram[0x83] = (i >> 24) & 0xFF; + ram[0x84] = bitcount; + ram[0x85] = (i % 10) | ((i / 10) % 10) << 4; + ram[0x86] = ((i / 100) % 10) | ((i / 1000) % 10) << 4; + ram[0x87] = ((i / 10000) % 10); + ram[0x88] = ((i / 10000) % 10) + '0'; + ram[0x89] = ((i / 1000) % 10) + '0'; + ram[0x8A] = ((i / 100) % 10) + '0'; + ram[0x8B] = ((i / 10) % 10) + '0'; + ram[0x8C] = (i % 10) + '0'; + + start = clock(); + rc_runtime_do_frame(&runtime, event_handler, peek, &memory, NULL); + end = clock(); + + total_clocks += (end - start); + } + + elapsed = (double)total_clocks * 1000 / CLOCKS_PER_SEC; + average = elapsed * 1000 / i; + printf("\n%0.6fms elapsed, %0.6fus average", elapsed, average); + + ASSERT_NUM_EQUALS(trigger_count[ 0], 0); /* no achievement 0 */ + ASSERT_NUM_EQUALS(trigger_count[ 1], 32768); /* 0xH0000 = 1 every other frame */ + ASSERT_NUM_EQUALS(trigger_count[ 2], 32768); /* 0xH0001 = 1 2 / 4 frames */ + ASSERT_NUM_EQUALS(trigger_count[ 3], 32766); /* 0xH0001 = 0 2 / 4 frames (won't trigger until after first time it's 1) */ + ASSERT_NUM_EQUALS(trigger_count[ 4], 16383); /* 0xH0002!=d0xH0002 every two frames (no delta for first frame) */ + ASSERT_NUM_EQUALS(trigger_count[ 5], 65536); /* sum of bits = bitcount (16-bit) */ + ASSERT_NUM_EQUALS(trigger_count[ 6], 65536); /* sum of bits = bitcount (8-bit) */ + ASSERT_NUM_EQUALS(trigger_count[ 7], 6912); /* I:0xH0024_0xL00C0>8 pointer advances every eight frames */ + ASSERT_NUM_EQUALS(trigger_count[ 8], 65280); /* number of times any bit is set in lower byte */ + ASSERT_NUM_EQUALS(trigger_count[ 9], 256); /* number of times all bits are set in lower byte */ + ASSERT_NUM_EQUALS(trigger_count[10], 7400); /* hundreds digit less than thousands, but greater than tens */ + ASSERT_NUM_EQUALS(trigger_count[11], 256); /* number of times individual bits were 0 or 1 is equal */ + ASSERT_NUM_EQUALS(trigger_count[12], 2048); /* number of times bit3 was set without bit 4 or 5 */ + ASSERT_NUM_EQUALS(trigger_count[13], 3071); /* number of times bit3 was set without bit 4 xor 5 */ + ASSERT_NUM_EQUALS(trigger_count[14], 10350); /* I:0xH0024_0xH0040>d0xH0040 - delta increase across moving target */ + ASSERT_NUM_EQUALS(trigger_count[15], 1100); /* BCD check (only valid for two digits) */ + ASSERT_NUM_EQUALS(trigger_count[16], 24); /* random collection of bit changes being equal */ + ASSERT_NUM_EQUALS(trigger_count[17], 22187); /* number of times 20 bits or more are set across frames */ + ASSERT_NUM_EQUALS(trigger_count[18], 65536); /* BCD reconstruction */ + + rc_runtime_destroy(&runtime); +} + +void test_timing(void) { + TEST_SUITE_BEGIN(); + TEST(do_timing); + TEST(do_timing); + TEST(do_timing); + TEST(do_timing); + TEST(do_timing); + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_trigger.c b/src/rcheevos/test/rcheevos/test_trigger.c new file mode 100644 index 000000000..001f6298a --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_trigger.c @@ -0,0 +1,1961 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_parse_trigger(rc_trigger_t** trigger, void* buffer, const char* memaddr) +{ + int size; + unsigned* overflow; + + size = rc_trigger_size(memaddr); + ASSERT_NUM_GREATER(size, 0); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + *trigger = rc_parse_trigger(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(*trigger); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_parse_trigger(trigger, buffer, memaddr) ASSERT_HELPER(_assert_parse_trigger(trigger, buffer, memaddr), "assert_parse_trigger") + +static void _assert_evaluate_trigger(rc_trigger_t* trigger, memory_t* memory, int expected_result) { + int result = rc_test_trigger(trigger, peek, memory, NULL); + ASSERT_NUM_EQUALS(result, expected_result); +} +#define assert_evaluate_trigger(trigger, memory, expected_result) ASSERT_HELPER(_assert_evaluate_trigger(trigger, memory, expected_result), "assert_evaluate_trigger") + +static rc_condition_t* trigger_get_cond(rc_trigger_t* trigger, int group_index, int cond_index) { + rc_condset_t* condset = trigger->requirement; + rc_condition_t* cond; + + if (group_index != 0) { + --group_index; + condset = trigger->alternative; + while (group_index-- != 0) { + if (condset == NULL) + break; + + condset = condset->next; + } + } + + if (condset == NULL) + return NULL; + + cond = condset->conditions; + while (cond_index-- != 0) { + if (cond == NULL) + break; + + cond = cond->next; + } + + return cond; +} + +static void _assert_hit_count(rc_trigger_t* trigger, int group_index, int cond_index, unsigned expected_hit_count) { + rc_condition_t* cond = trigger_get_cond(trigger, group_index, cond_index); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hit_count); +} +#define assert_hit_count(trigger, group_index, cond_index, expected_hit_count) ASSERT_HELPER(_assert_hit_count(trigger, group_index, cond_index, expected_hit_count), "assert_hit_count") + +static int evaluate_trigger(rc_trigger_t* self, memory_t* memory) { + return rc_evaluate_trigger(self, peek, memory, NULL); +} + +/* ======================================================== */ + +static void test_alt_groups() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=16S0xH0002=52S0xL0004=6"); + + /* core not true, both alts are */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 1, 0, 1); + assert_hit_count(trigger, 2, 0, 1); + + /* core and both alts true */ + ram[1] = 16; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 1, 0, 2); + assert_hit_count(trigger, 2, 0, 2); + + /* core and first alt true */ + ram[4] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2); + assert_hit_count(trigger, 1, 0, 3); + assert_hit_count(trigger, 2, 0, 2); + + /* core true, but neither alt */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 3); + assert_hit_count(trigger, 1, 0, 3); + assert_hit_count(trigger, 2, 0, 2); + + /* core and second alt true */ + ram[4] = 6; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 4); + assert_hit_count(trigger, 1, 0, 3); + assert_hit_count(trigger, 2, 0, 3); +} + +static void test_empty_core() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "S0xH0002=2S0xL0004=4"); + + /* core implicitly true, neither alt true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 0); + assert_hit_count(trigger, 2, 0, 0); + + /* first alt true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 1); + assert_hit_count(trigger, 2, 0, 0); + + /* both alts true */ + ram[4] = 4; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 2); + assert_hit_count(trigger, 2, 0, 1); + + /* second alt true */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 2); + assert_hit_count(trigger, 2, 0, 2); +} + +static void test_empty_alt() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0002=2SS0xL0004=4"); + + /* core false, first alt implicitly true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 2, 0, 0); + + /* core true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 2, 0, 0); + + /* core and both alts true */ + ram[4] = 4; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2); + assert_hit_count(trigger, 2, 0, 1); +} + +static void test_empty_last_alt() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0002=2S0xL0004=4S"); + + /* core false, second alt implicitly true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* core true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 1, 0, 0); + + /* core and both alts true */ + ram[4] = 4; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2); + assert_hit_count(trigger, 1, 0, 1); +} + +static void test_empty_all_alts() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0002=2SS"); + + /* core false, all alts implicitly true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + + /* core true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); +} + +static void test_resetif_in_alt_group() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18(1)_R:0xH0000=1S0xH0002=52(1)S0xL0004=6(1)_R:0xH0000=2"); + + /* all conditions true, no resets */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + + /* reset in core group resets everything */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 0, 0U); + + /* all conditions true, no resets */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + + /* reset in alt group resets everything */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 0, 0U); +} + +static void test_pauseif_in_alt_group() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_P:0xH0000=1S0xH0002=52S0xL0004=6_P:0xH0000=2"); + + /* all conditions true, no resets */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + + /* pause in core group only pauses core group */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 2U); + + /* unpaused */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 3U); + + /* pause in alt group only pauses alt group */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + assert_hit_count(trigger, 1, 0, 4U); + assert_hit_count(trigger, 2, 0, 3U); +} + +static void test_pauseif_resetif_in_alt_group() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=0.1._0xH0003=2SP:0xH0001=18_R:0xH0002=52"); + + /* capture hitcount */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* prevent future hit counts */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* unpause alt group, hit count should be reset */ + ram[1] = 16; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + + /* repause alt group, capture hitcount */ + ram[0] = 0; + ram[1] = 18; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* trigger condition. alt group is paused, so should be considered false */ + ram[3] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* trigger condition. alt group is unpaused, so reset will prevent trigger */ + ram[1] = 16; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + + /* trigger condition. alt group is unpaused, and not resetting, so allow trigger */ + ram[2] = 30; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); +} + +static void test_pauseif_hitcount_with_reset() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_P:0xH0002=52.1._R:0xH0003=1SR:0xH0003=2"); + + /* pauseif triggered, non-pauseif conditions ignored */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* pause condition is no longer true, but hitcount keeps it paused */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* resetif in paused group is ignored */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* resetif in alternate group is honored, active resetif prevents trigger */ + ram[3] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 0); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* resetif no longer active, pause not active, first condition true, trigger activates */ + ram[3] = 3; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 0, 1, 0); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); +} + +static void test_measured() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)"); + ASSERT_NUM_EQUALS(trigger->measured_as_percent, 0); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented to reach target */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - target previously met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_as_percent() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) */ + assert_parse_trigger(&trigger, buffer, "G:0xH0002=52(3)"); + ASSERT_NUM_EQUALS(trigger->measured_as_percent, 1); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented to reach target */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - target previously met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_comparison() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(byte(2) >= 80) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002>=80"); + + /* condition is not true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0x34U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is still not true */ + ram[2] = 79; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 79U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true - value matches */ + ram[2] = 80; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 80U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true - value exceeds */ + ram[2] = 255; + trigger->state = RC_TRIGGER_STATE_ACTIVE; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 255U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); +} + +static void test_measured_addhits() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(5, byte(1) == 10 || byte(2) == 10)) */ + assert_parse_trigger(&trigger, buffer, "C:0xH0001=10_M:0xH0002=10(5)"); + + /* neither is true - hit count should not be captured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second is true - hit count should be incremented by one */ + ram[2] = 10; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* both are true - hit count should be incremented by two */ + ram[1] = 10; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* only first is true - hit count should be incremented by one */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* neither is true - hit count should not increment */ + ram[1] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first is true - hit count should be incremented by one and trigger */ + ram[1] = 10; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); +} + +static void test_measured_indirect() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(byte(0) + 2) == 52)) */ + assert_parse_trigger(&trigger, buffer, "I:0xH0000_M:0xH0002=52(3)"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is no longer true - hit count should not be incremented */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is no longer true - hit count should not be incremented */ + ram[2] = 30; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_multiple() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple measured conditions are only okay if they all have the same target, in which + * case, the maximum of all the measured values is returned */ + + /* measured(repeated(3, byte(2) == 52)) || measured(repeated(3, byte(3) == 17)) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(3)SM:0xH0003=17(3)"); + + /* first condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* second condition is true - second hit count should be incremented - both will be the same */ + ram[2] = 9; + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* second condition still true - second hit count should be incremented and become prominent */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* switch back to first condition */ + ram[2] = 52; + ram[3] = 8; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* first hit count will be incremented and target met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* both true, only second will increment as first target is met */ + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* both true, both targets met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_multiple_with_hitcount_in_core() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple measured conditions are only okay if they all have the same target, in which + * case, the maximum of all the measured values is returned */ + + /* repeated(7, byte(1) == 18) && (measured(repeated(3, byte(2) == 52)) || measured(repeated(3, byte(3) == 17))) */ + assert_parse_trigger(&trigger, buffer, "0xH0001=18(7)SM:0xH0002=52(3)SM:0xH0003=17(3)"); + + /* first condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* second condition is true - second hit count should be incremented - both will be the same */ + /* core hit target is greater than any measured value, but should not be measured */ + ram[2] = 9; + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_while_paused() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* unpaused - hit count should be incremented */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); +} + +static void test_measured_while_paused_multiple() { + unsigned char ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* (measured(repeated(6, byte(2) == 52)) && unless(bit0(1) == 1)) || (measured(repeated(6, byte(0) == 0)) && unless(bit1(1) == 1)) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(6)_P:0xM0001=1SM:0xH0000=0(6)_P:0xN0001=1"); + + /* both alts should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* first alt paused - second should update */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* first still paused - second should update again */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* both paused - neither should update - expect last measured value to be kept */ + ram[1] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* first unpaused - it will update, measured will use the active value */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* both paused - neither should update - expect last measured value to be kept */ + ram[1] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* both unpaused - both updated, will use higher */ + ram[1] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 4U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); +} + +static void test_measured_while_paused_reset_alt() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1SR:0xH0003=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* reset - hit count should be cleared */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* unpaused, reset still true, hit count should remain cleared */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* reset not true - hit count should be incremented */ + ram[3] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); +} + +static void test_measured_while_paused_reset_core() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ + assert_parse_trigger(&trigger, buffer, "R:0xH0003=1SM:0xH0002=52(3)_P:0xH0001=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* reset - hit count should be cleared */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* unpaused, reset still true, hit count should remain cleared */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* reset not true - hit count should be incremented */ + ram[3] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); +} + +static void test_measured_while_paused_reset_non_hitcount() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=99_P:0xH0001=1SR:0xH0003=1"); + + /* initial value */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + ASSERT_NUM_EQUALS(trigger->measured_target, 99U); + + /* paused - capture value and return that*/ + ram[1] = 1; + ram[2] = 60; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + + /* reset - captured value should not be cleared */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + + /* unpaused, reset still true, value should reflect current */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 60U); +} + +static void test_measured_reset_hitcount() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && never(byte(3) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1_R:0xH0003=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* reset primed, but ignored by pause */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* unpaused, reset should clear value */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* no longer reset, hit count should increment */ + ram[3] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + + /* reset again, hit count should go back to 0 */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); +} + +static void test_measured_reset_comparison() { + unsigned char ram[] = {0x00, 0x12, 0x02, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(byte(2) >= 10) && unless(byte(1) == 1) && never(byte(3) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002>=10_P:0xH0001=1_R:0xH0003=1"); + + /* condition is true - measured will come from value */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 10U); + + /* condition is true - value updated */ + ram[2] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + + /* paused - updated value should be ignored */ + ram[1] = 1; + ram[2] = 4; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + + /* reset primed, but ignored by pause */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + + /* unpaused, reset should not affect non-hitcount measurement */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + + /* no longer reset, value updated */ + ram[3] = 0; + ram[2] = 5; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + + /* reset again, should not affect non-hitcount measurement */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); +} + +static void test_measured_if() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52), when=byte(0) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_Q:0xH0000=1"); + + /* condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented and measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented to reach target, but it's not measured */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - target previously met, but now it's measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_if_comparison() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(byte(2) >= 80, when=byte(0)==1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002>=80_Q:0xH0000=1"); + + /* condition is not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition not true, but measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0x34U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is still not true, but measured */ + ram[2] = 79; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 79U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true, but not measured */ + ram[0] = 0; + ram[2] = 80; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true and measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 80U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); +} + +static void test_measured_if_multiple_measured() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple measured conditions are only okay if they all have the same target, in which + * case, the maximum of all the measured values is returned */ + + /* measured(repeated(5, byte(2) == 52), when=byte(0)=1) || measured(repeated(5, byte(3) == 17), when=byte(0)=2) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(5)_Q:0xH0000=1SM:0xH0003=17(5)_Q:0xH0000=2"); + + /* first condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition is true - second hit count should be incremented - both will be the same; still not measured */ + ram[2] = 9; + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition still true - second hit count should be incremented and become prominent, but first is measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* switch back to first condition */ + ram[2] = 52; + ram[3] = 8; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first hit count will be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first hit count will be incremented, but neither measured */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 4U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first will increment to trigger state, but it's not measured - second is */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 5U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first is measured and will trigger */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 5U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); +} + +static void test_measured_if_multiple_measured_if() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(5, byte(2) == 52), when=byte(0)=1 && byte(1)==1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(5)_Q:0xH0000=1_Q:0xH0001=1"); + + /* first condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition is true - hit count still incremented, but not measured because of third condition */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* third condition is true, measured should be measured */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition no longer true, measured ignored */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 4U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* hit target met, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 5U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* hit target met, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 5U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition true, measured should be measured, trigger will fire */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 5U); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); +} + +static void test_measured_if_while_paused() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52), when=byte(0)==1) && unless(byte(1) == 1) */ + /* NOTE: this test also verifies the behavior when the MeasuredIf is first */ + assert_parse_trigger(&trigger, buffer, "Q:0xH0000=1_M:0xH0002=52(3)_P:0xH0001=1"); + + /* condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* paused - but measured - measured_value is not updated when paused */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* unpaused - hit count should be incremented and measured value captured */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented, and last hit count should be measured */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused but not measured - pause will prevent evaluation of MeasuredIf, so measured retained */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); +} + +static void test_resetnextif_trigger() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf byte(0x0002)=1 + * byte(0x0001)=1 (1) + * Trigger byte(0x0003)=0 + */ + assert_parse_trigger(&trigger, buffer, "Z:0xH0002=1_0xH0001=1.1._T:0xH0003=0"); + + /* both conditions false */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); +} + +static void test_evaluate_trigger_inactive() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_INACTIVE; + + /* Inactive is a permanent state - trigger is initially true */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ram[2] = 24; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* Trigger no longer true, still inactive */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* hits should not be tallied when inactive */ + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 0U); + + /* memrefs should be updated while inactive */ + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.value, 24U); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.changed, 0); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.prior, 52U); + + /* reset should be ignored while inactive */ + ram[4] = 4; + trigger_get_cond(trigger, 0, 0)->current_hits = 1U; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + assert_hit_count(trigger, 0, 0, 1U); +} + +static void test_evaluate_trigger_waiting() { + unsigned char ram[] = {0x00, 0x12, 0x18, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_WAITING; + + /* trigger is ready to fire, but won't as long as its waiting */ + /* prevents triggers from uninitialized memory */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + ram[2] = 16; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + + /* waiting trigger should not tally hits */ + ASSERT_FALSE(trigger->has_hits); + + /* ResetIf makes the trigger state false, so the trigger should become active */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* reset to previous state */ + trigger->state = RC_TRIGGER_STATE_WAITING; + ram[4] = 9; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + ASSERT_FALSE(trigger->has_hits); + + /* trigger is no longer true, proceed to active state */ + ram[1] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 1U); +} + +static void test_evaluate_trigger_reset() { + unsigned char ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* generate a hit count */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetIf that resets hits returns RESET, but doesn't change the state */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(trigger->has_hits); + + /* ResetIf that doesn't resets hits doesn't return RESET */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(trigger->has_hits); +} + +static void test_evaluate_trigger_reset_next() { + unsigned char ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "Z:0xL0004=4_0xH0001=5.2._0xH0003=3"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* generate a hit count */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext that resets hits returns RESET, but doesn't change the state */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); /* ResetNext will have a hit */ + + /* ResetNext that doesn't resets hits doesn't return RESET */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); /* ResetNext will have a hit */ + + /* Secondary hit should still be tallied, ResetNext that doesn't reset hits doesn't return RESET */ + ram[3] = 3; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext no longer true, tally hit */ + ram[4] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext that resets hits returns RESET, but doesn't reset the secondary hits */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext no longer true, tally hit */ + ram[4] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* tally second hit to trigger */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_triggered() { + unsigned char ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* transition to TRIGGERED */ + ram[1] = 18; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 1U); + + /* triggered trigger remains triggered, but returns INACTIVE and does not increment hit counts */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 1U); + + /* triggered trigger remains triggered when no longer true */ + ram[1] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 1U); + + /* triggered trigger does not update deltas */ + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.value.memref->value.value, 18U); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.value.memref->value.changed, 1U); +} + +static void test_evaluate_trigger_paused() { + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0003=171_P:0xH0002=1SR:0xH0004=4"); + + /* INACTIVE is a permanent state - trigger is initially true */ + trigger->state = RC_TRIGGER_STATE_INACTIVE; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* PauseIf is ignored when INACTIVE */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* unpause, switch to WAITING, ready to trigger, so will stay WAITING */ + ram[2] = 2; + trigger->state = RC_TRIGGER_STATE_WAITING; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + + /* PauseIf makes the evaluation false, so will transition to ACTIVE, but PAUSED */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + ASSERT_TRUE(trigger->has_hits); /* the PauseIf has a hit */ + assert_hit_count(trigger, 0, 0, 0U); + + /* hitcounts will update when unpaused; adjust memory so trigger is no longer true */ + ram[2] = 2; + ram[3] = 99; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 1U); + + /* hitcounts should remain while paused */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + ASSERT_TRUE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 1U); + + /* ResetIf while paused should notify, but not change state */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PAUSED); + ASSERT_FALSE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 0U); + + /* ResetIf without hitcounts should return current state */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + + /* trigger while paused is ignored */ + ram[4] = 0; + ram[3] = 171; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + + /* trigger should file when unpaused */ + ram[2] = 2; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + + /* triggered trigger ignore pause */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_primed() { + unsigned char ram[] = {0x00, 0x01, 0x00, 0x01, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1_T:0xH0001=1_0xH0002=1_T:0xH0003=1_0xH0004=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* T (trigger) conditions are true, but nothing else */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* one non-trigger condition is still false */ + ram[0] = ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* all non-trigger conditions are true, one trigger condition is not true */ + ram[1] = 0; ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* non-trigger condition is false again */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* all conditions are true */ + ram[0] = ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + + /* one non-trigger condition is false */ + ram[3] = 0; + trigger->state = RC_TRIGGER_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* all conditions are true */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_primed_in_alts() { + unsigned char ram[] = {0x01, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1_0xH0002=1ST:0xH0003=1_0xH0004=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* core is true, but neither alt is primed */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* both alts primed */ + ram[2] = ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* only second alt is primed */ + ram[4] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* neither alt is primed */ + ram[2] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* both alts primed */ + ram[2] = ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* alt 2 is true */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_primed_one_alt() { + unsigned char ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1S0xH0002=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* core must be true for trigger to be primed */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* second alt is true, but core is not */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* first alt is true, but core is not */ + ram[2] = 0; ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* only core is true, first alt is marked as Trigger, eligible to fire */ + ram[1] = 0; ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* alt is true */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_disabled() { + unsigned char ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1S0xH0002=1"); + trigger->state = RC_TRIGGER_STATE_DISABLED; + + /* state stays DISABLED, but evaluate returns INACTIVE */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_DISABLED); +} + +static void test_evaluate_trigger_chained_resetnextif() { + unsigned char ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* once(byte(4)==1 && never(repeated(2, byte(3)==1 && never(byte(1)==1 || byte(2)==1))) && trigger_when(byte(0)==1) */ + assert_parse_trigger(&trigger, buffer, "O:0xH0001=1_Z:0xH0002=1_Z:0xH0003=1.2._0xH0004=1.1._T:0xH0000=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* nothing is true */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + assert_hit_count(trigger, 0, 0, 0); /* OrNext 0x0001 == 1 */ + assert_hit_count(trigger, 0, 1, 0); /* ResetNextIf 0x0002 == 1 */ + assert_hit_count(trigger, 0, 2, 0); /* ResetNextIf 0x0003 == 1 (2) */ + assert_hit_count(trigger, 0, 3, 0); /* 0x0004 == 1 (1) */ + assert_hit_count(trigger, 0, 4, 0); /* Trigger 0x0000 == 1 */ + + /* non-trigger condition is true */ + ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 0, 3, 1); + + /* second ResetNextIf is true */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 0, 2, 1); + assert_hit_count(trigger, 0, 3, 1); + + /* OrNext resets second ResetNextIf */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); /* result is RESET */ + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); /* state is PRIMED */ + assert_hit_count(trigger, 0, 0, 1); /* OrNext tallies a hit of its own */ + assert_hit_count(trigger, 0, 1, 1); /* ResetNextIf gets a hit from the OrNext */ + assert_hit_count(trigger, 0, 2, 0); /* hit is reset by the ResetNextIf */ + assert_hit_count(trigger, 0, 3, 1); /* hit is not affected by the reset ResetNextIf */ + + /* OrNext no longer true */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 1); + assert_hit_count(trigger, 0, 3, 1); + + /* second ResetNextIf fires */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 2); + assert_hit_count(trigger, 0, 3, 0); +} + +static void test_prev_prior_share_memref() { + rc_trigger_t* trigger; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xH0001=d0xH0001_0xH0001!=p0xH0001"); + + ASSERT_NUM_EQUALS(trigger->memrefs->address, 1U); + ASSERT_NUM_EQUALS(trigger->memrefs->value.size, RC_MEMSIZE_8_BITS); + ASSERT_PTR_NULL(trigger->memrefs->next); + + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand2.type, RC_OPERAND_DELTA); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand2.type, RC_OPERAND_PRIOR); +} + +static void test_bit_lookups_share_memref() { + rc_trigger_t* trigger; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xM0001=1_0xN0x0001=0_0xO0x0001=1"); + + ASSERT_NUM_EQUALS(trigger->memrefs->address, 1U); + ASSERT_NUM_EQUALS(trigger->memrefs->value.size, RC_MEMSIZE_8_BITS); + ASSERT_PTR_NULL(trigger->memrefs->next); + + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.size, RC_MEMSIZE_BIT_0); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.size, RC_MEMSIZE_BIT_1); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 2)->operand1.size, RC_MEMSIZE_BIT_2); +} + +static void test_bitcount_shares_memref() { + rc_trigger_t* trigger; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xH0001>5_0xK0001!=3"); + + ASSERT_NUM_EQUALS(trigger->memrefs->address, 1U); + ASSERT_NUM_EQUALS(trigger->memrefs->value.size, RC_MEMSIZE_8_BITS); + ASSERT_PTR_NULL(trigger->memrefs->next); + + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.size, RC_MEMSIZE_BITCOUNT); +} + +static void test_large_memref_not_shared() { + rc_trigger_t* trigger; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xH1234=1_0xX1234>d0xX1234"); + + /* this could be shared, but isn't currently */ + ASSERT_NUM_EQUALS(trigger->memrefs->address, 0x1234); + ASSERT_NUM_EQUALS(trigger->memrefs->value.size, RC_MEMSIZE_8_BITS); + ASSERT_PTR_NOT_NULL(trigger->memrefs->next); + + ASSERT_NUM_EQUALS(trigger->memrefs->next->address, 0x1234); + ASSERT_NUM_EQUALS(trigger->memrefs->next->value.size, RC_MEMSIZE_32_BITS); + ASSERT_PTR_NULL(trigger->memrefs->next->next); +} + +/* ======================================================== */ + +void test_trigger(void) { + TEST_SUITE_BEGIN(); + + /* alt groups */ + TEST(test_alt_groups); + TEST(test_empty_core); + TEST(test_empty_alt); + TEST(test_empty_last_alt); + TEST(test_empty_all_alts); + + /* resetif */ + TEST(test_resetif_in_alt_group); + + /* pauseif */ + TEST(test_pauseif_in_alt_group); + TEST(test_pauseif_resetif_in_alt_group); + TEST(test_pauseif_hitcount_with_reset); + + /* measured */ + TEST(test_measured); + TEST(test_measured_as_percent); + TEST(test_measured_comparison); + TEST(test_measured_addhits); + TEST(test_measured_indirect); + TEST(test_measured_multiple); + TEST(test_measured_multiple_with_hitcount_in_core); + TEST(test_measured_while_paused); + TEST(test_measured_while_paused_multiple); + TEST(test_measured_while_paused_reset_alt); + TEST(test_measured_while_paused_reset_core); + TEST(test_measured_while_paused_reset_non_hitcount); + TEST(test_measured_reset_hitcount); + TEST(test_measured_reset_comparison); + TEST(test_measured_if); + TEST(test_measured_if_comparison); + TEST(test_measured_if_multiple_measured); + TEST(test_measured_if_multiple_measured_if); + TEST(test_measured_if_while_paused); + + /* trigger */ + TEST(test_resetnextif_trigger); + + /* rc_evaluate_trigger */ + TEST(test_evaluate_trigger_inactive); + TEST(test_evaluate_trigger_waiting); + TEST(test_evaluate_trigger_reset); + TEST(test_evaluate_trigger_reset_next); + TEST(test_evaluate_trigger_triggered); + TEST(test_evaluate_trigger_paused); + TEST(test_evaluate_trigger_primed); + TEST(test_evaluate_trigger_primed_in_alts); + TEST(test_evaluate_trigger_primed_one_alt); + TEST(test_evaluate_trigger_disabled); + TEST(test_evaluate_trigger_chained_resetnextif); + + /* memref sharing */ + TEST(test_prev_prior_share_memref); + TEST(test_bit_lookups_share_memref); + TEST(test_bitcount_shares_memref); + TEST(test_large_memref_not_shared); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_value.c b/src/rcheevos/test/rcheevos/test_value.c new file mode 100644 index 000000000..c91aa25e3 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_value.c @@ -0,0 +1,598 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void test_evaluate_value(const char* memaddr, int expected_value) { + rc_value_t* self; + /* bytes 5-8 are the float value for pi */ + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; + memory_t memory; + char buffer[2048]; + unsigned* overflow; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + overflow = (unsigned*)(((char*)buffer) + ret); + *overflow = 0xCDCDCDCD; + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + ret = rc_evaluate_value(self, peek, &memory, NULL); + ASSERT_NUM_EQUALS(ret, expected_value); +} + +static void test_invalid_value(const char* memaddr, int expected_error) { + int ret = rc_value_size(memaddr); + ASSERT_NUM_EQUALS(ret, expected_error); +} + +static void test_measured_value_target(const char* memaddr, int expected_target) { + rc_value_t* self; + char buffer[2048]; + unsigned* overflow; + int ret; + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + overflow = (unsigned*)(((char*)buffer) + ret); + *overflow = 0xCDCDCDCD; + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + ASSERT_NUM_EQUALS(self->conditions->conditions->required_hits, expected_target); +} + +static void test_evaluate_measured_value_with_pause() { + rc_value_t* self; + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + char buffer[2048]; + const char* memaddr = "P:0xH0003=hAB_M:0xH0002!=d0xH0002"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + /* should initially be paused, no hits captured */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* pause should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* unpause should not report the change that occurred while paused */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* pause should return current hitcount */ + ram[3] = 0xAB; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* pause should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* unpause should not report the change that occurred while paused */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* additional hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 2); +} + +static void test_evaluate_measured_value_with_reset() { + rc_value_t* self; + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + char buffer[2048]; + const char* memaddr = "R:0xH0003=hAB_M:0xH0002!=d0xH0002"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + /* reset should initially be true, no hits captured */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset no longer true, change while reset shouldn't be captured */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* additional hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* reset should clear hit count */ + ram[3] = 0xAB; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset no longer true, change while reset shouldn't be captured */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* additional hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); +} + +static void init_typed_value(rc_typed_value_t* value, char type, unsigned u32, double f32) { + value->type = type; + + switch (type) { + case RC_VALUE_TYPE_UNSIGNED: + value->value.u32 = u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = (int)u32; + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = (float)f32; + break; + + case RC_VALUE_TYPE_NONE: + value->value.u32 = 0xCDCDCDCD; /* force uninitialized value */ + break; + + default: + break; + } +} + +static void _assert_typed_value(const rc_typed_value_t* value, char type, unsigned u32, double f32) { + ASSERT_NUM_EQUALS(value->type, type); + + switch (type) { + case RC_VALUE_TYPE_UNSIGNED: + ASSERT_NUM_EQUALS(value->value.u32, u32); + break; + + case RC_VALUE_TYPE_SIGNED: + ASSERT_NUM_EQUALS(value->value.i32, (int)u32); + break; + + case RC_VALUE_TYPE_FLOAT: + ASSERT_FLOAT_EQUALS(value->value.f32, (float)f32); + break; + + default: + break; + } +} +#define assert_typed_value(value, type, u32, f32) ASSERT_HELPER(_assert_typed_value(value, type, u32, f32), "assert_typed_value") + +static void test_typed_value_convert(char type, unsigned u32, double f32, char new_type, unsigned new_u32, double new_f32) { + rc_typed_value_t value; + init_typed_value(&value, type, u32, f32); + + rc_typed_value_convert(&value, new_type); + + assert_typed_value(&value, new_type, new_u32, new_f32); +} + +static void test_typed_value_conversion() { + /* unsigned source */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4294967295.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* signed source */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_FLOAT, 0, -1.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_FLOAT, 0, -1.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 3.14159); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 3, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, 3, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_FLOAT, 0, -3.14159); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFD, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-3, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_NONE, 0, 0); +} + +static void test_typed_value_add(char type, unsigned u32, double f32, + char amount_type, unsigned amount_u32, double amount_f32, unsigned result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_add(&value, &amount); + + if (type == RC_VALUE_TYPE_NONE) { + assert_typed_value(&value, amount_type, result_u32, result_f32); + } + else { + assert_typed_value(&value, type, result_u32, result_f32); + } +} + +static void test_typed_value_addition() { + /* no source */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 8, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, 0, 8.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 0xFFFFFFFE, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, (unsigned)-2, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, 0, -2.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 0.0); + + /* unsigned source */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 14, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, 14, 0.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, 4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, 6, 0.0); + + /* signed source */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, (unsigned)-4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, (unsigned)-4, 0.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 2, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, (unsigned)-8, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, (unsigned)-4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, (unsigned)-6, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, 0, 8.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, 8.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 0, 4294967300.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 0, 4.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, 0, -2.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 6.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, 0, 5.14159); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 0, 1.14159); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, 5.14159); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, 0, 9.16459); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, 0, -4.85841); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 3.14159); +} + +static void test_typed_value_multiply(char type, unsigned u32, double f32, + char amount_type, unsigned amount_u32, double amount_f32, + char result_type, unsigned result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_multiply(&value, &amount); + + assert_typed_value(&value, result_type, result_u32, result_f32); +} + +static void test_typed_value_multiplication() { + /* unsigned source */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, RC_VALUE_TYPE_UNSIGNED, 48, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, RC_VALUE_TYPE_FLOAT, 0, 48.0); + + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFF4, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFF4, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* signed source */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-12, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 25769803764.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -48.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 6.28318); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -6.28318); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 6.28318); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 18.92179657); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -25.13272); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); +} + +static void test_typed_value_divide(char type, unsigned u32, double f32, + char amount_type, unsigned amount_u32, double amount_f32, + char result_type, unsigned result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_divide(&value, &amount); + + assert_typed_value(&value, result_type, result_u32, result_f32); +} + +static void test_typed_value_division() { + /* division by zero */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* unsigned source */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_UNSIGNED, 3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.5, RC_VALUE_TYPE_FLOAT, 0, 2.4); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); + + /* signed source */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_SIGNED, 3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_SIGNED, 3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + + /* float source (whole numbers) */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.00000000139698386); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -0.75); + + /* float source (non-whole numbers) */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 1.570795); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -1.570795); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 1.570795); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 0.52159887099); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -0.39269875); +} + +static void test_typed_value_negate(char type, int i32, double f32, char expected_type, signed result_i32, double result_f32) { + rc_typed_value_t value; + + init_typed_value(&value, type, (unsigned)i32, f32); + + rc_typed_value_negate(&value); + + assert_typed_value(&value, expected_type, (unsigned)result_i32, result_f32); +} + +static void test_typed_value_negation() { + /* unsigned source */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 99, 0.0, RC_VALUE_TYPE_SIGNED, -99, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_SIGNED, 1, 0.0); + + /* signed source */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, 99, 0.0, RC_VALUE_TYPE_SIGNED, -99, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, -1, 0.0, RC_VALUE_TYPE_SIGNED, 1, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 99.0, RC_VALUE_TYPE_FLOAT, 0, -99.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_FLOAT, 0, 1.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 0.1, RC_VALUE_TYPE_FLOAT, 0, -0.1); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -3.14159); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, -2.7, RC_VALUE_TYPE_FLOAT, 0, 2.7); +} + +void test_value(void) { + TEST_SUITE_BEGIN(); + + /* ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; */ + + /* classic format - supports multipliers, max, inversion */ + TEST_PARAMS2(test_evaluate_value, "V6", 6); + TEST_PARAMS2(test_evaluate_value, "V6*2", 12); + TEST_PARAMS2(test_evaluate_value, "V6*0.5", 3); + TEST_PARAMS2(test_evaluate_value, "V-6", -6); + TEST_PARAMS2(test_evaluate_value, "V-6*2", -12); + + TEST_PARAMS2(test_evaluate_value, "0xH0001_0xH0002", 0x12 + 0x34); + TEST_PARAMS2(test_evaluate_value, "0xH0001*100_0xH0002*0.5_0xL0003", 0x12 * 100 + 0x34 / 2 + 0x0B); + TEST_PARAMS2(test_evaluate_value, "0xH0001$0xH0002", 0x34); + TEST_PARAMS2(test_evaluate_value, "0xH0001_0xH0004*3$0xH0002*0xL0003", 0x34 * 0x0B); + TEST_PARAMS2(test_evaluate_value, "0xH001_V-20", 0x12 - 20); + TEST_PARAMS2(test_evaluate_value, "0xH0001_H10", 0x12 + 0x10); + TEST_PARAMS2(test_evaluate_value, "0xh0000*-1_99_0xh0001*-100_5900", 4199); + TEST_PARAMS2(test_evaluate_value, "v5900_0xh0000*-1.0_0xh0001*-100.0", 4100); + TEST_PARAMS2(test_evaluate_value, "v5900_0xh0000*v-1_0xh0001*v-100", 4100); + + TEST_PARAMS2(test_evaluate_value, "0xH01*4", 0x12 * 4); /* multiply by constant */ + TEST_PARAMS2(test_evaluate_value, "0xH01*0.5", 0x12 / 2); /* multiply by fraction */ + TEST_PARAMS2(test_evaluate_value, "0xH01/2", 0x12 / 2); /* divide by constant */ + TEST_PARAMS2(test_evaluate_value, "0xH01*0xH02", 0x12 * 0x34); /* multiply by second address */ + TEST_PARAMS2(test_evaluate_value, "0xH01*0xT02", 0); /* multiply by bit */ + TEST_PARAMS2(test_evaluate_value, "0xH01*~0xT02", 0x12); /* multiply by inverse bit */ + TEST_PARAMS2(test_evaluate_value, "0xH01*~0xH02", 0x12 * (0x34 ^ 0xff)); /* multiply by inverse byte */ + + TEST_PARAMS2(test_evaluate_value, "B0xH01", 12); + TEST_PARAMS2(test_evaluate_value, "B0x00001", 3412); + TEST_PARAMS2(test_evaluate_value, "B0xH03", 111); /* 0xAB not really BCD */ + + /* non-comparison measured values just return the value at the address and have no target */ + TEST_PARAMS2(test_measured_value_target, "M:0xH0002", 0); + + /* hitcount based measured values always have unbounded targets, even if one is specified */ + TEST_PARAMS2(test_measured_value_target, "M:0xH0002!=d0xH0002", (unsigned)-1); + TEST_PARAMS2(test_measured_value_target, "M:0xH0002!=d0xH0002.99.", (unsigned)-1); + /* measured values always assumed to be hitcount based - they do not stop/trigger when the condition is met */ + TEST_PARAMS2(test_measured_value_target, "M:0xH0002<100", (unsigned)-1); + + /* measured format - supports hit counts and combining flags + * (AddSource, SubSource, AddHits, SubHits, AndNext, OrNext, and AddAddress) */ + TEST_PARAMS2(test_evaluate_value, "M:0xH0002", 0x34); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001_M:0xH0002", 0x12 + 0x34); + TEST_PARAMS2(test_evaluate_value, "B:0xH0001_M:0xH0002", 0x34 - 0x12); + TEST_PARAMS2(test_evaluate_value, "C:0xH0000=0_M:0xH0002=52", 2); + TEST_PARAMS2(test_evaluate_value, "C:0xH0000=0_D:0xH0001=18_M:0xH0002=52", 1); + TEST_PARAMS2(test_evaluate_value, "N:0xH0000=0_M:0xH0002=52", 1); + TEST_PARAMS2(test_evaluate_value, "O:0xH0000=0_M:0xH0002=0", 1); + TEST_PARAMS2(test_evaluate_value, "I:0xH0000_M:0xH0002", 0x34); + + TEST_PARAMS2(test_evaluate_value, "M:0xH0002*2", 0x34 * 2); + TEST_PARAMS2(test_evaluate_value, "M:0xH0002/2", 0x34 / 2); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001*2_A:0xH0002*2_M:0", 0x12 * 2 + 0x34 * 2); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001*2_M:0xH0002*2", 0x12 * 2 + 0x34 * 2); /* multiplier in final condition */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/2_M:0xH0002/2", 0x12 / 2 + 0x34 / 2); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001&15_M:0xH0002&15", (0x12 & 15) + (0x34 & 15)); + + /* measured format does not support alt groups */ + TEST_PARAMS2(test_invalid_value, "M:0xH0002=6SM:0xH0003=6", RC_INVALID_VALUE_FLAG); + /* does not start with X:, so legacy parser says it's an invalid memory accessor */ + TEST_PARAMS2(test_invalid_value, "SM:0xH0002=6SM:0xH0003=6", RC_INVALID_MEMORY_OPERAND); + + /* measured format does not support trigger flag */ + TEST_PARAMS2(test_invalid_value, "T:0xH0002=6", RC_INVALID_VALUE_FLAG); + + /* measured format requires a measured condition */ + TEST_PARAMS2(test_invalid_value, "A:0xH0002_0xH0003>10.99.", RC_INVALID_VALUE_FLAG); /* no flag on condition 2 */ + TEST_PARAMS2(test_invalid_value, "A:0xH0002_A:0xH0003", RC_MISSING_VALUE_MEASURED); + + /* measured value with float data */ + TEST_PARAMS2(test_evaluate_value, "M:fF0005", 3); /* 3.141592 -> 3 */ + TEST_PARAMS2(test_evaluate_value, "A:fF0005*10_M:0", 31); /* 3.141592 x 10 -> 31.415 -> 31 */ + TEST_PARAMS2(test_evaluate_value, "A:fF0005*f11.2_M:f6.9", 42); /* 3.141592 x 11.2 -> 35.185 + 6.9 -> -> 42.085 -> 42 */ + TEST_PARAMS2(test_evaluate_value, "A:fF0005*f5.555555555555555555555555555555555555555555555556_M:f6.9", 24); /* 3.141592 x 5.555556 -> 17.4532902 + 6.9 -> -> 24.353290 -> 24 */ + + /* delta should initially be 0, so a hit will be tallied */ + TEST_PARAMS2(test_evaluate_value, "M:0xH0002!=d0xH0002", 1); + + /* division (cannot occur directly on measured condition, so use AddSource) */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/2_M:0", 9); /* 18/2 = 9 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/5_M:0", 3); /* 18/5 = 3 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0002/0xH0001_M:0", 2); /* 52/18 = 2 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0002_M:0", 0); /* 18/52 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0001_M:0", 1); /* 18/18 = 1 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0000_M:0", 0); /* 18/0 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000/0xH0000_M:0", 0); /* 0/0 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000/0xH0001_M:0", 0); /* 0/18 = 0 */ + + /* rounding */ + TEST_PARAMS2(test_evaluate_value, "0xH03/2_0xH03/2", 0xAA); /* integer division results in rounding */ + TEST_PARAMS2(test_evaluate_value, "0xH03/f2.0_0xH03/f2.0", 0xAB); /* float division does not result in rounding */ + TEST_PARAMS2(test_evaluate_value, "0xH03*0.5_0xH03*0.5", 0xAB); /* float multiplication does not result in rounding */ + TEST_PARAMS2(test_evaluate_value, "A:0xH03/2_A:0xH03/2_M:0", 0xAA); /* integer division results in rounding */ + TEST_PARAMS2(test_evaluate_value, "A:0xH03/f2.0_A:0xH03/f2.0_M:0", 0xAB); /* float division does not result in rounding */ + + /* using measured_if */ + TEST_PARAMS2(test_evaluate_value, "Q:0xH0001!=0_M:0xH0002", 0x34); + TEST_PARAMS2(test_evaluate_value, "Q:0xH0001=0_M:0xH0002", 0); + TEST_PARAMS2(test_evaluate_value, "Q:0xH0001!=0_M:1", 1); + + /* pause and reset affect hit count */ + TEST(test_evaluate_measured_value_with_pause); + TEST(test_evaluate_measured_value_with_reset); + + /* overflow - 145406052 * 86 = 125049208332 -> 0x1D1D837E0C, leading 0x1D is truncated off */ + TEST_PARAMS2(test_evaluate_value, "0xX0001*0xH0004", 0x1D837E0C); + + test_typed_value_conversion(); + test_typed_value_addition(); + test_typed_value_multiplication(); + test_typed_value_division(); + test_typed_value_negation(); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/data.c b/src/rcheevos/test/rhash/data.c new file mode 100644 index 000000000..80913def3 --- /dev/null +++ b/src/rcheevos/test/rhash/data.c @@ -0,0 +1,829 @@ +#include "data.h" + +#include "../src/rcheevos/rc_compat.h" + +#include +#include + +/* first 64 bytes of SUPER MARIO 64 ROM in each N64 format */ +uint8_t test_rom_z64[64] = { + 0x80, 0x37, 0x12, 0x40, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x24, 0x60, 0x00, 0x00, 0x00, 0x14, 0x44, + 0x63, 0x5A, 0x2B, 0xFF, 0x8B, 0x02, 0x23, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x53, 0x55, 0x50, 0x45, 0x52, 0x20, 0x4D, 0x41, 0x52, 0x49, 0x4F, 0x20, 0x36, 0x34, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x53, 0x4D, 0x45, 0x00 +}; + +uint8_t test_rom_v64[64] = { + 0x37, 0x80, 0x40, 0x12, 0x00, 0x00, 0x0F, 0x00, 0x24, 0x80, 0x00, 0x60, 0x00, 0x00, 0x44, 0x14, + 0x5A, 0x63, 0xFF, 0x2B, 0x02, 0x8B, 0x26, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0x53, 0x45, 0x50, 0x20, 0x52, 0x41, 0x4D, 0x49, 0x52, 0x20, 0x4F, 0x34, 0x36, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x4D, 0x53, 0x00, 0x45 +}; + +uint8_t test_rom_n64[64] = { + 0x40, 0x12, 0x37, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x60, 0x24, 0x80, 0x44, 0x14, 0x00, 0x00, + 0xFF, 0x2B, 0x5A, 0x63, 0x26, 0x23, 0x02, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x45, 0x50, 0x55, 0x53, 0x41, 0x4D, 0x20, 0x52, 0x20, 0x4F, 0x49, 0x52, 0x20, 0x20, 0x34, 0x36, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x45, 0x4D, 0x53 +}; + +/* first 64 bytes of DOSHIN THE GIANT in ndd format */ +uint8_t test_rom_ndd[64] = { + 0xE8, 0x48, 0xD3, 0x16, 0x10, 0x13, 0x00, 0x45, 0x0C, 0x18, 0x24, 0x30, 0x3C, 0x48, 0x54, 0x60, + 0x6C, 0x78, 0x84, 0x90, 0x9C, 0xA8, 0xB4, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x02, 0x5C, 0x00, + 0x10, 0x16, 0x1C, 0x22, 0x28, 0x2A, 0x31, 0x32, 0x3A, 0x40, 0x46, 0x4C, 0x04, 0x0C, 0x14, 0x1C, + 0x24, 0x2C, 0x34, 0x3C, 0x44, 0x4C, 0x54, 0x5C, 0x04, 0x0C, 0x14, 0x1C, 0x24, 0x2C, 0x34, 0x3C +}; + +static void fill_image(uint8_t* image, size_t size) +{ + int seed = (int)(size ^ (size >> 8) ^ ((size - 1) * 25387)); + int count; + uint8_t value; + + while (size > 0) + { + switch (seed & 0xFF) + { + case 0: + count = (((seed >> 8) & 0x3F) & ~(size & 0x0F)); + if (count == 0) + count = 1; + value = 0; + break; + + case 1: + count = ((seed >> 8) & 0x07) + 1; + value = ((seed >> 16) & 0xFF); + break; + + case 2: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0xFF; + break; + + case 3: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0xA5; + break; + + case 4: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0xC3; + break; + + case 5: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0x96; + break; + + case 6: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0x78; + break; + + case 7: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0x78; + break; + + default: + count = 1; + value = ((seed >> 8) ^ (seed >> 16)) & 0xFF; + break; + } + + do + { + *image++ = value; + --size; + } while (size && --count); + + /* state mutation from psuedo-random number generator */ + seed = (seed * 0x41C64E6D + 12345) & 0x7FFFFFFF; + } +} + +uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size) +{ + uint8_t* image; + size_t size_needed = kb * 1024; + if (with_header) + size_needed += 16; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) + { + if (with_header) + { + image[0] = 'N'; + image[1] = 'E'; + image[2] = 'S'; + image[3] = '\x1A'; + image[4] = (uint8_t)(kb / 16); + + fill_image(image + 16, size_needed - 16); + } + else + { + fill_image(image, size_needed); + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +uint8_t* generate_fds_file(size_t sides, int with_header, size_t* image_size) +{ + uint8_t* image; + size_t size_needed = sides * 65500; + if (with_header) + size_needed += 16; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) + { + if (with_header) + { + image[0] = 'F'; + image[1] = 'D'; + image[2] = 'S'; + image[3] = '\x1A'; + image[4] = (uint8_t)sides; + + fill_image(image + 16, size_needed - 16); + } + else + { + fill_image(image, size_needed); + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +uint8_t* generate_nds_file(size_t mb, unsigned arm9_size, unsigned arm7_size, size_t* image_size) +{ + uint8_t* image; + const size_t size_needed = mb * 1024 * 1024; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) + { + uint32_t arm9_addr = 65536; + uint32_t arm7_addr = arm9_addr + arm9_size; + uint32_t icon_addr = arm7_addr + arm7_size; + + fill_image(image, size_needed); + + image[0x20] = (arm9_addr & 0xFF); + image[0x21] = ((arm9_addr >> 8) & 0xFF); + image[0x22] = ((arm9_addr >> 16) & 0xFF); + image[0x23] = ((arm9_addr >> 24) & 0xFF); + image[0x2C] = (arm9_size & 0xFF); + image[0x2D] = ((arm9_size >> 8) & 0xFF); + image[0x2E] = ((arm9_size >> 16) & 0xFF); + image[0x2F] = ((arm9_size >> 24) & 0xFF); + + image[0x30] = (arm7_addr & 0xFF); + image[0x31] = ((arm7_addr >> 8) & 0xFF); + image[0x32] = ((arm7_addr >> 16) & 0xFF); + image[0x33] = ((arm7_addr >> 24) & 0xFF); + image[0x3C] = (arm7_size & 0xFF); + image[0x3D] = ((arm7_size >> 8) & 0xFF); + image[0x3E] = ((arm7_size >> 16) & 0xFF); + image[0x3F] = ((arm7_size >> 24) & 0xFF); + + image[0x68] = (icon_addr & 0xFF); + image[0x69] = ((icon_addr >> 8) & 0xFF); + image[0x6A] = ((icon_addr >> 16) & 0xFF); + image[0x6B] = ((icon_addr >> 24) & 0xFF); + } + + if (image_size) + *image_size = size_needed; + return image; +} + +uint8_t* generate_gamecube_iso(size_t mb, size_t* image_size) +{ + uint8_t* image; + const size_t size_needed = mb * 1024 * 1024; + int ix; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) + { + uint32_t apploader_sizes_addr = 0x2440 + 0x14; + uint32_t dol_offset_addr = 0x420; + uint32_t dol_sizes_addr = 0x3000; + + fill_image(image, size_needed); + + image[0x1c] = 0xC2; + image[0x1d] = 0x33; + image[0x1e] = 0x9F; + image[0x1f] = 0x3D; + + for (ix = 0; ix < 8; ix++) + { + /* 0x000000ff for both */ + image[apploader_sizes_addr + ix] = (ix % 4 == 3) ? 0xff : 0; + } + for (ix = 0; ix < 4; ix++) + { + /* 0x00003000 */ + image[dol_offset_addr + ix] = (ix % 4 == 2) ? 0x30 : 0; + } + for (ix = 0; ix < 18 * 4; ix++) + { + /* offsets start at 0x00003100 and increment */ + image[dol_sizes_addr + ix] = (ix % 4 == 2) ? (0x30 + 1 + ix / 4) : 0; + /* 0x000000ff for every other size */ + image[dol_sizes_addr + 0x90 + ix] = (ix % 8 == 3) ? 0xff : 0; + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +uint8_t* generate_atari_7800_file(size_t kb, int with_header, size_t* image_size) +{ + uint8_t* image; + size_t size_needed = kb * 1024; + if (with_header) + size_needed += 128; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) + { + if (with_header) + { + const uint8_t header[128] = { + 3, 'A', 'T', 'A', 'R', 'I', '7', '8', '0', '0', 0, 0, 0, 0, 0, 0, /* version + magic text */ + 0, 'G', 'a', 'm', 'e', 'N', 'a', 'm', 'e', 0, 0, 0, 0, 0, 0, 0, /* game name */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* game name (cont'd) */ + 0, 0, 2, 0, 0, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, /* attributes */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* unused */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* unused */ + 0, 0, 0, 0, 'A', 'C', 'T', 'U', 'A', 'L', ' ', 'C', 'A', 'R', 'T',/* magic text*/ + 'D', 'A', 'T', 'A', ' ', 'S', 'T', 'A', 'R', 'T', 'S', ' ', 'H', 'E', 'R', 'E' /* magic text */ + }; + memcpy(image, header, sizeof(header)); + image[50] = (uint8_t)(kb / 4); /* 4-byte value starting at address 49 is the ROM size without header */ + + fill_image(image + 128, size_needed - 128); + } + else + { + fill_image(image, size_needed); + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +uint8_t* generate_3do_bin(unsigned root_directory_sectors, unsigned binary_size, size_t* image_size) +{ + const uint8_t volume_header[] = { + 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01, 0x00, /* header */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* comment */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 'C', 'D', '-', 'R', 'O', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* label */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x2D, 0x79, 0x6E, 0x96, /* identifier */ + 0x00, 0x00, 0x08, 0x00, /* block size */ + 0x00, 0x00, 0x05, 0x00, /* block count */ + 0x31, 0x5a, 0xf2, 0xe6, /* root directory identifier */ + 0x00, 0x00, 0x00, 0x01, /* root directory size in blocks */ + 0x00, 0x00, 0x08, 0x00, /* block size in root directory */ + 0x00, 0x00, 0x00, 0x06, /* number of copies of root directory */ + 0x00, 0x00, 0x00, 0x01, /* block location of root directory */ + 0x00, 0x00, 0x00, 0x01, /* block location of first copy of root directory */ + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, /* block location of last copy of root directory */ + }; + + const uint8_t directory_data[] = { + 0xFF, 0xFF, 0xFF, 0xFF, /* next block */ + 0xFF, 0xFF, 0xFF, 0xFF, /* previous block */ + 0x00, 0x00, 0x00, 0x00, /* flags */ + 0x00, 0x00, 0x00, 0xA4, /* end of block */ + 0x00, 0x00, 0x00, 0x14, /* start of block */ + + 0x00, 0x00, 0x00, 0x07, /* flags - directory */ + 0x00, 0x00, 0x00, 0x00, /* identifier */ + 0x00, 0x00, 0x00, 0x00, /* type */ + 0x00, 0x00, 0x08, 0x00, /* block size */ + 0x00, 0x00, 0x00, 0x00, /* length in bytes */ + 0x00, 0x00, 0x00, 0x00, /* length in blocks */ + 0x00, 0x00, 0x00, 0x00, /* burst */ + 0x00, 0x00, 0x00, 0x00, /* gap */ + 'f', 'o', 'l', 'd', 'e', 'r', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* filename */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x00, 0x00, 0x00, 0x00, /* extra copies */ + 0x00, 0x00, 0x00, 0x00, /* directory block address */ + + 0x00, 0x00, 0x00, 0x02, /* flags - file */ + 0x00, 0x00, 0x00, 0x00, /* identifier */ + 0x00, 0x00, 0x00, 0x00, /* type */ + 0x00, 0x00, 0x08, 0x00, /* block size */ + 0x00, 0x00, 0x00, 0x00, /* length in bytes */ + 0x00, 0x00, 0x00, 0x00, /* length in blocks */ + 0x00, 0x00, 0x00, 0x00, /* burst */ + 0x00, 0x00, 0x00, 0x00, /* gap */ + 'L', 'a', 'u', 'n', 'c', 'h', 'M', 'e', 0, 0, 0, 0, 0, 0, 0, 0, /* filename */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x00, 0x00, 0x00, 0x00, /* extra copies */ + 0x00, 0x00, 0x00, 0x02, /* directory block address */ + }; + + size_t size_needed = (root_directory_sectors + 1 + ((binary_size + 2047) / 2048)) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + size_t offset = 2048; + unsigned i; + + if (!image) + return NULL; + + /* first sector - volume header */ + memcpy(image, volume_header, sizeof(volume_header)); + image[0x5B] = (uint8_t)root_directory_sectors; + + /* root directory sectors */ + for (i = 0; i < root_directory_sectors; ++i) + { + memcpy(&image[offset], directory_data, sizeof(directory_data)); + if (i < root_directory_sectors - 1) + { + image[offset + 0] = 0; + image[offset + 1] = 0; + image[offset + 2] = 0; + image[offset + 3] = (uint8_t)(i + 1); + + memcpy(&image[offset + 0x14 + 0x48 + 0x20], "filename", 8); + } + else + { + image[offset + 0x14 + 0x48 + 0x11] = (binary_size >> 16) & 0xFF; + image[offset + 0x14 + 0x48 + 0x12] = (binary_size >> 8) & 0xFF; + image[offset + 0x14 + 0x48 + 0x13] = (binary_size & 0xFF); + + image[offset + 0x14 + 0x48 + 0x16] = (((binary_size + 2047) / 2048) >> 8) & 0xFF; + image[offset + 0x14 + 0x48 + 0x17] = ((binary_size + 2047) / 2048) & 0xFF; + + image[offset + 0x14 + 0x48 + 0x47] = (uint8_t)(i + 2); + } + + if (i > 0) + { + image[offset + 4] = 0; + image[offset + 5] = 0; + image[offset + 6] = 0; + image[offset + 7] = (uint8_t)(i - 1); + } + + offset += 2048; + } + + /* binary data */ + fill_image(&image[offset], binary_size); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_dreamcast_bin(unsigned track_first_sector, unsigned binary_size, size_t* image_size) +{ + /* https://mc.pp.se/dc/ip0000.bin.html */ + const uint8_t volume_header[] = + "SEGA SEGAKATANA " + "SEGA ENTERPRISES" + "5966 GD-ROM1/1 " /* device info */ + " U 918FA01 " /* region and peripherals */ + "X-1234N V1.001" /* product number and version */ + "20200910 " /* release date */ + "1ST_READ.BIN " /* boot file */ + "RETROACHIEVEMENT" /* company name */ + "UNIT TEST " /* product name */ + " " + " " + " " + " " + " " + " " + " "; + + const uint8_t directory_data[] = { + 0x30, /* length of directory record */ + 0x00, /* extended attribute record length */ + 0xD9, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* first logical block of file */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* length in bytes */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* date/time */ + 0x00, 0x00, 0x00, /* flags, unit size, gap size */ + 0x00, 0x00, 0x00, 0x00, /* sequence number*/ + 0x0E, /* length of file identifier */ + '1', 'S', 'T', '_', 'R', 'E', 'A', 'D', '.', 'B', 'I', 'N', ';', '1', /* file identifier */ + }; + + const size_t binary_sectors = (binary_size + 2047) / 2048; + const size_t size_needed = (binary_sectors + 18) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + if (!image) + return NULL; + + /* volume header goes in sector 0 */ + memcpy(&image[0], volume_header, sizeof(volume_header)); + + /* directory information goes in sectors 16 and 17 */ + memcpy(&image[2048 * 16], "1CD001", 6); + image[2048 * 16 + 156 + 2] = 45017 & 0xFF; + image[2048 * 16 + 156 + 3] = (45017 >> 8) & 0xFF; + image[2048 * 16 + 156 + 4] = (45017 >> 16) & 0xFF; + memcpy(&image[2048 * 17], directory_data, sizeof(directory_data)); + + track_first_sector += 18; + image[2048 * 17 + 2] = (track_first_sector & 0xFF); + image[2048 * 17 + 3] = ((track_first_sector >> 8) & 0xFF); + image[2048 * 17 + 4] = ((track_first_sector >> 16) & 0xFF); + image[2048 * 17 + 10] = (binary_size & 0xFF); + image[2048 * 17 + 11] = ((binary_size >> 8) & 0xFF); + image[2048 * 17 + 12] = ((binary_size >> 16) & 0xFF); + image[2048 * 17 + 13] = ((binary_size >> 24) & 0xFF); + + /* binary data */ + fill_image(&image[2048 * 18], binary_sectors * 2048); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_pce_cd_bin(unsigned binary_sectors, size_t* image_size) +{ + const uint8_t volume_header[] = { + 0x00, 0x00, 0x02, /* first sector of boot code */ + 0x14, /* number of sectors for boot code */ + 0x00, 0x40, /* program load address */ + 0x00, 0x40, /* program execute address */ + 0, 1, 2, 3, 4, /* IPLMPR */ + 0, /* open mode */ + 0, 0, 0, 0, 0, 0, /* GRPBLK and addr */ + 0, 0, 0, 0, 0, /* ADPBLK and rate */ + 0, 0, 0, 0, 0, 0, 0, /* reserved */ + 'P', 'C', ' ', 'E', 'n', 'g', 'i', 'n', 'e', ' ', 'C', 'D', '-', 'R', 'O', 'M', + ' ', 'S', 'Y', 'S', 'T', 'E', 'M', '\0', 'C', 'o', 'p', 'y', 'r', 'i', 'g', 'h', + 't', ' ', 'H', 'U', 'D', 'S', 'O', 'N', ' ', 'S', 'O', 'F', 'T', ' ', '/', ' ', + 'N', 'E', 'C', ' ', 'H', 'o', 'm', 'e', ' ', 'E', 'l', 'e', 'c', 't', 'r', 'o', + 'n', 'i', 'c', 's', ',', 'L', 't', 'd', '.', '\0', 'G', 'A', 'M', 'E', 'N', 'A', + 'M', 'E', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' + }; + + size_t size_needed = (binary_sectors + 2) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + if (!image) + return NULL; + + /* volume header goes in second sector */ + memcpy(&image[2048], volume_header, sizeof(volume_header)); + image[2048 + 0x03] = (uint8_t)binary_sectors; + + /* binary data */ + fill_image(&image[4096], binary_sectors * 2048); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_pcfx_bin(unsigned binary_sectors, size_t* image_size) +{ + const uint8_t volume_header[] = { + 'G', 'A', 'M', 'E', 'N', 'A', 'M', 'E', 0, 0, 0, 0, 0, 0, 0, 0, /* title (32-bytes) */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x02, 0x00, 0x00, 0x00, /* first sector of boot code */ + 0x14, 0x00, 0x00, 0x00, /* number of sectors for boot code */ + 0x00, 0x80, 0x00, 0x00, /* program load address */ + 0x00, 0x80, 0x00, 0x00, /* program execute address */ + 'N', '/', 'A', '\0', /* maker id */ + 'r', 'c', 'h', 'e', 'e', 'v', 'o', 's', 't', 'e', 's', 't', 0, 0, 0, 0, /* maker name (60-bytes) */ + 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, + 0x00, 0x00, 0x00, 0x00, /* volume number */ + 0x00, 0x01, /* version */ + 0x01, 0x00, /* country */ + '2', '0', '2', '0', 'X', 'X', 'X', 'X', /* date */ + }; + + size_t size_needed = (binary_sectors + 2) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + if (!image) + return NULL; + + /* volume header goes in second sector */ + strcpy_s((char*)&image[0], size_needed, "PC-FX:Hu_CD-ROM"); + memcpy(&image[2048], volume_header, sizeof(volume_header)); + image[2048 + 0x24] = (uint8_t)binary_sectors; + + /* binary data */ + fill_image(&image[4096], binary_sectors * 2048); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_iso9660_bin(unsigned num_sectors, const char* volume_label, size_t* image_size) +{ + const uint8_t identifier[] = { 0x01, 'C', 'D', '0', '0', '1', 0x01, 0x00 }; + uint8_t* volume_descriptor;; + uint8_t* image; + + *image_size = num_sectors * 2048; + image = calloc(*image_size, 1); + if (!image) + return NULL; + + volume_descriptor = &image[16 * 2048]; + + /* CD001 identifier */ + memcpy(volume_descriptor, identifier, 8); + + /* volume label */ + memcpy(&volume_descriptor[40], volume_label, strlen(volume_label)); + + /* number of sectors (little endian, then big endian) */ + volume_descriptor[80] = image[87] = num_sectors & 0xFF; + volume_descriptor[81] = image[86] = (num_sectors >> 8) & 0xFF; + volume_descriptor[82] = image[85] = (num_sectors >> 16) & 0xFF; + volume_descriptor[83] = image[84] = (num_sectors >> 24) & 0xFF; + + /* size of each sector */ + volume_descriptor[128] = (2048) & 0xFF; + volume_descriptor[129] = (2048 >> 8) & 0xFF; + + /* root directory record location */ + volume_descriptor[158] = 17; + + /* helper for tracking next free sector - not actually part of iso9660 spec */ + image[17 * 2048 - 4] = 18; + + return image; +} + +uint8_t* generate_iso9660_file(uint8_t* image, const char* filename, const uint8_t* contents, size_t contents_size) +{ + const unsigned root_directory_record_offset = 17 * 2048; + uint8_t* file_entry_start = &image[root_directory_record_offset]; + uint8_t* file_contents_start; + size_t filename_len; + unsigned next_free_sector = image[root_directory_record_offset - 4] | + (image[root_directory_record_offset - 3] << 8) | (image[root_directory_record_offset - 2] << 16); + const char* separator; + + /* we start at the root. ignore explicit root path */ + if (*filename == '\\') + ++filename; + + /* handle subdirectories */ + do + { + separator = filename; + while (*separator && *separator != '\\') + ++separator; + + if (!*separator) + break; + + filename_len = separator - filename; + int found = 0; + while (*file_entry_start) + { + if (file_entry_start[25] && /* is directory */ + file_entry_start[33 + filename_len] == '\0' && memcmp(&file_entry_start[33], filename, filename_len) == 0) + { + const unsigned directory_sector = file_entry_start[2]; + file_entry_start = &image[directory_sector * 2048]; + found = 1; + break; + } + + file_entry_start += *file_entry_start; + } + + if (!found) + { + /* entry size*/ + file_entry_start[0] = (filename_len & 0xFF) + 48; + + /* directory sector */ + file_entry_start[2] = next_free_sector & 0xFF; + file_entry_start[3] = (next_free_sector >> 8) & 0xFF; + + /* is directory */ + file_entry_start[25] = 1; + + /* directory name */ + file_entry_start[32] = filename_len & 0xFF; + memcpy(&file_entry_start[33], filename, filename_len); + file_entry_start[33 + filename_len] = '\0'; + + /* advance to next sector */ + file_entry_start = &image[next_free_sector * 2048]; + next_free_sector++; + } + + filename = separator + 1; + } while (1); + + /* skip over any items already in the directory */ + while (*file_entry_start) + file_entry_start += *file_entry_start; + + /* create the new entry */ + + /* entry size*/ + filename_len = separator - filename; + file_entry_start[0] = (filename_len & 0xFF) + 48; + + /* file sector */ + file_entry_start[2] = next_free_sector & 0xFF; + file_entry_start[3] = (next_free_sector >> 8) & 0xFF; + + /* file size */ + file_entry_start[10] = contents_size & 0xFF; + file_entry_start[11] = (contents_size >> 8) & 0xFF; + file_entry_start[12] = (contents_size >> 16) & 0xFF; + + /* file name */ + file_entry_start[32] = (filename_len + 2) & 0xFF; + memcpy(&file_entry_start[33], filename, filename_len); + file_entry_start[33 + filename_len] = ';'; + file_entry_start[34 + filename_len] = '1'; + + /* contents */ + file_contents_start = &image[next_free_sector * 2048]; + + if (contents) + memcpy(file_contents_start, contents, contents_size); + else + fill_image(file_contents_start, contents_size); + + /* update next free sector */ + next_free_sector += (unsigned)(contents_size + 2047) / 2048; + image[root_directory_record_offset - 4] = (next_free_sector & 0xFF); + image[root_directory_record_offset - 3] = (next_free_sector >> 8) & 0xFF; + image[root_directory_record_offset - 2] = (next_free_sector >> 16) & 0xFF; + + /* return pointer to contents so caller can modify if desired */ + return file_contents_start; +} + +uint8_t* generate_jaguarcd_bin(unsigned header_offset, unsigned binary_size, int byteswapped, size_t* image_size) +{ + size_t size_needed = (((binary_size + 64 + 32 + 8) + 2351) / 2352) * 2352; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + size_t i; + + if (!image) + return NULL; + + /* header is 64 bytes of ATRI repeating followed by approved data message, load address, and binary size */ + for (i = 0; i < 64; i += 4) + memcpy(&image[header_offset + i], "ATRI", 4); + memcpy(&image[header_offset + 64], "ATARI APPROVED DATA HEADER ATRI ", 32); + image[header_offset + 64 + 32 + 2] = 0xA0; + image[header_offset + 64 + 32 + 4 + 1] = (binary_size >> 16); + image[header_offset + 64 + 32 + 4 + 2] = (binary_size >> 8) & 0xFF; + image[header_offset + 64 + 32 + 4 + 3] = (binary_size & 0xFF); + + /* binary data */ + fill_image(&image[header_offset + 64 + 32 + 8], size_needed - (header_offset + 64 + 32 + 8)); + + if (byteswapped) + { + for (i = 0; i < size_needed; i += 2) + { + uint8_t tmp = image[i]; + image[i] = image[i + 1]; + image[i + 1] = tmp; + } + } + + *image_size = size_needed; + return image; +} + +uint8_t* generate_psx_bin(const char* binary_name, unsigned binary_size, size_t* image_size) +{ + const unsigned sectors_needed = (((binary_size + 2047) / 2048) + 20); + char system_cnf[256]; + uint8_t* image; + uint8_t* exe; + + snprintf(system_cnf, sizeof(system_cnf), "BOOT=cdrom:\\%s;1\nTCB=4\nEVENT=10\nSTACK=801FFFF0\n", binary_name); + + image = generate_iso9660_bin(sectors_needed, "TEST", image_size); + generate_iso9660_file(image, "SYSTEM.CNF", (const uint8_t*)system_cnf, strlen(system_cnf)); + + /* binary data */ + exe = generate_iso9660_file(image, binary_name, NULL, binary_size); + memcpy(exe, "PS-X EXE", 8); + + binary_size -= 2048; + exe[28] = binary_size & 0xFF; + exe[29] = (binary_size >> 8) & 0xFF; + exe[30] = (binary_size >> 16) & 0xFF; + exe[31] = (binary_size >> 24) & 0xFF; + + return image; +} + +uint8_t* generate_ps2_bin(const char* binary_name, unsigned binary_size, size_t* image_size) +{ + const unsigned sectors_needed = (((binary_size + 2047) / 2048) + 20); + char system_cnf[256]; + uint8_t* image; + uint8_t* exe; + + snprintf(system_cnf, sizeof(system_cnf), "BOOT2 = cdrom0:\\%s;1\nVER = 1.0\nVMODE = NTSC\n", binary_name); + + image = generate_iso9660_bin(sectors_needed, "TEST", image_size); + generate_iso9660_file(image, "SYSTEM.CNF", (const uint8_t*)system_cnf, strlen(system_cnf)); + + /* binary data */ + exe = generate_iso9660_file(image, binary_name, NULL, binary_size); + memcpy(exe, "\x7f\x45\x4c\x46", 4); + + return image; +} + +uint8_t* generate_generic_file(size_t size) +{ + uint8_t* image; + image = (uint8_t*)calloc(size, 1); + if (image != NULL) + fill_image(image, size); + + return image; +} + +uint8_t* convert_to_2352(uint8_t* input, size_t* size, uint32_t first_sector) +{ + const uint32_t num_sectors = (uint32_t)((*size + 2047) / 2048); + const uint32_t output_size = num_sectors * 2352; + const uint8_t sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + }; + uint8_t* output = (uint8_t*)malloc(output_size); + uint8_t* input_ptr = input; + uint8_t* ptr = output; + uint8_t minutes, seconds, frames; + uint32_t i; + + first_sector += 150; + frames = (first_sector % 75); + first_sector /= 75; + seconds = (first_sector % 60); + minutes = first_sector / 60; + + for (i = 0; i < num_sectors; i++) + { + /* 16 - byte sync header */ + memcpy(ptr, sync_pattern, 12); + ptr += 12; + *ptr++ = ((minutes / 10) << 4) | (minutes % 10); + *ptr++ = ((seconds / 10) << 4) | (seconds % 10); + *ptr++ = ((frames / 10) << 4) | (frames % 10); + if (++frames == 75) + { + frames = 0; + if (++seconds == 60) + { + seconds = 0; + ++minutes; + } + } + *ptr++ = 2; + + /* 2048 bytes data */ + memcpy(ptr, input_ptr, 2048); + input_ptr += 2048; + + /* 288 bytes parity / checksums */ + ptr += 2352 - 16; + } + + free(input); + *size = output_size; + return output; +} diff --git a/src/rcheevos/test/rhash/data.h b/src/rcheevos/test/rhash/data.h new file mode 100644 index 000000000..6bfaf22d4 --- /dev/null +++ b/src/rcheevos/test/rhash/data.h @@ -0,0 +1,41 @@ +#ifndef RHASH_TEST_DATA_H +#define RHASH_TEST_DATA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +uint8_t* generate_generic_file(size_t size); + +uint8_t* convert_to_2352(uint8_t* input, size_t* input_size, uint32_t first_sector); + +uint8_t* generate_3do_bin(unsigned root_directory_sectors, unsigned binary_size, size_t* image_size); +uint8_t* generate_dreamcast_bin(unsigned track_first_sector, unsigned binary_size, size_t* image_size); +uint8_t* generate_jaguarcd_bin(unsigned header_offset, unsigned binary_size, int byteswapped, size_t* image_size); +uint8_t* generate_pce_cd_bin(unsigned binary_sectors, size_t* image_size); +uint8_t* generate_pcfx_bin(unsigned binary_sectors, size_t* image_size); +uint8_t* generate_psx_bin(const char* binary_name, unsigned binary_size, size_t* image_size); +uint8_t* generate_ps2_bin(const char* binary_name, unsigned binary_size, size_t* image_size); + +uint8_t* generate_atari_7800_file(size_t kb, int with_header, size_t* image_size); +uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size); +uint8_t* generate_fds_file(size_t sides, int with_header, size_t* image_size); +uint8_t* generate_nds_file(size_t mb, unsigned arm9_size, unsigned arm7_size, size_t* image_size); +uint8_t* generate_gamecube_iso(size_t mb, size_t* image_size); + +uint8_t* generate_iso9660_bin(unsigned binary_sectors, const char* volume_label, size_t* image_size); +uint8_t* generate_iso9660_file(uint8_t* image, const char* filename, const uint8_t* contents, size_t contents_size); + +extern uint8_t test_rom_z64[64]; +extern uint8_t test_rom_n64[64]; +extern uint8_t test_rom_v64[64]; +extern uint8_t test_rom_ndd[64]; + +#ifdef __cplusplus +} +#endif + +#endif /* RHASH_TEST_DATA_H */ diff --git a/src/rcheevos/test/rhash/mock_filereader.c b/src/rcheevos/test/rhash/mock_filereader.c new file mode 100644 index 000000000..bd5c0648d --- /dev/null +++ b/src/rcheevos/test/rhash/mock_filereader.c @@ -0,0 +1,227 @@ +#include "rc_hash.h" + +#include "../rcheevos/rc_compat.h" + +#include +#include + +typedef struct mock_file_data +{ + const char* path; + const uint8_t* data; + int64_t size; + int64_t pos; + int first_sector; +} mock_file_data; + +static mock_file_data mock_file_instance[16]; +static int mock_cd_tracks; + +static void* _mock_file_open(const char* path) +{ + int i; + for (i = 0; i < sizeof(mock_file_instance) / sizeof(mock_file_instance[0]); ++i) + { + if (strcmp(path, mock_file_instance[i].path) == 0) + { + mock_file_instance[i].pos = 0; + return &mock_file_instance[i]; + } + } + + return NULL; +} + +static void _mock_file_seek(void* file_handle, int64_t offset, int origin) +{ + mock_file_data* file = (mock_file_data*)file_handle; + switch (origin) + { + case SEEK_SET: + file->pos = offset; + break; + case SEEK_CUR: + file->pos += offset; + break; + case SEEK_END: + file->pos = file->size - offset; + break; + } + + if (file->pos > file->size) + file->pos = file->size; +} + +static int64_t _mock_file_tell(void* file_handle) +{ + mock_file_data* file = (mock_file_data*)file_handle; + return file->pos; +} + +static size_t _mock_file_read(void* file_handle, void* buffer, size_t count) +{ + mock_file_data* file = (mock_file_data*)file_handle; + const size_t remaining = (size_t)(file->size - file->pos); + if (count > remaining) + count = remaining; + + if (count > 0) + { + if (file->data) + memcpy(buffer, &file->data[file->pos], count); + else + memset(buffer, 0, count); + + file->pos += count; + } + + return count; +} + +static void _mock_file_close(void* file_handle) +{ + (void)file_handle; +} + +static void reset_mock_files() +{ + int i; + + memset(&mock_file_instance, 0, sizeof(mock_file_instance)); + for (i = 0; i < sizeof(mock_file_instance) / sizeof(mock_file_instance[0]); ++i) + mock_file_instance[i].path = ""; + + mock_cd_tracks = 0; +} + +void init_mock_filereader() +{ + struct rc_hash_filereader reader; + reader.open = _mock_file_open; + reader.seek = _mock_file_seek; + reader.tell = _mock_file_tell; + reader.read = _mock_file_read; + reader.close = _mock_file_close; + + rc_hash_init_custom_filereader(&reader); + + reset_mock_files(); +} + +void mock_file(int index, const char* filename, const uint8_t* buffer, size_t buffer_size) +{ + if (index == 0) + reset_mock_files(); + + mock_file_instance[index].path = filename; + mock_file_instance[index].data = buffer; + mock_file_instance[index].size = buffer_size; + mock_file_instance[index].pos = 0; + mock_file_instance[index].first_sector = 0; +} + +void mock_file_text(int index, const char* filename, const char* contents) +{ + mock_file(index, filename, (const uint8_t*)contents, strlen(contents)); +} + +void mock_file_first_sector(int index, int first_sector) +{ + mock_file_instance[index].first_sector = first_sector; +} + +void mock_file_size(int index, size_t mock_size) +{ + mock_file_instance[index].size = mock_size; +} + +void mock_empty_file(int index, const char* filename, size_t mock_size) +{ + mock_file(index, filename, NULL, mock_size); +} + +static void* _mock_cd_open_track(const char* path, uint32_t track) +{ + if (track == RC_HASH_CDTRACK_LAST) + track = mock_cd_tracks; + + if (track == 1 || track == RC_HASH_CDTRACK_FIRST_DATA || track == RC_HASH_CDTRACK_LARGEST) + { + if (strstr(path, ".cue")) + { + mock_file_data* file = (mock_file_data*)_mock_file_open(path); + if (!file) + return file; + + return _mock_file_open((const char*)file->data); + } + + return _mock_file_open(path); + } + else if (strstr(path, ".cue")) + { + mock_file_data* file = (mock_file_data*)_mock_file_open(path); + if (file) + { + char buffer[256]; + + const size_t file_len = strlen((const char*)file->data); + memcpy(buffer, file->data, file_len - 4); + snprintf(&buffer[file_len - 4], sizeof(buffer) - (file_len - 4), "%d%s", track, &file->data[file_len - 4]); + + return _mock_file_open(buffer); + } + } + else if (strstr(path, ".gdi")) + { + mock_file_data* file = (mock_file_data*)_mock_file_open(path); + if (file) + { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "track%02d.bin", track); + return _mock_file_open(buffer); + } + } + + return NULL; +} + +static size_t _mock_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + mock_file_data* file = (mock_file_data*)track_handle; + sector -= file->first_sector; + + _mock_file_seek(track_handle, sector * 2048, SEEK_SET); + return _mock_file_read(track_handle, buffer, requested_bytes); +} + +static uint32_t _mock_cd_first_track_sector(void* track_handle) +{ + mock_file_data* file = (mock_file_data*)track_handle; + return file->first_sector; +} + +void mock_cd_num_tracks(int num_tracks) +{ + mock_cd_tracks = num_tracks; +} + +void init_mock_cdreader() +{ + struct rc_hash_cdreader cdreader; + memset(&cdreader, 0, sizeof(cdreader)); + cdreader.open_track = _mock_cd_open_track; + cdreader.close_track = _mock_file_close; + cdreader.read_sector = _mock_cd_read_sector; + cdreader.first_track_sector = _mock_cd_first_track_sector; + + rc_hash_init_custom_cdreader(&cdreader); + + mock_cd_tracks = 0; +} + +const char* get_mock_filename(void* file_handle) +{ + mock_file_data* file = (mock_file_data*)file_handle; + return file->path; +} diff --git a/src/rcheevos/test/rhash/mock_filereader.h b/src/rcheevos/test/rhash/mock_filereader.h new file mode 100644 index 000000000..84cb23961 --- /dev/null +++ b/src/rcheevos/test/rhash/mock_filereader.h @@ -0,0 +1,30 @@ +#ifndef RHASH_MOCK_FILEREADER_H +#define RHASH_MOCK_FILEREADER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +void init_mock_filereader(); +void init_mock_cdreader(); + +void rc_hash_reset_filereader(); + +void mock_file(int index, const char* filename, const uint8_t* buffer, size_t buffer_size); +void mock_file_text(int index, const char* filename, const char* contents); +void mock_empty_file(int index, const char* filename, size_t mock_size); +void mock_file_size(int index, size_t mock_size); +void mock_file_first_sector(int index, int first_sector); + +void mock_cd_num_tracks(int num_tracks); + +const char* get_mock_filename(void* file_handle); + +#ifdef __cplusplus +} +#endif + +#endif /* RHASH_MOCK_FILEREADER_H */ diff --git a/src/rcheevos/test/rhash/test_cdreader.c b/src/rcheevos/test/rhash/test_cdreader.c new file mode 100644 index 000000000..703de7244 --- /dev/null +++ b/src/rcheevos/test/rhash/test_cdreader.c @@ -0,0 +1,849 @@ +#include "rc_hash.h" + +#include "../rcheevos/rc_compat.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +extern struct rc_hash_cdreader* cdreader; + +/* as defined in cdreader.c */ +typedef struct cdrom_t +{ + void* file_handle; + int sector_size; + int sector_header_size; + int raw_data_size; + int64_t file_track_offset; + int track_first_sector; + int track_pregap_sectors; +#ifndef NDEBUG + uint32_t track_id; +#endif +} cdrom_t; + +static const unsigned char sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 +}; + +static char cue_single_track[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE2/2352\n" + " INDEX 01 00:00:00\n"; + +static char cue_single_bin_multiple_data[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " PREGAP 00:03:00\n" + " INDEX 01 00:55:45\n" + " TRACK 03 MODE1/2352\n" + " INDEX 01 11:30:74\n" + " TRACK 04 MODE1/2352\n" + " INDEX 01 13:31:51\n" + " TRACK 05 MODE1/2352\n" + " INDEX 01 13:48:56\n" + " TRACK 06 MODE1/2352\n" + " INDEX 01 34:48:19\n" + " TRACK 07 MODE1/2352\n" + " INDEX 01 50:42:74\n" + " TRACK 08 MODE1/2352\n" + " INDEX 01 55:20:74\n" + " TRACK 09 MODE1/2352\n" + " INDEX 01 56:25:67\n" + " TRACK 10 MODE1/2352\n" + " INDEX 01 59:04:08\n" + " TRACK 11 MODE1/2352\n" + " INDEX 01 61:17:18\n" + " TRACK 12 MODE1/2352\n" + " INDEX 01 62:44:33\n" + " TRACK 13 AUDIO\n" + " PREGAP 00:02:00\n" + " INDEX 01 66:24:37\n"; + +static char cue_multiple_bin_multiple_data[] = + "FILE \"track1.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track2.bin\" BINARY\n" + " TRACK 02 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:03:00\n" + "FILE \"track3.bin\" BINARY\n" + " TRACK 03 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track4.bin\" BINARY\n" + " TRACK 04 AUDIO\n" + " INDEX 00 00:00:00\n"; + +static char gdi_three_tracks[] = + "3\n" + "1 0 4 2352 track01.bin 0\n" + "2 600 0 2352 track02.raw 0\n" + "3 45000 4 2352 track03.bin 0"; + +static char gdi_many_tracks[] = + "26\n" + "1 0 4 2352 track01.bin 0\n" + "2 450 0 2352 track02.raw 0\n" + "3 45000 4 2352 track03.bin 0\n" + "4 370673 0 2352 track04.raw 0\n" + "5 371347 0 2352 track05.raw 0\n" + "6 372014 0 2352 track06.raw 0\n" + "7 372915 0 2352 track07.raw 0\n" + "8 373626 0 2352 track08.raw 0\n" + "9 379011 0 2352 track09.raw 0\n" + "10 384738 0 2352 track10.raw 0\n" + "11 390481 0 2352 track11.raw 0\n" + "12 395473 0 2352 track12.raw 0\n" + "13 398926 0 2352 track13.raw 0\n" + "14 404448 0 2352 track14.raw 0\n" + "15 425246 0 2352 track15.raw 0\n" + "16 445520 0 2352 track16.raw 0\n" + "17 466032 0 2352 track17.raw 0\n" + "18 474231 0 2352 track18.raw 0\n" + "19 485598 0 2352 track19.raw 0\n" + "20 486386 0 2352 track20.raw 0\n" + "21 487098 0 2352 track21.raw 0\n" + "22 487822 0 2352 track22.raw 0\n" + "23 498356 0 2352 track23.raw 0\n" + "24 508297 0 2352 track24.raw 0\n" + "25 527383 0 2352 track25.raw 0\n" + "26 548106 4 2352 track26.bin 0\n"; + +static void test_open_cue_track_2() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 2); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 9807840); /* track 2: 0x95A7E0 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_12() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 12); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 664047216); /* track 12: 0x27948E70 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_14() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + /* only 13 tracks */ + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 14); + ASSERT_PTR_NULL(track_handle); +} + +static void test_open_cue_track_missing_bin() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 2); + ASSERT_PTR_NULL(track_handle); +} + +static void test_open_gdi_track_3() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.gdi", gdi_three_tracks); + mock_empty_file(1, "track03.bin", 1185760800); + + track_handle = (cdrom_t*)cdreader->open_track("game.gdi", 3); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track03.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_gdi_track_3_quoted() +{ + const char gdi_contents[] = + "3\n" + "1 0 4 2352 \"track 01.bin\" 0\n" + "2 600 0 2352 \"track 02.raw\" 0\n" + "3 45000 4 2352 \"track 03.bin\" 0"; + + cdrom_t* track_handle; + + mock_file_text(0, "game.gdi", gdi_contents); + mock_empty_file(1, "track 03.bin", 1185760800); + + track_handle = (cdrom_t*)cdreader->open_track("game.gdi", 3); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track 03.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_gdi_track_3_extra_whitespace() +{ + const char gdi_contents[] = + "3\n\n" + " 1 0 4 2352 \"track 01.bin\" 0\n\n" + " 2 600 0 2352 \"track 02.raw\" 0\n\n" + " 3 45000 4 2352 \"track 03.bin\" 0\n\n"; + + cdrom_t* track_handle; + + mock_file_text(0, "game.gdi", gdi_contents); + mock_empty_file(1, "track 03.bin", 1185760800); + + track_handle = (cdrom_t*)cdreader->open_track("game.gdi", 3); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track 03.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_open_gdi_track_last() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.gdi", gdi_many_tracks); + mock_empty_file(1, "track26.bin", 2457600); + + track_handle = (cdrom_t*)cdreader->open_track("game.gdi", RC_HASH_CDTRACK_LAST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track26.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 548106); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_multiple_bin() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_multiple_bin_multiple_data); + mock_empty_file(1, "track2.bin", 406423248); + mock_empty_file(2, "track3.bin", 11553024); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track2.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 225); /* INDEX 01 00:03:00 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_backwards_compatibility() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + /* before defining the enum, 0 meant largest */ + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 0); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_last_track() +{ + cdrom_t* track_handle; + const char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " PREGAP 00:03:00\n" + " INDEX 01 00:55:45\n" + " TRACK 03 MODE1/2352\n" + " INDEX 01 11:30:74\n" + " TRACK 04 MODE1/2352\n" + " INDEX 01 13:31:51\n" + " TRACK 05 MODE1/2352\n" + " INDEX 01 13:48:56\n"; + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 (13:48:56) */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_index0s() +{ + cdrom_t* track_handle; + const char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " INDEX 00 00:44:65\n" + " INDEX 01 00:47:65\n" + " TRACK 03 AUDIO\n" + " INDEX 00 01:19:52\n" + " INDEX 01 01:21:52\n"; + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 7914480); /* track 2: 0x78C3F0 (00:44:65) */ + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 225); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_index2() +{ + cdrom_t* track_handle; + const char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + " INDEX 02 00:08:64\n"; + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); /* 00:02:00 = 150 frames in */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_multiple_bins() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_multiple_bin_multiple_data); + mock_empty_file(1, "track1.bin", 4132464); + mock_empty_file(2, "track2.bin", 30080102); + mock_empty_file(3, "track3.bin", 40343152); + mock_empty_file(4, "track4.bin", 47277552); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 0); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track3.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); /* 00:02:00 = 150 frames in */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_open_cue_track_largest_data_only_audio() +{ + const char cue[] = + "FILE \"track1.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track2.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:03:00\n" + "FILE \"track3.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track4.bin\" BINARY\n" + " TRACK 04 AUDIO\n" + " INDEX 00 00:00:00\n"; + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "track1.bin", 4132464); + mock_empty_file(2, "track2.bin", 30080102); + mock_empty_file(3, "track3.bin", 40343152); + mock_empty_file(4, "track4.bin", 47277552); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 0); + ASSERT_PTR_NULL(track_handle); +} + +static void test_open_cue_track_first_data() +{ + cdrom_t* track_handle; + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", RC_HASH_CDTRACK_FIRST_DATA); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 9807840); /* track 2: 0x0095a7e0 (00:55:45) */ + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + cdreader->close_track(track_handle); +} + +static void test_determine_sector_size_sync(int sector_size) +{ + cdrom_t* track_handle; + const size_t image_size = (size_t)sector_size * 32; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[sector_size * 16], sync_pattern, sizeof(sync_pattern)); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor(int sector_size) +{ + cdrom_t* track_handle; + const size_t image_size = (size_t)sector_size * 32; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[sector_size * 16], sync_pattern, sizeof(sync_pattern)); + memcpy(&image[sector_size * 16 + 25], "CD001", 5); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor_index0(int sector_size) +{ + char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE2/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n"; + + cdrom_t* track_handle; + const size_t image_size = (size_t)sector_size * 200; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue); + mock_file(1, "game.bin", image, image_size); + + char sector_size_str[16]; + snprintf(sector_size_str, sizeof(sector_size_str), "%d", sector_size); + memcpy(&cue[40], sector_size_str, 4); + + memset(image, 0, image_size); + memcpy(&image[sector_size * (150 + 16)], sync_pattern, sizeof(sync_pattern)); + memcpy(&image[sector_size * (150 + 16) + 25], "CD001", 5); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_determine_sector_size_sync_2048() +{ + cdrom_t* track_handle; + const int sector_size = 2048; + const size_t image_size = (size_t)sector_size * 32; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + + /* 2048 byte sectors don't have a sync pattern - will use mode specified in header */ + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor_2048() +{ + cdrom_t* track_handle; + const int sector_size = 2048; + const size_t image_size = (size_t)sector_size * 32; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[sector_size * 16 + 1], "CD001", 5); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 0); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor_index0_2048() +{ + char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE1/2048\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n"; + + cdrom_t* track_handle; + const int sector_size = 2048; + const size_t image_size = (size_t)sector_size * 200; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue); + mock_file(1, "game.bin", image, image_size); + + char sector_size_str[16]; + snprintf(sector_size_str, sizeof(sector_size_str), "%d", sector_size); + memcpy(&cue[40], sector_size_str, 4); + + memset(image, 0, image_size); + memcpy(&image[sector_size * (150 + 16) + 1], "CD001", 5); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 0); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_absolute_sector_to_track_sector_cue_pregap() +{ + const char cue[] = + "FILE \"game1.bin\" BINARY\n"/* file contains 500 sectors of data [1176000 bytes] */ + " TRACK 01 MODE2/2352\n" + " INDEX 00 00:00:00\n" /* 150 pre-gap sectors */ + " INDEX 01 00:02:00\n" /* 350 sectors of data */ + "FILE \"game2.bin\" BINARY\n" + " TRACK 02 MODE2/2352\n" + " INDEX 00 00:00:00\n" /* 150 pre-gap sectors */ + " INDEX 01 00:02:00\n"; + + cdrom_t* track_handle; + const size_t image_size = (size_t)60 * 200; + unsigned char* image = (unsigned char*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue); + mock_file(1, "game1.bin", NULL, (size_t)500 * 2352); + mock_file(2, "game2.bin", image, image_size); + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 2); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game2.bin"); + + /* pregap of second track starts at sector 500 */ + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 500); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); + + /* data for second track starts at sector 650 */ + ASSERT_NUM_EQUALS(cdreader->first_track_sector(track_handle), 650); + + cdreader->close_track(track_handle); + free(image); +} + +static void test_absolute_sector_to_track_sector_gdi() +{ + cdrom_t* track_handle; + mock_file_text(0, "game.gdi", gdi_many_tracks); + mock_file(1, "track26.bin", NULL, 1234567); + + track_handle = (cdrom_t*)cdreader->open_track("game.gdi", 26); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track26.bin"); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 548106); + + ASSERT_NUM_EQUALS(cdreader->first_track_sector(track_handle), 548106); + + cdreader->close_track(track_handle); +} + +static void test_read_sector() +{ + char buffer[4096]; + cdrom_t* track_handle; + const size_t image_size = (size_t)2352 * 32; + unsigned char* image = (unsigned char*)malloc(image_size); + int offset, i; + ASSERT_PTR_NOT_NULL(image); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[2352 * 16], sync_pattern, sizeof(sync_pattern)); + image[2352 * 16 + 12] = 0; + image[2352 * 16 + 13] = 2; + image[2352 * 16 + 14] = 0x16; + image[2352 * 16 + 15] = 2; + + offset = 2352 * 1 + 16; + for (i = 0; i < 26; i++) + { + memset(&image[offset], i + 'A', 256); + offset += 256; + + if ((i % 8) == 7) + offset += (2352 - 2048); + } + + track_handle = (cdrom_t*)cdreader->open_track("game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + /* read across multiple sectors */ + ASSERT_NUM_EQUALS(cdreader->read_sector(track_handle, 1, buffer, sizeof(buffer)), 4096); + + ASSERT_NUM_EQUALS(buffer[0], 'A'); + ASSERT_NUM_EQUALS(buffer[255], 'A'); + ASSERT_NUM_EQUALS(buffer[256], 'B'); + ASSERT_NUM_EQUALS(buffer[2047], 'H'); + ASSERT_NUM_EQUALS(buffer[2048], 'I'); + ASSERT_NUM_EQUALS(buffer[4095], 'P'); + + /* read of partial sector */ + ASSERT_NUM_EQUALS(cdreader->read_sector(track_handle, 2, buffer, 10), 10); + ASSERT_NUM_EQUALS(buffer[0], 'I'); + ASSERT_NUM_EQUALS(buffer[9], 'I'); + ASSERT_NUM_EQUALS(buffer[10], 'A'); + + cdreader->close_track(track_handle); + free(image); +} + +/* ========================================================================= */ + +void test_cdreader(void) { + TEST_SUITE_BEGIN(); + + init_mock_filereader(); + rc_hash_init_default_cdreader(); + + TEST(test_open_cue_track_2); + TEST(test_open_cue_track_12); + TEST(test_open_cue_track_14); + TEST(test_open_cue_track_missing_bin); + + TEST(test_open_gdi_track_3); + TEST(test_open_gdi_track_3_quoted); + TEST(test_open_gdi_track_3_extra_whitespace); + TEST(test_open_gdi_track_last); + + TEST(test_open_cue_track_largest_data); + TEST(test_open_cue_track_largest_data_multiple_bin); + TEST(test_open_cue_track_largest_data_backwards_compatibility); + TEST(test_open_cue_track_largest_data_last_track); + TEST(test_open_cue_track_largest_data_index0s); + TEST(test_open_cue_track_largest_data_index2); + TEST(test_open_cue_track_largest_data_multiple_bins); + TEST(test_open_cue_track_largest_data_only_audio); + + TEST(test_open_cue_track_first_data); + + TEST_PARAMS1(test_determine_sector_size_sync, 2352); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor, 2352); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor_index0, 2352); + + TEST_PARAMS1(test_determine_sector_size_sync, 2336); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor, 2336); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor_index0, 2336); + + TEST(test_determine_sector_size_sync_2048); + TEST(test_determine_sector_size_sync_primary_volume_descriptor_2048); + TEST(test_determine_sector_size_sync_primary_volume_descriptor_index0_2048); + + TEST(test_absolute_sector_to_track_sector_cue_pregap); + TEST(test_absolute_sector_to_track_sector_gdi); + + TEST(test_read_sector); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/test_hash.c b/src/rcheevos/test/rhash/test_hash.c new file mode 100644 index 000000000..260bdcc8e --- /dev/null +++ b/src/rcheevos/test/rhash/test_hash.c @@ -0,0 +1,2298 @@ +#include "rc_hash.h" + +#include "../rcheevos/rc_compat.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +static int hash_mock_file(const char* filename, char hash[33], int console_id, const uint8_t* buffer, size_t buffer_size) +{ + mock_file(0, filename, buffer, buffer_size); + + return rc_hash_generate_from_file(hash, console_id, filename); +} + +static void iterate_mock_file(struct rc_hash_iterator *iterator, const char* filename, const uint8_t* buffer, size_t buffer_size) +{ + mock_file(0, filename, buffer, buffer_size); + + rc_hash_initialize_iterator(iterator, filename, NULL, 0); +} + +/* ========================================================================= */ + +static void test_hash_full_file(int console_id, const char* filename, size_t size, const char* expected_md5) +{ + uint8_t* image = generate_generic_file(size); + char hash_buffer[33], hash_file[33], hash_iterator[33]; + + /* test full buffer hash */ + int result_buffer = rc_hash_generate_from_buffer(hash_buffer, console_id, image, size); + + /* test full file hash */ + int result_file = hash_mock_file(filename, hash_file, console_id, image, size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + iterate_mock_file(&iterator, filename, image, size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_buffer, 1); + ASSERT_STR_EQUALS(hash_buffer, expected_md5); + + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_unknown_format(int console_id, const char* path) +{ + char hash_file[33] = "", hash_iterator[33] = ""; + + /* test file hash (won't match) */ + int result_file = rc_hash_generate_from_file(hash_file, console_id, path); + + /* test file identification from iterator (won't match) */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, path, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_STR_EQUALS(hash_file, ""); + + ASSERT_NUM_EQUALS(result_iterator, 0); + ASSERT_STR_EQUALS(hash_iterator, ""); +} + +static void test_hash_m3u(int console_id, const char* filename, size_t size, const char* expected_md5) +{ + uint8_t* image = generate_generic_file(size); + char hash_file[33], hash_iterator[33]; + const char* m3u_filename = "test.m3u"; + + mock_file(0, filename, image, size); + mock_file(1, m3u_filename, (uint8_t*)filename, strlen(filename)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, console_id, m3u_filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_filename(int console_id, const char* path, const char* expected_md5) +{ + char hash_file[33], hash_iterator[33]; + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, console_id, path); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, path, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_3do_bin() +{ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 123456, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "9b2266b8f5abed9c12cce780750e88d6"; + + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + mock_file_size(0, 45678901); /* must be > 32MB for iterator to consider CD formats for bin */ + rc_hash_initialize_iterator(&iterator, "game.bin", NULL, 0); + mock_file_size(0, image_size); /* change it back before doing the hashing */ + + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_3do_cue() +{ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 9347, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "257d1d19365a864266b236214dbea29c"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_3do_iso() +{ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 9347, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "257d1d19365a864266b236214dbea29c"; + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_3do_invalid_header() +{ + /* this is meant to simulate attempting to open a non-3DO CD. TODO: generate PSX CD */ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 12, &image_size); + char hash_file[33]; + + /* make the header not match */ + image[3] = 0x34; + + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); +} + +static void test_hash_3do_launchme_case_insensitive() +{ + /* main executable for "Captain Quazar" is "launchme" */ + /* main executable for "Rise of the Robots" is "launchMe" */ + /* main executable for "Road Rash" is "LaunchMe" */ + /* main executable for "Sewer Shark" is "Launchme" */ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 6543, &image_size); + char hash_file[33]; + const char* expected_md5 = "59622882e3261237e8a1e396825ae4f5"; + + memcpy(&image[2048 + 0x14 + 0x48 + 0x20], "launchme", 8); + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); +} + +static void test_hash_3do_no_launchme() +{ + /* this case should not happen */ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 6543, &image_size); + char hash_file[33]; + + memcpy(&image[2048 + 0x14 + 0x48 + 0x20], "filename", 8); + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); +} + +static void test_hash_3do_long_directory() +{ + /* root directory for "Dragon's Lair" uses more than one sector */ + size_t image_size; + uint8_t* image = generate_3do_bin(3, 6543, &image_size); + char hash_file[33]; + const char* expected_md5 = "8979e876ae502e0f79218f7ff7bd8c2a"; + + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_arduboy() +{ + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; + + const char* hex_input = + ":100000000C94690D0C94910D0C94910D0C94910D20\n" + ":100010000C94910D0C94910D0C94910D0C94910DE8\n" + ":100020000C94910D0C94910D0C94C32A0C94352BC7\n" + ":00000001FF\n"; + mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_arduboy_crlf() +{ + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; + + const char* hex_input = + ":100000000C94690D0C94910D0C94910D0C94910D20\r\n" + ":100010000C94910D0C94910D0C94910D0C94910DE8\r\n" + ":100020000C94910D0C94910D0C94C32A0C94352BC7\r\n" + ":00000001FF\r\n"; + mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_arduboy_no_final_lf() +{ + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; + + const char* hex_input = + ":100000000C94690D0C94910D0C94910D0C94910D20\n" + ":100010000C94910D0C94910D0C94910D0C94910DE8\n" + ":100020000C94910D0C94910D0C94C32A0C94352BC7\n" + ":00000001FF"; + mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_atari_7800() +{ + size_t image_size; + uint8_t* image = generate_atari_7800_file(16, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_ATARI_7800, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "455f07d8500f3fabc54906737866167f"); + ASSERT_NUM_EQUALS(image_size, 16384); +} + +static void test_hash_atari_7800_with_header() +{ + size_t image_size; + uint8_t* image = generate_atari_7800_file(16, 1, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_ATARI_7800, image, image_size); + free(image); + + /* NOTE: expectation is that this hash matches the hash in test_hash_atari_7800 */ + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "455f07d8500f3fabc54906737866167f"); + ASSERT_NUM_EQUALS(image_size, 16384 + 128); +} + +/* ========================================================================= */ + +static void test_hash_atari_jaguar_cd() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 60024, 0, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c324d95dc5831c2d5c470eefb18c346b"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track02.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_atari_jaguar_cd_byteswapped() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 60024, 1, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c324d95dc5831c2d5c470eefb18c346b"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track02.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_atari_jaguar_cd_track3() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(1470, 99200, 1, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "060e9d223c584b581cf7d7ce17c0e5dc"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track03.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_atari_jaguar_cd_no_header() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 32768, 1, &image_size); + char hash_file[33], hash_iterator[33]; + + image[2 + 64 + 12] = 'B'; /* corrupt the header */ + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track02.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_atari_jaguar_cd_no_sessions() +{ + const char* cue_file = + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 99200, 1, &image_size); + char hash_file[33], hash_iterator[33]; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track03.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +extern const char* _rc_hash_jaguar_cd_homebrew_hash; + +static void test_hash_atari_jaguar_cd_homebrew() +{ + /* Jaguar CD homebrew games all appear to have a common bootloader in the primary boot executable space. They only + * differ in a secondary executable in the second track (part of the first session). This doesn't appear to be + * intentional behavior based on the CD BIOS documentation, which states that all developer code should be in the + * first track of the second session. I speculate this is done to work around the authentication logic. */ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size, image_size2; + uint8_t* image = generate_jaguarcd_bin(2, 45760, 1, &image_size); + uint8_t* image2 = generate_jaguarcd_bin(2, 986742, 1, &image_size2); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "3fdf70e362c845524c9e447aacaed0a9"; + + image2[0x60] = 0x21; /* ATARI APPROVED DATA HEADER ATRI! */ + memcpy(&image2[0xA2], &image2[0x62], 8); /* addr / size */ + memcpy(&image2[0x62], "RTKARTKARTKARTKA", 16); /* KARTKARTKARTKART */ + memcpy(&image2[0x72], "RTKARTKARTKARTKA", 16); + memcpy(&image2[0x82], "RTKARTKARTKARTKA", 16); + memcpy(&image2[0x92], "RTKARTKARTKARTKA", 16); + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(2, "track02.bin", image2, image_size2); + mock_file(1, "track03.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + _rc_hash_jaguar_cd_homebrew_hash = "4e4114b2675eff21bb77dd41e141ddd6"; /* mock the hash of the homebrew bootloader */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + _rc_hash_jaguar_cd_homebrew_hash = NULL; + free(image); + free(image2); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_dreamcast_single_bin() +{ + size_t image_size; + uint8_t* image = generate_dreamcast_bin(45000, 1458208, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "2a550500caee9f06e5d061fe10a46f6e"; + + mock_file(0, "track03.bin", image, image_size); + mock_file_first_sector(0, 45000); + mock_file(1, "game.gdi", (uint8_t*)"game.bin", 8); + mock_cd_num_tracks(3); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.gdi"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.gdi", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_dreamcast_split_bin() +{ + size_t image_size; + uint8_t* image = generate_dreamcast_bin(548106, 1830912, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "771e56aff169230ede4505013a4bcf9f"; + + mock_file(0, "game.gdi", (uint8_t*)"game.bin", 8); + mock_file(1, "track03.bin", image, image_size); + mock_file_first_sector(1, 45000); + mock_file(2, "track26.bin", image, image_size); + mock_file_first_sector(2, 548106); + mock_cd_num_tracks(26); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.gdi"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.gdi", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_dreamcast_cue() +{ + const char* cue_file = + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 MODE1/2352\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 MODE1/2352\n" + " INDEX 01 00:00:00\n" + "FILE \"track04.bin\" BINARY\n" + " TRACK 04 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track05.bin\" BINARY\n" + " TRACK 05 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:03:00\n"; + size_t image_size; + uint8_t* image = convert_to_2352(generate_dreamcast_bin(45000, 1697028, &image_size), &image_size, 45000); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c952864c3364591d2a8793ce2cfbf3a0"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track01.bin", image, 1425312); /* 606 sectors */ + mock_file(2, "track02.bin", image, 1589952); /* 676 sectors */ + mock_file(3, "track03.bin", image, image_size); /* 737 sectors */ + mock_file(4, "track04.bin", image, 1237152); /* 526 sectors */ + mock_file(5, "track05.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual first_track_sector calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_gamecube() +{ + size_t image_size; + uint8_t* image = generate_gamecube_iso(32, &image_size); + mock_file(0, "test.iso", image, image_size); + char hash[33]; + int result = rc_hash_generate_from_file(hash, RC_CONSOLE_GAMECUBE, "test.iso"); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "c7803b704fa43d22d8f6e55f4789cb45"); + ASSERT_NUM_EQUALS(image_size, 32 * 1024 * 1024); +} + +/* ========================================================================= */ + +static void test_hash_nes_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(image_size, 32768); +} + +static void test_hash_nes_32k_with_header() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + /* NOTE: expectation is that this hash matches the hash in test_hash_nes_32k */ + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(image_size, 32768 + 16); +} + +static void test_hash_nes_256k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(256, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "545d527301b8ae148153988d6c4fcb84"); + ASSERT_NUM_EQUALS(image_size, 262144); +} + +static void test_hash_fds_two_sides() +{ + size_t image_size; + uint8_t* image = generate_fds_file(2, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "fd770d4d34c00760fabda6ad294a8f0b"); + ASSERT_NUM_EQUALS(image_size, 65500 * 2); +} + +static void test_hash_fds_two_sides_with_header() +{ + size_t image_size; + uint8_t* image = generate_fds_file(2, 1, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + /* NOTE: expectation is that this hash matches the hash in test_hash_fds_two_sides */ + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "fd770d4d34c00760fabda6ad294a8f0b"); + ASSERT_NUM_EQUALS(image_size, 65500 * 2 + 16); +} + +static void test_hash_nes_file_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash[33]; + int result = hash_mock_file("test.nes", hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(image_size, 32768); +} + +static void test_hash_nes_iterator_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash1[33], hash2[33]; + int result1, result2; + struct rc_hash_iterator iterator; + iterate_mock_file(&iterator, "test.nes", image, image_size); + result1 = rc_hash_iterate(hash1, &iterator); + result2 = rc_hash_iterate(hash2, &iterator); + rc_hash_destroy_iterator(&iterator); + free(image); + + ASSERT_NUM_EQUALS(result1, 1); + ASSERT_STR_EQUALS(hash1, "6a2305a2b6675a97ff792709be1ca857"); + + ASSERT_NUM_EQUALS(result2, 0); + ASSERT_STR_EQUALS(hash2, ""); +} + +static void test_hash_nes_file_iterator_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash1[33], hash2[33]; + int result1, result2; + struct rc_hash_iterator iterator; + rc_hash_initialize_iterator(&iterator, "test.nes", image, image_size); + result1 = rc_hash_iterate(hash1, &iterator); + result2 = rc_hash_iterate(hash2, &iterator); + rc_hash_destroy_iterator(&iterator); + free(image); + + ASSERT_NUM_EQUALS(result1, 1); + ASSERT_STR_EQUALS(hash1, "6a2305a2b6675a97ff792709be1ca857"); + + ASSERT_NUM_EQUALS(result2, 0); + ASSERT_STR_EQUALS(hash2, ""); +} + +/* ========================================================================= */ + +static void test_hash_n64(uint8_t* buffer, size_t buffer_size, const char* expected_hash) +{ + char hash[33]; + int result; + + rc_hash_reset_filereader(); /* explicitly unset the filereader */ + result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO_64, buffer, buffer_size); + init_mock_filereader(); /* restore the mock filereader */ + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, expected_hash); +} + +static void test_hash_n64_file(const char* filename, uint8_t* buffer, size_t buffer_size, const char* expected_hash) +{ + char hash_file[33], hash_iterator[33]; + mock_file(0, filename, buffer, buffer_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_64, filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +/* ========================================================================= */ + +static void test_hash_nds() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + mock_file(0, "game.nds", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DS, "game.nds"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +static void test_hash_nds_supercard() +{ + size_t image_size, image2_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + ASSERT_PTR_NOT_NULL(image); + ASSERT_NUM_GREATER(image_size, 0); + + /* inject the SuperCard header (512 bytes) */ + image2_size = image_size + 512; + uint8_t* image2 = malloc(image2_size); + ASSERT_PTR_NOT_NULL(image2); + memcpy(&image2[512], &image[0], image_size); + memset(&image2[0], 0, 512); + image2[0] = 0x2E; + image2[1] = 0x00; + image2[2] = 0x00; + image2[3] = 0xEA; + image2[0xB0] = 0x44; + image2[0xB1] = 0x46; + image2[0xB2] = 0x96; + image2[0xB3] = 0x00; + + mock_file(0, "game.nds", image2, image2_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DS, "game.nds"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(image2); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +static void test_hash_nds_buffered() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_buffer[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + /* test file hash */ + int result_buffer = rc_hash_generate_from_buffer(hash_buffer, RC_CONSOLE_NINTENDO_DS, image, image_size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", image, image_size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_buffer, 1); + ASSERT_STR_EQUALS(hash_buffer, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +/* ========================================================================= */ + +static void test_hash_dsi() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + mock_file(0, "game.nds", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DSI, "game.nds"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +static void test_hash_dsi_buffered() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_buffer[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + /* test file hash */ + int result_buffer = rc_hash_generate_from_buffer(hash_buffer, RC_CONSOLE_NINTENDO_DSI, image, image_size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", image, image_size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_buffer, 1); + ASSERT_STR_EQUALS(hash_buffer, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +/* ========================================================================= */ + +static void test_hash_neogeocd() +{ + const char* ipl_txt = "FIXA.FIX,0,0\r\nPROG.PRG,0,0\r\nSOUND.PCM,0,0\r\n\x1a"; + const size_t prog_prg_size = 273470; + uint8_t* prog_prg = generate_generic_file(prog_prg_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "96f35b20c6cf902286da45e81a50b2a3"; + + generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); + generate_iso9660_file(image, "PROG.PRG", prog_prg, prog_prg_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(prog_prg); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_neogeocd_multiple_prg() +{ + const char* ipl_txt = "FIXA.FIX,0,0\r\nPROG1.PRG,0,0\r\nSOUND.PCM,0,0\r\nPROG2.PRG,0,44000\r\n\x1a"; + const size_t prog1_prg_size = 273470; + uint8_t* prog1_prg = generate_generic_file(prog1_prg_size); + const size_t prog2_prg_size = 13768; + uint8_t* prog2_prg = generate_generic_file(prog2_prg_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "d62df483c4786d3c63f27b6c5f17eeca"; + + generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); + generate_iso9660_file(image, "PROG1.PRG", prog1_prg, prog1_prg_size); + generate_iso9660_file(image, "PROG2.PRG", prog2_prg, prog2_prg_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(prog1_prg); + free(prog2_prg); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_neogeocd_lowercase_ipl_contents() +{ + const char* ipl_txt = "fixa.fix,0,0\r\nprog.prg,0,0\r\nsound.pcm,0,0\r\n\x1a"; + const size_t prog_prg_size = 273470; + uint8_t* prog_prg = generate_generic_file(prog_prg_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "96f35b20c6cf902286da45e81a50b2a3"; + + generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); + generate_iso9660_file(image, "PROG.PRG", prog_prg, prog_prg_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(prog_prg); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_pce_cd() +{ + size_t image_size; + uint8_t* image = generate_pce_cd_bin(72, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "6565819195a49323e080e7539b54f251"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC_ENGINE_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_pce_cd_invalid_header() +{ + size_t image_size; + uint8_t* image = generate_pce_cd_bin(72, &image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* make the header not match */ + image[2048 + 0x24] = 0x34; + + test_hash_unknown_format(RC_CONSOLE_PC_ENGINE_CD, "game.cue"); + + free(image); +} + +/* ========================================================================= */ + +static void test_hash_pcfx() +{ + size_t image_size; + uint8_t* image = generate_pcfx_bin(72, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "0a03af66559b8529c50c4e7788379598"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PCFX, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_pcfx_invalid_header() +{ + size_t image_size; + uint8_t* image = generate_pcfx_bin(72, &image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* make the header not match */ + image[12] = 0x34; + + test_hash_unknown_format(RC_CONSOLE_PCFX, "game.cue"); + + free(image); +} + +static void test_hash_pcfx_pce_cd() +{ + /* Battle Heat is formatted as a PC-Engine CD */ + size_t image_size; + uint8_t* image = generate_pce_cd_bin(72, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "6565819195a49323e080e7539b54f251"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + mock_file(2, "game2.bin", image, image_size); /* PC-Engine CD check only applies to track 2 */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PCFX, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd() +{ + /* BOOT=cdrom:\SLUS_007.45 */ + size_t image_size; + uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd_no_system_cnf() +{ + size_t image_size; + uint8_t* image; + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "e494c79a7315be0dc3e8571c45df162c"; + unsigned binary_size = 0x12000; + const unsigned sectors_needed = (((binary_size + 2047) / 2048) + 20); + uint8_t* exe; + + image = generate_iso9660_bin(sectors_needed, "HOMEBREW", &image_size); + exe = generate_iso9660_file(image, "PSX.EXE", NULL, binary_size); + memcpy(exe, "PS-X EXE", 8); + binary_size -= 2048; + exe[28] = binary_size & 0xFF; + exe[29] = (binary_size >> 8) & 0xFF; + exe[30] = (binary_size >> 16) & 0xFF; + exe[31] = (binary_size >> 24) & 0xFF; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd_exe_in_subfolder() +{ + /* BOOT=cdrom:\bin\SLUS_012.37 */ + size_t image_size; + uint8_t* image = generate_psx_bin("bin\\SCES_012.37", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "674018e23a4052113665dfb264e9c2fc"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd_extra_slash() +{ + /* BOOT=cdrom:\\SLUS_007.45 */ + size_t image_size; + uint8_t* image = generate_psx_bin("\\SLUS_007.45", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_ps2_iso() +{ + size_t image_size; + uint8_t* image = generate_ps2_bin("SLUS_200.64", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "01a517e4ad72c6c2654d1b839be7579d"; + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION_2, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_ps2_psx() +{ + size_t image_size; + uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION_2, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); /* PSX hash */ + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation (should not generate PS2 hash for PSX file) */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_psp() +{ + const size_t param_sfo_size = 690; + uint8_t* param_sfo = generate_generic_file(param_sfo_size); + const size_t eboot_bin_size = 273470; + uint8_t* eboot_bin = generate_generic_file(eboot_bin_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "80c0b42b2d89d036086869433a176a03"; + + generate_iso9660_file(image, "PSP_GAME\\PARAM.SFO", param_sfo, param_sfo_size); + generate_iso9660_file(image, "PSP_GAME\\SYSDIR\\EBOOT.BIN", eboot_bin, eboot_bin_size); + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(eboot_bin); + free(param_sfo); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psp_video() +{ + const size_t param_sfo_size = 690; + uint8_t* param_sfo = generate_generic_file(param_sfo_size); + const size_t eboot_bin_size = 273470; + uint8_t* eboot_bin = generate_generic_file(eboot_bin_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + + /* UMD video disc may have an UPDATE folder, but nothing in the PSP_GAME or SYSDIR folders. */ + generate_iso9660_file(image, "PSP_GAME\\SYSDIR\\UPDATE\\EBOOT.BIN", eboot_bin, eboot_bin_size); + /* the PARAM.SFO file is in the UMD_VIDEO folder. */ + generate_iso9660_file(image, "UMD_VIDEO\\PARAM.SFO", param_sfo, param_sfo_size); + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(eboot_bin); + free(param_sfo); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_sega_cd() +{ + /* the first 512 bytes of sector 0 are a volume header and ROM header. + * generate a generic block and add the Sega CD marker */ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "574498e1453cb8934df60c4ab906e783"; + memcpy(image, "SEGADISCSYSTEM ", 16); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SEGA_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_sega_cd_invalid_header() +{ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + test_hash_unknown_format(RC_CONSOLE_SEGA_CD, "game.cue"); + + free(image); +} + +static void test_hash_saturn() +{ + /* the first 512 bytes of sector 0 are a volume header and ROM header. + * generate a generic block and add the Sega CD marker */ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "4cd9c8e41cd8d137be15bbe6a93ae1d8"; + memcpy(image, "SEGA SEGASATURN ", 16); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SATURN, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_saturn_invalid_header() +{ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + test_hash_unknown_format(RC_CONSOLE_SATURN, "game.cue"); + + free(image); +} + +/* ========================================================================= */ + +static void assert_valid_m3u(const char* disc_filename, const char* m3u_filename, const char* m3u_contents) +{ + const size_t size = 131072; + uint8_t* image = generate_generic_file(size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "a0f425b23200568132ba76b2405e3933"; + + mock_file(0, disc_filename, image, size); + mock_file(1, m3u_filename, (uint8_t*)m3u_contents, strlen(m3u_contents)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC8800, m3u_filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_m3u_buffered() +{ + const size_t size = 131072; + uint8_t* image = generate_generic_file(size); + char hash_iterator[33]; + const char* m3u_filename = "test.m3u"; + const char* filename = "test.d88"; + const char* expected_md5 = "a0f425b23200568132ba76b2405e3933"; + uint8_t* m3u_contents = (uint8_t*)filename; + const size_t m3u_size = strlen(filename); + + mock_file(0, filename, image, size); + mock_file(1, m3u_filename, m3u_contents, m3u_size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, m3u_contents, m3u_size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_m3u_with_comments() +{ + assert_valid_m3u("test.d88", "test.m3u", + "#EXTM3U\r\n\r\n#EXTBYT:131072\r\ntest.d88\r\n"); +} + +static void test_hash_m3u_empty() +{ + char hash_file[33], hash_iterator[33]; + const char* m3u_filename = "test.m3u"; + const char* m3u_contents = "#EXTM3U\r\n\r\n#EXTBYT:131072\r\n"; + + mock_file(0, m3u_filename, (uint8_t*)m3u_contents, strlen(m3u_contents)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC8800, m3u_filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_m3u_trailing_whitespace() +{ + assert_valid_m3u("test.d88", "test.m3u", + "#EXTM3U \r\n \r\n#EXTBYT:131072 \r\ntest.d88 \t \r\n"); +} + +static void test_hash_m3u_line_ending() +{ + assert_valid_m3u("test.d88", "test.m3u", + "#EXTM3U\n\n#EXTBYT:131072\ntest.d88\n"); +} + +static void test_hash_m3u_extension_case() +{ + assert_valid_m3u("test.D88", "test.M3U", + "#EXTM3U\r\n\r\n#EXTBYT:131072\r\ntest.D88\r\n"); +} + +static void test_hash_m3u_relative_path() +{ + assert_valid_m3u("folder1/folder2/test.d88", "folder1/test.m3u", + "#EXTM3U\r\n\r\n#EXTBYT:131072\r\nfolder2/test.d88"); +} + +static void test_hash_m3u_absolute_path(const char* absolute_path) +{ + char m3u_contents[128]; + snprintf(m3u_contents, sizeof(m3u_contents), "#EXTM3U\r\n\r\n#EXTBYT:131072\r\n%s", absolute_path); + + assert_valid_m3u(absolute_path, "relative/test.m3u", m3u_contents); +} + +static void test_hash_file_without_ext() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + char hash_file[33], hash_iterator[33]; + const char* filename = "test"; + + mock_file(0, filename, image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO, filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + + /* specifying a console will use the appropriate hasher */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, "6a2305a2b6675a97ff792709be1ca857"); + + /* no extension will use the default full file iterator, so hash should include header */ + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, "64b131c5c7fec32985d9c99700babb7e"); +} + +/* ========================================================================= */ + +void test_hash(void) { + TEST_SUITE_BEGIN(); + + init_mock_filereader(); + init_mock_cdreader(); + + /* 3DO */ + TEST(test_hash_3do_bin); + TEST(test_hash_3do_cue); + TEST(test_hash_3do_iso); + TEST(test_hash_3do_invalid_header); + TEST(test_hash_3do_launchme_case_insensitive); + TEST(test_hash_3do_no_launchme); + TEST(test_hash_3do_long_directory); + + /* Amstrad CPC */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); + + /* Apple II */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.nib", 232960, "96e8d33bdc385fd494327d6e6791cbe4"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); + + /* Arduboy */ + TEST(test_hash_arduboy); + TEST(test_hash_arduboy_crlf); + TEST(test_hash_arduboy_no_final_lf); + + /* Arcade */ + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "game.7z", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "roms\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "C:\\roms\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/roms/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/games/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/roms/game.7z", "c8d46d341bea4fd5bff866a65ff8aea9"); + + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/nes_game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/nes/game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "C:\\roms\\nes\\game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "nes\\game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/snes/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/nes2/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/coleco/game.zip", "c546f63ae7de98add4b9f221a4749260"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/msx/game.zip", "59ab85f6b56324fd81b4e324b804c29f"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/pce/game.zip", "c414a783f3983bbe2e9e01d9d5320c7e"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/sgx/game.zip", "db545ab29694bfda1010317d4bac83b8"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/tg16/game.zip", "8b6c5c2e54915be2cdba63973862e143"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/fds/game.zip", "c0c135a97e8c577cfdf9204823ff211f"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/gamegear/game.zip", "f6f471e952b8103032b723f57bdbe767"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/sms/game.zip", "43f35f575dead94dd2f42f9caf69fe5a"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/megadriv/game.zip", "f99d0aaf12ba3eb6ced9878c76692c63"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/sg1000/game.zip", "e8f6c711c4371f09537b4f2a7a304d6c"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/spectrum/game.zip", "a5f62157b2617bd728c4b1bc885c29e9"); + TEST_PARAMS3(test_hash_filename, RC_CONSOLE_ARCADE, "/home/user/ngp/game.zip", "d4133b74c4e57274ca514e27a370dcb6"); + + /* Arcadia 2001 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ARCADIA_2001, "test.bin", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + + /* Atari 2600 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ATARI_2600, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + + /* Atari 7800 */ + TEST(test_hash_atari_7800); + TEST(test_hash_atari_7800_with_header); + + /* Atari Jaguar */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ATARI_JAGUAR, "test.jag", 0x400000, "a247ec8a8c42e18fcb80702dfadac14b"); + + /* Atari Jaguar CD */ + TEST(test_hash_atari_jaguar_cd); + TEST(test_hash_atari_jaguar_cd_byteswapped); + TEST(test_hash_atari_jaguar_cd_track3); + TEST(test_hash_atari_jaguar_cd_no_header); + TEST(test_hash_atari_jaguar_cd_no_sessions); + TEST(test_hash_atari_jaguar_cd_homebrew); + + /* Colecovision */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COLECOVISION, "test.col", 16384, "455f07d8500f3fabc54906737866167f"); + + /* Commodore 64 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.nib", 327936, "e7767d32b23e3fa62c5a250a08caeba3"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); + + /* Dreamcast */ + TEST(test_hash_dreamcast_single_bin); + TEST(test_hash_dreamcast_split_bin); + TEST(test_hash_dreamcast_cue); + + /* Elektor TV Games Computer */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, "test.pgm", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, "test.tvc", 1861, "37097124a29aff663432d049654a17dc"); + + /* Fairchild Channel F */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_FAIRCHILD_CHANNEL_F, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_FAIRCHILD_CHANNEL_F, "test.chf", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + + /* Gameboy */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY, "test.gb", 131072, "a0f425b23200568132ba76b2405e3933"); + + /* Gameboy Color */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY_COLOR, "test.gbc", 2097152, "cf86acf519625a25a17b1246975e90ae"); + + /* Gameboy Advance */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY_COLOR, "test.gba", 4194304, "a247ec8a8c42e18fcb80702dfadac14b"); + + /* Gamecube */ + TEST(test_hash_gamecube); + + /* Game Gear */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAME_GEAR, "test.gg", 524288, "68f0f13b598e0b66461bc578375c3888"); + + /* Intellivision */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_INTELLIVISION, "test.bin", 8192, "ce1127f881b40ce6a67ecefba50e2835"); + + /* Interton VC 4000 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_INTERTON_VC_4000, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + + /* Magnavox Odyssey 2 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MAGNAVOX_ODYSSEY2, "test.bin", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + + /* Master System */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MASTER_SYSTEM, "test.sms", 131072, "a0f425b23200568132ba76b2405e3933"); + + /* Mega Drive */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MEGA_DRIVE, "test.md", 1048576, "da9461b3b0f74becc3ccf6c2a094c516"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MEGA_DRIVE, "test.md", 1048576, "da9461b3b0f74becc3ccf6c2a094c516"); + + /* Mega Duck */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MEGADUCK, "test.bin", 65536, "8e6576cd5c21e44e0bbfc4480577b040"); + + /* MSX */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); + + /* Neo Geo CD */ + TEST(test_hash_neogeocd); + TEST(test_hash_neogeocd_multiple_prg); + TEST(test_hash_neogeocd_lowercase_ipl_contents); + + /* Neo Geo Pocket */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_NEOGEO_POCKET, "test.ngc", 2097152, "cf86acf519625a25a17b1246975e90ae"); + + /* NES */ + TEST(test_hash_nes_32k); + TEST(test_hash_nes_32k_with_header); + TEST(test_hash_nes_256k); + TEST(test_hash_fds_two_sides); + TEST(test_hash_fds_two_sides_with_header); + + TEST(test_hash_nes_file_32k); + TEST(test_hash_nes_file_iterator_32k); + TEST(test_hash_nes_iterator_32k); + + /* Nintendo 64 */ + TEST_PARAMS3(test_hash_n64, test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS3(test_hash_n64, test_rom_v64, sizeof(test_rom_v64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS3(test_hash_n64, test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.z64", test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.v64", test_rom_v64, sizeof(test_rom_v64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.n64", test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.n64", test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); /* misnamed */ + TEST_PARAMS4(test_hash_n64_file, "game.z64", test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); /* misnamed */ + TEST_PARAMS3(test_hash_n64, test_rom_ndd, sizeof(test_rom_ndd), "a698b32a52970d8a52a5a52c83acc2a9"); + + /* Nintendo DS */ + TEST(test_hash_nds); + TEST(test_hash_nds_supercard); + TEST(test_hash_nds_buffered); + + /* Nintendo DSi */ + TEST(test_hash_dsi); + TEST(test_hash_dsi_buffered); + + /* Oric (no fixed file size) */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ORIC, "test.tap", 18119, "953a2baa3232c63286aeae36b2172cef"); + + /* PC-8800 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); + + /* PC Engine */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 524288, "68f0f13b598e0b66461bc578375c3888"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 524288 + 512, "258c93ebaca1c3f488ab48218e5e8d38"); + + /* PC Engine CD */ + TEST(test_hash_pce_cd); + TEST(test_hash_pce_cd_invalid_header); + + /* PC-FX */ + TEST(test_hash_pcfx); + TEST(test_hash_pcfx_invalid_header); + TEST(test_hash_pcfx_pce_cd); + + /* Playstation */ + TEST(test_hash_psx_cd); + TEST(test_hash_psx_cd_no_system_cnf); + TEST(test_hash_psx_cd_exe_in_subfolder); + TEST(test_hash_psx_cd_extra_slash); + + /* Playstation 2 */ + TEST(test_hash_ps2_iso); + TEST(test_hash_ps2_psx); + + /* Playstation Portable */ + TEST(test_hash_psp); + TEST(test_hash_psp_video); + + /* Pokemon Mini */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_POKEMON_MINI, "test.min", 524288, "68f0f13b598e0b66461bc578375c3888"); + + /* Sega CD */ + TEST(test_hash_sega_cd); + TEST(test_hash_sega_cd_invalid_header); + + /* Sega 32X */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SEGA_32X, "test.bin", 3145728, "07d733f252896ec41b4fd521fe610e2c"); + + /* Sega Saturn */ + TEST(test_hash_saturn); + TEST(test_hash_saturn_invalid_header); + + /* SG-1000 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.sg", 32768, "6a2305a2b6675a97ff792709be1ca857"); + + /* SNES */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_NINTENDO, "test.smc", 524288, "68f0f13b598e0b66461bc578375c3888"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_NINTENDO, "test.smc", 524288 + 512, "258c93ebaca1c3f488ab48218e5e8d38"); + + /* TI-83 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TI83, "test.83g", 1695, "bfb6048395a425c69743900785987c42"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TI83, "test.83p", 2500, "6e81d530ee9a79d4f4f505729ad74bb5"); + + /* TIC-80 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TIC80, "test.tic", 67682, "79b96f4ffcedb3ce8210a83b22cd2c69"); + + /* Uzebox */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_UZEBOX, "test.uze", 53654, "a9aab505e92edc034d3c732869159789"); + + /* Vectrex */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.vec", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + + /* VirtualBoy */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.vb", 524288, "68f0f13b598e0b66461bc578375c3888"); + + /* Watara Supervision */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPERVISION, "test.sv", 32768, "6a2305a2b6675a97ff792709be1ca857"); + + /* WASM-4 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WASM4, "test.wasm", 33454, "bce38bb5f05622fc7e0e56757059d180"); + + /* WonderSwan */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WONDERSWAN, "test.ws", 524288, "68f0f13b598e0b66461bc578375c3888"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WONDERSWAN, "test.wsc", 4194304, "a247ec8a8c42e18fcb80702dfadac14b"); + + /* m3u support */ + TEST(test_hash_m3u_buffered); + TEST(test_hash_m3u_with_comments); + TEST(test_hash_m3u_empty); + TEST(test_hash_m3u_trailing_whitespace); + TEST(test_hash_m3u_line_ending); + TEST(test_hash_m3u_extension_case); + TEST(test_hash_m3u_relative_path); + TEST_PARAMS1(test_hash_m3u_absolute_path, "/absolute/test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "\\absolute\\test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "C:\\absolute\\test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "\\\\server\\absolute\\test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "samba:/absolute/test.d88"); + + /* other */ + TEST(test_hash_file_without_ext); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rurl/test_url.c b/src/rcheevos/test/rurl/test_url.c new file mode 100644 index 000000000..d5bc02393 --- /dev/null +++ b/src/rcheevos/test/rurl/test_url.c @@ -0,0 +1,127 @@ +#include "rc_url.h" + +#include "../rcheevos/rc_compat.h" +#include "../test_framework.h" + +#include + +static void test_ping(const char* rich_presence, const char* expected_param) +{ + char url_buffer[256]; + char post_buffer[256]; + + ASSERT_NUM_EQUALS(rc_url_ping(url_buffer, sizeof(url_buffer), post_buffer, sizeof(post_buffer), "User", "Token", 1234U, rich_presence), 0); + ASSERT_STR_EQUALS(url_buffer, "http://retroachievements.org/dorequest.php?r=ping&u=User&g=1234"); + + if (expected_param) { + char expected_post[256]; + snprintf(expected_post, sizeof(expected_post), "t=Token&%s", expected_param); + ASSERT_STR_EQUALS(post_buffer, expected_post); + } else { + ASSERT_STR_EQUALS(post_buffer, "t=Token"); + } +} + +static void test_ping_url_buffer(size_t buffer_size) +{ + char url_buffer[256]; + char post_buffer[256]; + unsigned* overflow = (unsigned*)(&url_buffer[buffer_size]); + *overflow = 0xCDCDCDCD; + + /* expectant URL length is 65 character, buffer must be at least 66 bytes to fit it */ + int result = rc_url_ping(url_buffer, buffer_size, post_buffer, sizeof(post_buffer), "Player", "ApiToken", 5678U, NULL); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + if (buffer_size < 66) { + ASSERT_NUM_EQUALS(result, -1); + if (buffer_size) + ASSERT_STR_EQUALS(url_buffer, ""); + } else { + ASSERT_NUM_EQUALS(result, 0); + ASSERT_NUM_EQUALS(strlen(url_buffer), 65); + } +} + +static void test_ping_post_buffer(size_t buffer_size) +{ + char url_buffer[256]; + char post_buffer[256]; + unsigned* overflow = (unsigned*)(&post_buffer[buffer_size]); + *overflow = 0xCDCDCDCD; + + /* expectant post length is 26 character, buffer must be at least 27 bytes to fit it */ + int result = rc_url_ping(url_buffer, sizeof(url_buffer), post_buffer, buffer_size, "Player", "ApiToken", 5678U, "50% to #1"); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + if (buffer_size < 27) { + ASSERT_NUM_EQUALS(result, -1); + if (buffer_size) + ASSERT_STR_EQUALS(post_buffer, ""); + } else { + ASSERT_NUM_EQUALS(result, 0); + ASSERT_NUM_EQUALS(strlen(post_buffer), 26); + } +} + +static void test_lbinfo() { + char url_buffer[256]; + + ASSERT_NUM_EQUALS(rc_url_get_lboard_entries(url_buffer, sizeof(url_buffer), 1234, 401, 100), 0); + ASSERT_STR_EQUALS(url_buffer, "http://retroachievements.org/dorequest.php?r=lbinfo&i=1234&o=400&c=100"); +} + +static void test_lbinfo_no_first() { + char url_buffer[256]; + + ASSERT_NUM_EQUALS(rc_url_get_lboard_entries(url_buffer, sizeof(url_buffer), 1234, 0, 50), 0); + ASSERT_STR_EQUALS(url_buffer, "http://retroachievements.org/dorequest.php?r=lbinfo&i=1234&c=50"); +} + +static void test_lbinfo_near_user() { + char url_buffer[256]; + + ASSERT_NUM_EQUALS(rc_url_get_lboard_entries_near_user(url_buffer, sizeof(url_buffer), 1234, "User", 11), 0); + ASSERT_STR_EQUALS(url_buffer, "http://retroachievements.org/dorequest.php?r=lbinfo&i=1234&u=User&c=11"); +} + +void test_url(void) { + TEST_SUITE_BEGIN(); + + /* rc_url_ping */ + TEST_PARAMS2(test_ping, NULL, NULL); + TEST_PARAMS2(test_ping, "", NULL); + TEST_PARAMS2(test_ping, " ", "m=+"); + TEST_PARAMS2(test_ping, "Test", "m=Test"); + TEST_PARAMS2(test_ping, "This is a test.", "m=This+is+a+test."); + TEST_PARAMS2(test_ping, "!Test?", "m=%21Test%3f"); + + TEST_PARAMS1(test_ping_url_buffer, 0); + TEST_PARAMS1(test_ping_url_buffer, 10); + TEST_PARAMS1(test_ping_url_buffer, 50); + TEST_PARAMS1(test_ping_url_buffer, 64); + TEST_PARAMS1(test_ping_url_buffer, 65); + TEST_PARAMS1(test_ping_url_buffer, 66); /* this is the threshold */ + TEST_PARAMS1(test_ping_url_buffer, 67); + TEST_PARAMS1(test_ping_url_buffer, 100); + + TEST_PARAMS1(test_ping_post_buffer, 0); + TEST_PARAMS1(test_ping_post_buffer, 25); + TEST_PARAMS1(test_ping_post_buffer, 26); + TEST_PARAMS1(test_ping_post_buffer, 27); /* this is the threshold */ + TEST_PARAMS1(test_ping_post_buffer, 28); + TEST_PARAMS1(test_ping_post_buffer, 50); + + /* rc_url_lbinfo */ + TEST(test_lbinfo); + TEST(test_lbinfo_no_first); + TEST(test_lbinfo_near_user); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/test.c b/src/rcheevos/test/test.c new file mode 100644 index 000000000..6ff3bd107 --- /dev/null +++ b/src/rcheevos/test/test.c @@ -0,0 +1,122 @@ +#include "rc_internal.h" + +#include "test_framework.h" + +#include + +#ifndef RC_DISABLE_LUA +#include "lua.h" +#include "lauxlib.h" + +#include "rcheevos/mock_memory.h" +#endif + +#define TIMING_TEST 0 + +static void test_lua(void) { + { + /*------------------------------------------------------------------------ + TestLua + ------------------------------------------------------------------------*/ + +#ifndef RC_DISABLE_LUA + + lua_State* L; + const char* luacheevo = "return { test = function(peek, ud) return peek(0, 4, ud) end }"; + unsigned char ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + L = luaL_newstate(); + luaL_loadbufferx(L, luacheevo, strlen(luacheevo), "luacheevo.lua", "t"); + lua_call(L, 0, 1); + + memory.ram = ram; + memory.size = sizeof(ram); + + trigger = rc_parse_trigger(buffer, "@test=0xX0", L, 1); + assert(rc_test_trigger(trigger, peek, &memory, L) != 0); + + lua_close(L); +#endif /* RC_DISABLE_LUA */ + } +} + +extern void test_timing(); + +extern void test_condition(); +extern void test_memref(); +extern void test_operand(); +extern void test_condset(); +extern void test_trigger(); +extern void test_value(); +extern void test_format(); +extern void test_lboard(); +extern void test_richpresence(); +extern void test_runtime(); +extern void test_runtime_progress(); +extern void test_client(); + +extern void test_consoleinfo(); +extern void test_rc_libretro(); +extern void test_rc_validate(); + +extern void test_url(); + +extern void test_cdreader(); +extern void test_hash(); + +extern void test_rapi_common(); +extern void test_rapi_user(); +extern void test_rapi_runtime(); +extern void test_rapi_info(); +extern void test_rapi_editor(); + +TEST_FRAMEWORK_DECLARATIONS() + +int main(void) { + TEST_FRAMEWORK_INIT(); + +#if TIMING_TEST + test_timing(); +#else + test_memref(); + test_operand(); + test_condition(); + test_condset(); + test_trigger(); + test_value(); + test_format(); + test_lboard(); + test_richpresence(); + test_runtime(); + test_runtime_progress(); + + test_consoleinfo(); + test_rc_libretro(); + test_rc_validate(); + + test_lua(); + + test_url(); + + test_rapi_common(); + test_rapi_user(); + test_rapi_runtime(); + test_rapi_info(); + test_rapi_editor(); + + test_client(); + + test_cdreader(); + test_hash(); +#endif + + TEST_FRAMEWORK_SHUTDOWN(); + + return TEST_FRAMEWORK_PASSED() ? 0 : 1; +} diff --git a/src/rcheevos/test/test_framework.h b/src/rcheevos/test/test_framework.h new file mode 100644 index 000000000..f8b61d0a5 --- /dev/null +++ b/src/rcheevos/test/test_framework.h @@ -0,0 +1,204 @@ +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include +#include + +typedef struct +{ + const char* current_suite; + const char* current_test; + const char* current_test_file_stack[16]; + const char* current_test_func_stack[16]; + unsigned current_test_line_stack[16]; + unsigned current_test_stack_index; + int current_test_fail; + int fail_count; + int run_count; + time_t time_start; +} test_framework_state_t; + +extern test_framework_state_t __test_framework_state; +extern const char* test_framework_basename(const char* path); + +#define TEST_FRAMEWORK_DECLARATIONS() \ + test_framework_state_t __test_framework_state; \ + \ + const char* test_framework_basename(const char* path) { \ + const char* last_slash = path; \ + while (*path) { \ + if (*path == '/' || *path == '\\') last_slash = path + 1; \ + ++path; \ + } \ + return last_slash; \ + } + +#define TEST_FRAMEWORK_INIT() \ + memset(&__test_framework_state, 0, sizeof(__test_framework_state)); \ + __test_framework_state.time_start = time(0) + +#define TEST_FRAMEWORK_SHUTDOWN() \ + printf("\nDone. %d/%d passed in %u seconds\n", \ + __test_framework_state.run_count - __test_framework_state.fail_count, __test_framework_state.run_count, \ + (unsigned)(time(0) - __test_framework_state.time_start)) + +#define TEST_FRAMEWORK_PASSED() (__test_framework_state.fail_count == 0) + +#define TEST_SUITE_BEGIN() \ + __test_framework_state.current_suite = __func__; \ + printf("%s ", __test_framework_state.current_suite); \ + fflush(stdout) + +#define TEST_SUITE_END() __test_framework_state.current_suite = 0; \ + printf("\n"); \ + fflush(stdout) + +#define TEST_PUSH_CURRENT_LINE(func_name) \ + __test_framework_state.current_test_file_stack[__test_framework_state.current_test_stack_index] = __FILE__; \ + __test_framework_state.current_test_line_stack[__test_framework_state.current_test_stack_index] = __LINE__; \ + __test_framework_state.current_test_func_stack[__test_framework_state.current_test_stack_index] = func_name; \ + ++__test_framework_state.current_test_stack_index; + +#define TEST_POP_CURRENT_LINE() --__test_framework_state.current_test_stack_index; + +#define TEST_INIT() \ + __test_framework_state.current_test_stack_index = 0; \ + TEST_PUSH_CURRENT_LINE(__func__); \ + __test_framework_state.current_test_fail = 0; \ + ++__test_framework_state.run_count; \ + printf("."); \ + fflush(stdout); + +#define TEST(func) \ + __test_framework_state.current_test = #func; \ + TEST_INIT() \ + func(); + +#define TEST_PARAMS1(func, p1) \ + __test_framework_state.current_test = #func "(" #p1 ")"; \ + TEST_INIT() \ + func(p1); + +#define TEST_PARAMS2(func, p1, p2) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ")"; \ + TEST_INIT() \ + func(p1, p2); + +#define TEST_PARAMS3(func, p1, p2, p3) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ")"; \ + TEST_INIT() \ + func(p1, p2, p3); + +#define TEST_PARAMS4(func, p1, p2, p3, p4) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4); + +#define TEST_PARAMS5(func, p1, p2, p3, p4, p5) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5); + +#define TEST_PARAMS6(func, p1, p2, p3, p4, p5, p6) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6); + +#define TEST_PARAMS7(func, p1, p2, p3, p4, p5, p6, p7) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6, p7); + +#define TEST_PARAMS8(func, p1, p2, p3, p4, p5, p6, p7, p8) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ", " #p8 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6, p7, p8); + +#define TEST_PARAMS9(func, p1, p2, p3, p4, p5, p6, p7, p8, p9) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ", " #p8 ", " #p9 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6, p7, p8, p9); + +#define ASSERT_HELPER(func_call, func_name) { \ + TEST_PUSH_CURRENT_LINE(func_name); \ + func_call; \ + TEST_POP_CURRENT_LINE(); \ + if (__test_framework_state.current_test_fail) \ + return; \ +} + +#define ASSERT_MESSAGE(message, ...) { \ + unsigned __stack_index; \ + if (!__test_framework_state.current_test_fail) { \ + __test_framework_state.current_test_fail = 1; \ + ++__test_framework_state.fail_count; \ + fprintf(stderr, "\n* %s/%s (%s:%d)", __test_framework_state.current_suite, __test_framework_state.current_test, \ + test_framework_basename(__test_framework_state.current_test_file_stack[0]), __test_framework_state.current_test_line_stack[0]); \ + } \ + for (__stack_index = 1; __stack_index < __test_framework_state.current_test_stack_index; ++__stack_index) { \ + fprintf(stderr, "\n via %s (%s:%d)", \ + __test_framework_state.current_test_func_stack[__stack_index], \ + test_framework_basename(__test_framework_state.current_test_file_stack[__stack_index]), \ + __test_framework_state.current_test_line_stack[__stack_index]); \ + } \ + fprintf(stderr, "\n "); \ + fprintf(stderr, message "\n", ## __VA_ARGS__); \ + fflush(stderr); \ +} + +#define ASSERT_FAIL(message, ...) { ASSERT_MESSAGE(message, ## __VA_ARGS__); return; } + +#define ASSERT_COMPARE(value, compare, expected, type, format) { \ + type __v = (type)(value); \ + type __e = (type)(expected); \ + if (!(__v compare __e)) { \ + ASSERT_FAIL("Expected: " #value " " #compare " " #expected " (%s:%d)\n Found: " format " " #compare " " format, \ + test_framework_basename(__FILE__), __LINE__, __v, __e); \ + } \ +} + +#define ASSERT_NUM_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, int, "%d") +#define ASSERT_NUM_NOT_EQUALS(value, expected) ASSERT_COMPARE(value, !=, expected, int, "%d") +#define ASSERT_NUM_GREATER(value, expected) ASSERT_COMPARE(value, >, expected, int, "%d") +#define ASSERT_NUM_GREATER_EQUALS(value, expected) ASSERT_COMPARE(value, >=, expected, int, "%d") +#define ASSERT_NUM_LESS(value, expected) ASSERT_COMPARE(value, <, expected, int, "%d") +#define ASSERT_NUM_LESS_EQUALS(value, expected) ASSERT_COMPARE(value, <=, expected, int, "%d") + +#define ASSERT_FLOAT_EQUALS(value, expected) { \ + float __v = (float)value; \ + float __e = (float)expected; \ + double diff = (__v > __e) ? ((double)__v - (double)__e) : ((double)__e - (double)__v); \ + if (diff >= 0.0000002) { /* FLT_EPSILON is ~1.19e-7 */ \ + ASSERT_FAIL("Expected: " #value " = " #expected " (%s:%d)\n Found: %f = %f", \ + test_framework_basename(__FILE__), __LINE__, __v, __e); \ + } \ +} + +/* TODO: figure out some way to detect c89 so we can use int64_t and %lld on non-c89 builds */ +#define ASSERT_NUM64_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, int, "%d") + +#define ASSERT_TRUE(value) ASSERT_NUM_NOT_EQUALS(value, 0) +#define ASSERT_FALSE(value) ASSERT_NUM_EQUALS(value, 0) + +#define ASSERT_UNUM_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, unsigned, "%u") +#define ASSERT_DBL_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, double, "%g") +#define ASSERT_PTR_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, void*, "%p") +#define ASSERT_PTR_NOT_NULL(value) ASSERT_COMPARE(value, !=, NULL, void*, "%p") +#define ASSERT_PTR_NULL(value) ASSERT_COMPARE(value, ==, NULL, void*, "%p") + +#define ASSERT_STR_EQUALS(value, expected) { \ + const char* __v = (const char*)(value); \ + const char* __e = (const char*)(expected); \ + if (strcmp(__v, __e) != 0) { \ + ASSERT_FAIL( "String mismatch for: " #value " (%s:%d)\n Expected: %s\n Found: %s", test_framework_basename(__FILE__), __LINE__, __e, __v); \ + }} + +#define ASSERT_STR_NOT_EQUALS(value, expected) { \ + const char* __v = (const char*)(value); \ + const char* __e = (const char*)(expected); \ + if (strcmp(__v, __e) == 0) { \ + ASSERT_FAIL( "String match for: " #value " (%s:%d)\n Found, but not expected: %s", test_framework_basename(__FILE__), __LINE__, __v); \ + }} + +#endif /* TEST_FRAMEWORK_H */ diff --git a/src/rcheevos/validator/Makefile b/src/rcheevos/validator/Makefile new file mode 100644 index 000000000..c20bb9f52 --- /dev/null +++ b/src/rcheevos/validator/Makefile @@ -0,0 +1,31 @@ +RC_SRC=../src/rcheevos +RC_HASH_SRC=../src/rhash +RC_API_SRC=../src/rapi +LUA_SRC=../test/lua/src + +OBJ=$(RC_SRC)/trigger.o $(RC_SRC)/condset.o $(RC_SRC)/condition.o $(RC_SRC)/operand.o \ + $(RC_SRC)/value.o $(RC_SRC)/lboard.o $(RC_SRC)/richpresence.o $(RC_SRC)/runtime.o \ + $(RC_SRC)/alloc.o $(RC_SRC)/memref.o $(RC_SRC)/format.o $(RC_SRC)/compat.o \ + $(RC_SRC)/consoleinfo.o $(RC_SRC)/rc_validate.o \ + $(RC_HASH_SRC)/md5.o \ + $(RC_API_SRC)/rc_api_common.o $(RC_API_SRC)/rc_api_runtime.o \ + $(LUA_SRC)/lapi.o $(LUA_SRC)/lcode.o $(LUA_SRC)/lctype.o $(LUA_SRC)/ldebug.o \ + $(LUA_SRC)/ldo.o $(LUA_SRC)/ldump.o $(LUA_SRC)/lfunc.o $(LUA_SRC)/lgc.o $(LUA_SRC)/llex.o \ + $(LUA_SRC)/lmem.o $(LUA_SRC)/lobject.o $(LUA_SRC)/lopcodes.o $(LUA_SRC)/lparser.o \ + $(LUA_SRC)/lstate.o $(LUA_SRC)/lstring.o $(LUA_SRC)/ltable.o $(LUA_SRC)/ltm.o \ + $(LUA_SRC)/lundump.o $(LUA_SRC)/lvm.o $(LUA_SRC)/lzio.o $(LUA_SRC)/lauxlib.o \ + $(LUA_SRC)/lbaselib.o $(LUA_SRC)/lbitlib.o $(LUA_SRC)/lcorolib.o $(LUA_SRC)/ldblib.o \ + $(LUA_SRC)/liolib.o $(LUA_SRC)/lmathlib.o $(LUA_SRC)/loslib.o $(LUA_SRC)/lstrlib.o \ + $(LUA_SRC)/ltablib.o $(LUA_SRC)/lutf8lib.o $(LUA_SRC)/loadlib.o $(LUA_SRC)/linit.o \ + validator.o + +all: validator + +%.o: %.c + gcc -Wall -O0 -g -std=c89 -ansi -Wno-long-long -DLUA_32BITS -I../include -I$(RC_SRC) -I$(LUA_SRC) -c $< -o $@ + +validator: $(OBJ) + gcc -o $@ $+ -lm + +clean: + rm -f test $(OBJ) diff --git a/src/rcheevos/validator/validator.c b/src/rcheevos/validator/validator.c new file mode 100644 index 000000000..d286601ac --- /dev/null +++ b/src/rcheevos/validator/validator.c @@ -0,0 +1,654 @@ +#include "rc_internal.h" +#include "rc_url.h" +#include "rc_api_runtime.h" +#include "rc_consoles.h" +#include "rc_validate.h" + +#include +#include +#include +#include +#include /* memset */ +#include + +#ifdef _MSC_VER /* windows build */ + #define WIN32_LEAN_AND_MEAN + #include +#else + #include + #include + #define stricmp strcasecmp +#endif + +/* usage exmaple: + * + * ./validator.exe d "E:\RetroAchievements\dump" | sort > results.txt + * grep -v ": OK" results.txt | grep -v "File: " | grep . + */ + +static const char* flag_string(char type) { + switch (type) { + case RC_CONDITION_STANDARD: return ""; + case RC_CONDITION_PAUSE_IF: return "PauseIf "; + case RC_CONDITION_RESET_IF: return "ResetIf "; + case RC_CONDITION_MEASURED_IF: return "MeasuredIf "; + case RC_CONDITION_TRIGGER: return "Trigger "; + case RC_CONDITION_MEASURED: return "Measured "; + case RC_CONDITION_ADD_SOURCE: return "AddSource "; + case RC_CONDITION_SUB_SOURCE: return "SubSource "; + case RC_CONDITION_ADD_ADDRESS: return "AddAddress "; + case RC_CONDITION_ADD_HITS: return "AddHits "; + case RC_CONDITION_SUB_HITS: return "SubHits "; + case RC_CONDITION_RESET_NEXT_IF: return "ResetNextIf "; + case RC_CONDITION_AND_NEXT: return "AndNext "; + case RC_CONDITION_OR_NEXT: return "OrNext "; + default: return "Unknown"; + } +} + +static const char* type_string(char type) { + switch (type) { + case RC_OPERAND_ADDRESS: return "Mem"; + case RC_OPERAND_DELTA: return "Delta"; + case RC_OPERAND_CONST: return "Value"; + case RC_OPERAND_FP: return "Float"; + case RC_OPERAND_LUA: return "Lua"; + case RC_OPERAND_PRIOR: return "Prior"; + case RC_OPERAND_BCD: return "BCD"; + case RC_OPERAND_INVERTED: return "Inverted"; + default: return "Unknown"; + } +} + +static const char* size_string(char size) { + switch (size) { + case RC_MEMSIZE_8_BITS: return "8-bit"; + case RC_MEMSIZE_16_BITS: return "16-bit"; + case RC_MEMSIZE_24_BITS: return "24-bit"; + case RC_MEMSIZE_32_BITS: return "32-bit"; + case RC_MEMSIZE_LOW: return "Lower4"; + case RC_MEMSIZE_HIGH: return "Upper4"; + case RC_MEMSIZE_BIT_0: return "Bit0"; + case RC_MEMSIZE_BIT_1: return "Bit1"; + case RC_MEMSIZE_BIT_2: return "Bit2"; + case RC_MEMSIZE_BIT_3: return "Bit3"; + case RC_MEMSIZE_BIT_4: return "Bit4"; + case RC_MEMSIZE_BIT_5: return "Bit5"; + case RC_MEMSIZE_BIT_6: return "Bit6"; + case RC_MEMSIZE_BIT_7: return "Bit7"; + case RC_MEMSIZE_BITCOUNT: return "BitCount"; + case RC_MEMSIZE_16_BITS_BE: return "16-bit BE"; + case RC_MEMSIZE_24_BITS_BE: return "24-bit BE"; + case RC_MEMSIZE_32_BITS_BE: return "32-bit BE"; + case RC_MEMSIZE_VARIABLE: return "Variable"; + default: return "Unknown"; + } +} + +static const char* operator_string(char oper) { + switch (oper) { + case RC_OPERATOR_NONE: return ""; + case RC_OPERATOR_AND: return "&"; + case RC_OPERATOR_XOR: return "^"; + case RC_OPERATOR_MULT: return "*"; + case RC_OPERATOR_DIV: return "/"; + case RC_OPERATOR_EQ: return "="; + case RC_OPERATOR_NE: return "!="; + case RC_OPERATOR_GE: return ">="; + case RC_OPERATOR_GT: return ">"; + case RC_OPERATOR_LE: return "<="; + case RC_OPERATOR_LT: return "<"; + default: return "?"; + } +} + +static void append_condition(char result[], size_t result_size, const rc_condition_t* cond) { + const char* flag = flag_string(cond->type); + const char* src_type = type_string(cond->operand1.type); + const char* tgt_type = type_string(cond->operand2.type); + const char* cmp = operator_string(cond->oper); + char val1[32], val2[32]; + + if (rc_operand_is_memref(&cond->operand1)) + snprintf(val1, sizeof(val1), "%s 0x%06x", size_string(cond->operand1.size), cond->operand1.value.memref->address); + else + snprintf(val1, sizeof(val1), "0x%06x", cond->operand1.value.num); + + if (rc_operand_is_memref(&cond->operand2)) + snprintf(val2, sizeof(val2), "%s 0x%06x", size_string(cond->operand2.size), cond->operand2.value.memref->address); + else + snprintf(val2, sizeof(val2), "0x%06x", cond->operand2.value.num); + + const size_t message_len = strlen(result); + result += message_len; + result_size -= message_len; + + if (cond->oper == RC_OPERATOR_NONE) + snprintf(result, result_size, ": %s%s %s", flag, src_type, val1); + else + snprintf(result, result_size, ": %s%s %s %s %s %s", flag, src_type, val1, cmp, tgt_type, val2); +} + +static void append_invalid_condition(char result[], size_t result_size, const rc_condset_t* condset) { + if (strncmp(result, "Condition ", 10) == 0) { + int index = atoi(&result[10]); + const rc_condition_t* cond; + for (cond = condset->conditions; cond; cond = cond->next) { + if (--index == 0) { + const size_t error_length = strlen(result); + append_condition(result + error_length, result_size - error_length, cond); + return; + } + } + } +} + +static void append_invalid_trigger_condition(char result[], size_t result_size, const rc_trigger_t* trigger) { + if (strncmp(result, "Alt", 3) == 0) { + int index = atoi(&result[3]); + const rc_condset_t* condset; + for (condset = trigger->alternative; condset; condset = condset->next) { + if (--index == 0) { + result += 4; + result_size -= 4; + while (isdigit(*result)) { + ++result; + --result_size; + } + ++result; + --result_size; + append_invalid_condition(result, result_size, condset); + return; + } + } + } + else if (strncmp(result, "Core ", 5) == 0) { + append_invalid_condition(result + 5, result_size - 5, trigger->requirement); + } + else { + append_invalid_condition(result, result_size, trigger->requirement); + } +} + +static int validate_trigger(const char* trigger, char result[], size_t result_size, int console_id) { + char* buffer; + rc_trigger_t* compiled; + int success = 0; + + int ret = rc_trigger_size(trigger); + if (ret < 0) { + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_trigger(buffer, trigger, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { + snprintf(result, result_size, "%d OK", ret); + success = 1; + } + else { + append_invalid_trigger_condition(result, result_size, compiled); + } + + free(buffer); + return success; +} + +static int validate_leaderboard(const char* leaderboard, char result[], const size_t result_size, int console_id) +{ + char* buffer; + rc_lboard_t* compiled; + int success = 0; + + int ret = rc_lboard_size(leaderboard); + if (ret < 0) { + /* generic problem parsing the leaderboard, attempt to report where */ + const char* start = leaderboard; + char part[4] = { 0,0,0,0 }; + do { + char* next = strstr(start, "::"); + part[0] = toupper((int)start[0]); + part[1] = toupper((int)start[1]); + part[2] = toupper((int)start[2]); + start += 4; + + if (strcmp(part, "VAL") == 0) { + int ret2 = rc_value_size(start); + if (ret2 == ret) { + snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); + return 0; + } + } + else { + int ret2 = rc_trigger_size(start); + if (ret2 == ret) { + snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); + return 0; + } + } + + if (!next) + break; + + start = next + 2; + } while (1); + + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_lboard(buffer, leaderboard, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else { + snprintf(result, result_size, "STA: "); + success = rc_validate_trigger_for_console(&compiled->start, result + 5, result_size - 5, console_id); + if (!success) { + append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->start); + } + else { + snprintf(result, result_size, "SUB: "); + success = rc_validate_trigger_for_console(&compiled->submit, result + 5, result_size - 5, console_id); + if (!success) { + append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->submit); + } + else { + snprintf(result, result_size, "CAN: "); + success = rc_validate_trigger_for_console(&compiled->cancel, result + 5, result_size - 5, console_id); + + if (!success) { + append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->cancel); + } + else { + snprintf(result, result_size, "%d OK", ret); + } + } + } + } + + free(buffer); + return success; +} + +static int validate_macros(const rc_richpresence_t* richpresence, const char* script, char result[], const size_t result_size) +{ + const unsigned short RC_FORMAT_UNKNOWN_MACRO = 103; /* enum not exposed by header */ + + rc_richpresence_display_t* display = richpresence->first_display; + while (display != NULL) { + rc_richpresence_display_part_t* part = display->display; + while (part != NULL) { + if (part->display_type == RC_FORMAT_UNKNOWN_MACRO) { + /* include opening parenthesis to prevent partial match */ + size_t macro_len = strchr(part->text, '(') - part->text + 1; + + /* find the display portion of the script */ + const char* ptr = script; + int line = 1; + while (strncmp(ptr, "Display:", 8) != 0) { + while (*ptr != '\n') + ++ptr; + + ++line; + ++ptr; + } + + /* find the first matching reference to the unknown macro */ + do { + while (*ptr != '@') { + if (*ptr == '\n') + ++line; + + if (*ptr == '\0') { + /* unexpected, but prevent potential infinite loop */ + snprintf(result, result_size, "Unknown macro \"%.*s\"", (int)(macro_len - 1), part->text); + return 0; + } + + ++ptr; + } + ++ptr; + + if (strncmp(ptr, part->text, macro_len) == 0) { + snprintf(result, result_size, "Line %d: Unknown macro \"%.*s\"", line, (int)(macro_len - 1), part->text); + return 0; + } + } while (1); + } + + part = part->next; + } + + display = display->next; + } + + return 1; +} + +static int validate_richpresence(const char* script, char result[], const size_t result_size, int console_id) +{ + char* buffer; + rc_richpresence_t* compiled; + int lines; + int success = 0; + + int ret = rc_richpresence_size_lines(script, &lines); + if (ret < 0) { + snprintf(result, result_size, "Line %d: %s", lines, rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_richpresence(buffer, script, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else { + const rc_richpresence_display_t* display; + int index = 1; + for (display = compiled->first_display; display; display = display->next) { + const size_t prefix_length = snprintf(result, result_size, "Display%d: ", index++); + success = rc_validate_trigger_for_console(&display->trigger, result + prefix_length, result_size - prefix_length, console_id); + if (!success) + break; + } + + if (success) + success = rc_validate_memrefs_for_console(compiled->memrefs, result, result_size, console_id); + if (success) + success = validate_macros(compiled, script, result, result_size); + if (success) + snprintf(result, result_size, "%d OK", ret); + } + + free(buffer); + return success; +} + +static void validate_richpresence_file(const char* richpresence_file, char result[], const size_t result_size) +{ + char* file_contents; + size_t file_size; + FILE* file; + + fopen_s(&file, richpresence_file, "rb"); + if (!file) { + snprintf(result, result_size, "could not open file"); + return; + } + + fseek(file, 0, SEEK_END); + file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + file_contents = (char*)malloc(file_size + 1); + if (!file_contents) { + snprintf(result, result_size, "malloc failed"); + return; + } + + fread(file_contents, 1, file_size, file); + file_contents[file_size] = '\0'; + fclose(file); + + validate_richpresence(file_contents, result, result_size, 0); + + free(file_contents); +} + +static int validate_patchdata_file(const char* patchdata_file, const char* filename, int errors_only) { + char* file_contents; + size_t file_size; + FILE* file; + rc_api_fetch_game_data_response_t fetch_game_data_response; + int result; + size_t i; + char file_title[256]; + char buffer[256]; + int success = 1; + + fopen_s(&file, patchdata_file, "rb"); + if (!file) { + printf("File: %s: could not open file\n", filename); + return 0; + } + + fseek(file, 0, SEEK_END); + file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + file_contents = (char*)malloc(file_size + 1); + fread(file_contents, 1, file_size, file); + file_contents[file_size] = '\0'; + fclose(file); + + /* rc_api_process_fetch_game_data_response expects the PatchData to be + * a subobject, but the DLL strips that when writing to the RACache. + * if it looks like the nested object, wrap it again */ + if (strncmp(file_contents, "{\"ID\":", 6) == 0) { + char* expanded_contents = (char*)malloc(file_size + 15); + memcpy(expanded_contents, "{\"PatchData\":", 13); + memcpy(&expanded_contents[13], file_contents, file_size); + expanded_contents[file_size + 13] = '}'; + expanded_contents[file_size + 14] = '\0'; + + free(file_contents); + file_contents = expanded_contents; + } + + result = rc_api_process_fetch_game_data_response(&fetch_game_data_response, file_contents); + if (result != RC_OK) { + if (fetch_game_data_response.response.error_message) + printf("File: %s: %s\n", filename, fetch_game_data_response.response.error_message); + else + printf("File: %s: %s\n", filename, rc_error_str(result)); + return 0; + } + + free(file_contents); + + snprintf(file_title, sizeof(file_title), "File: %s: %s\n", filename, fetch_game_data_response.title); + + if (fetch_game_data_response.rich_presence_script && *fetch_game_data_response.rich_presence_script) { + result = validate_richpresence(fetch_game_data_response.rich_presence_script, + buffer, sizeof(buffer), fetch_game_data_response.console_id); + success &= result; + + if (!result || !errors_only) { + printf("%s", file_title); + file_title[0] = '\0'; + + printf(" rich presence %d: %s\n", fetch_game_data_response.id, buffer); + } + } + + for (i = 0; i < fetch_game_data_response.num_achievements; ++i) { + const char* trigger = fetch_game_data_response.achievements[i].definition; + result = validate_trigger(trigger, buffer, sizeof(buffer), fetch_game_data_response.console_id); + success &= result; + + if (!result || !errors_only) { + if (file_title[0]) { + printf("%s", file_title); + file_title[0] = '\0'; + } + + printf(" achievement %d%s: %s\n", fetch_game_data_response.achievements[i].id, + (fetch_game_data_response.achievements[i].category == 3) ? "" : " (Unofficial)", buffer); + } + } + + for (i = 0; i < fetch_game_data_response.num_leaderboards; ++i) { + result = validate_leaderboard(fetch_game_data_response.leaderboards[i].definition, + buffer, sizeof(buffer), fetch_game_data_response.console_id); + success &= result; + + if (!result || !errors_only) { + if (file_title[0]) { + printf("%s", file_title); + file_title[0] = '\0'; + } + + printf(" leaderboard %d: %s\n", fetch_game_data_response.leaderboards[i].id, buffer); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); + + return success; +} + +#ifdef _MSC_VER +static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { + WIN32_FIND_DATA fdFile; + HANDLE hFind = NULL; + int need_newline = 0; + + char filename[MAX_PATH]; + snprintf(filename, sizeof(filename), "%s\\*.json", patchdata_directory); + + if ((hFind = FindFirstFile(filename, &fdFile)) == INVALID_HANDLE_VALUE) { + printf("failed to open directory"); + return; + } + + do + { + if (!(fdFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + if (need_newline) { + printf("\n"); + need_newline = 0; + } + + snprintf(filename, sizeof(filename), "%s\\%s", patchdata_directory, fdFile.cFileName); + if (!validate_patchdata_file(filename, fdFile.cFileName, errors_only) || !errors_only) + need_newline = 1; + } + } while(FindNextFile(hFind, &fdFile)); + + FindClose(hFind); +} +#else +static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { + struct dirent* entry; + char* filename; + size_t filename_len; + char path[2048]; + int need_newline = 0; + + DIR* dir = opendir(patchdata_directory); + if (!dir) { + printf("failed to open directory"); + return; + } + + while ((entry = readdir(dir)) != NULL) { + filename = entry->d_name; + filename_len = strlen(filename); + if (filename_len > 5 && stricmp(&filename[filename_len - 5], ".json") == 0) { + if (need_newline) { + printf("\n"); + need_newline = 0; + } + + sprintf(path, "%s/%s", patchdata_directory, filename); + if (!validate_patchdata_file(path, filename, errors_only) || !errors_only) + need_newline = 1; + } + } + + closedir(dir); +} +#endif + +static int usage() { + printf("validator [type] [data]\n" + "\n" + "where [type] is one of the following:\n" + " a achievement, [data] = trigger definition\n" + " l leaderboard, [data] = leaderboard definition\n" + " r rich presence, [data] = path to rich presence script\n" + " f patchdata file, [data] = path to patchdata json file\n" + " d patchdata directory, [data] = path to directory containing one or more patchdata json files\n" + " e same as 'd', but only reports errors\n" + ); + + return 0; +} + +int main(int argc, char* argv[]) { + char buffer[256]; + + if (argc < 3) + return usage(); + + switch (argv[1][0]) + { + case 'a': + validate_trigger(argv[2], buffer, sizeof(buffer), 0); + printf("Achievement: %s\n", buffer); + break; + + case 'l': + validate_leaderboard(argv[2], buffer, sizeof(buffer), 0); + printf("Leaderboard: %s\n", buffer); + break; + + case 'r': + validate_richpresence_file(argv[2], buffer, sizeof(buffer)); + printf("Rich Presence: %s\n", buffer); + break; + + case 'f': + validate_patchdata_file(argv[2], argv[2], 0); + break; + + case 'd': + printf("Directory: %s:\n", argv[2]); + validate_patchdata_directory(argv[2], 0); + break; + + case 'e': + printf("Directory: %s:\n", argv[2]); + validate_patchdata_directory(argv[2], 1); + break; + + default: + return usage(); + } + + return 0; +} diff --git a/src/rcheevos/validator/validator.vcxproj b/src/rcheevos/validator/validator.vcxproj new file mode 100644 index 000000000..eca6319f3 --- /dev/null +++ b/src/rcheevos/validator/validator.vcxproj @@ -0,0 +1,184 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613} + rcheevostest + Application + MultiByte + v143 + + + v142 + + + v141 + + + + true + + + false + true + + + true + + + false + true + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)..\test\lua\src + + + Console + + + + + Level3 + Disabled + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)..\test\lua\src + + + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)..\test\lua\src + + + true + true + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;$(ProjectDir)..\test\lua\src + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/rcheevos/validator/validator.vcxproj.filters b/src/rcheevos/validator/validator.vcxproj.filters new file mode 100644 index 000000000..b1f53be08 --- /dev/null +++ b/src/rcheevos/validator/validator.vcxproj.filters @@ -0,0 +1,181 @@ + + + + + {125bb837-6e5b-4a66-a607-e2250b31f108} + + + {36DC4CE2-0F57-46DE-A037-25E4272A7747} + + + {DD82F9EB-1066-40C6-9568-D5C7EEB2A49B} + + + {efdc689a-471b-432e-8ecb-dde3a3202949} + + + {8570c19e-e882-409f-84d8-b87887371c26} + + + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\lua + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + + src\rhash + + + src\rcheevos + + + src\rapi + + + src\rapi + + + src\rcheevos + + + src\rcheevos + + + + + src\rcheevos + + + src\rcheevos + + + \ No newline at end of file diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp new file mode 100644 index 000000000..21656ad83 --- /dev/null +++ b/src/retro_achievements.cpp @@ -0,0 +1,179 @@ +extern "C" { + #include "retro_achievements.h" +} +#include "rcheevos/include/rc_client.h" +#include "https.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#define STBI_ONLY_PNG +#include "stb_image.h" + +rc_client_t* ra_client = NULL; + +struct Image +{ + int width = 0, height = 0; + std::vector pixel_data; +}; + +std::unordered_map image_cache; +std::mutex image_cache_mutex; +std::vector> pending_callbacks; + +static void server_callback(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + std::string url = request->url; + std::string content_type = request->content_type; + std::string post_data = request->post_data; + http_request_e type; + + if (post_data.empty()) + { + type = http_request_e::GET; + } else { + type = http_request_e::POST; + } + +#ifndef EMSCRIPTEN +std::thread thread([type, url, post_data, callback, callback_data](){ +#endif + https_request(type, url, post_data, { {"User-Agent", "SkyEmu/4.0"} }, [callback, callback_data](const std::vector& result) { + if (result.empty()) + { + printf("[rcheevos]: empty response\n"); + return; + } + + rc_api_server_response_t response; + response.body = (const char*)result.data(); + response.body_length = result.size(); + response.http_status_code = 200; + callback(&response, callback_data); + }); +#ifndef EMSCRIPTEN +}); +thread.detach(); +#endif +} + +static void log_message(const char* message, const rc_client_t* client) +{ + printf("[rcheevos]: %s\n", message); +} + +void ra_initialize_client(rc_client_read_memory_func_t memory_read_func) +{ + if(ra_client) + { + printf("[rcheevos]: client already initialized!\n"); + } + else + { + ra_client = rc_client_create(memory_read_func, server_callback); + // RetroAchievements doesn't enable CORS, so we use a reverse proxy + rc_api_set_host("https://api.achieve.skyemoo.pandasemi.co"); + rc_api_set_image_host("https://media.retroachievements.org"); + #ifndef NDEBUG + rc_client_enable_logging(ra_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); + #endif + // TODO: should probably be an option after we're finished testing + rc_client_set_hardcore_enabled(ra_client, 0); + } +} + +void ra_shutdown_client() +{ + if(ra_client) + { + rc_client_destroy(ra_client); + ra_client = nullptr; + } +} + +void ra_login_credentials(const char* username, const char* password, rc_client_callback_t login_callback) +{ + rc_client_begin_login_with_password(ra_client, username, password, login_callback, NULL); +} + +void ra_login_token(const char* username, const char* token, rc_client_callback_t login_callback) +{ + std::string username_str = username; + std::string token_str = token; + std::thread login_thread([=](){ + rc_client_begin_login_with_token(ra_client, username_str.c_str(), token_str.c_str(), login_callback, NULL); + }); + login_thread.detach(); +} + +void ra_load_game(const uint8_t *rom, size_t rom_size, int console_id, rc_client_callback_t callback) +{ + // Make a copy of the ROM as the original may be destroyed before the thread finishes + std::vector rom_copy(rom, rom + rom_size); + std::thread load_game_thread([rom_copy](int console_id, rc_client_callback_t callback){ + rc_client_begin_identify_and_load_game(ra_client, console_id, + NULL, rom_copy.data(), rom_copy.size(), callback, NULL); + }, console_id, callback); + load_game_thread.detach(); +} + +void ra_get_image(const char* url, get_image_callback_t callback, void* user_data) +{ + std::lock_guard lock(image_cache_mutex); + if (image_cache.find(url) != image_cache.end()) + { + auto& image = image_cache[url]; + pending_callbacks.push_back([callback, user_data, &image](){ + callback(image.pixel_data.data(), image.pixel_data.size(), image.width, image.height, user_data); + }); + return; + } + + std::string url_str = url; + #ifndef EMSCRIPTEN + std::thread thread([url_str, callback, user_data](){ + #endif + https_request(http_request_e::GET, url_str, {}, {}, [callback, user_data, url_str](const std::vector& result) { + std::lock_guard lock(image_cache_mutex); + rc_api_server_response_t response; + response.body = (const char*)result.data(); + response.body_length = result.size(); + response.http_status_code = 200; + auto& image = image_cache[url_str]; + uint8_t* pixel_data = stbi_load_from_memory((const uint8_t*)response.body, response.body_length, &image.width, &image.height, NULL, 4); + image.pixel_data.resize(image.width * image.height * 4); + memcpy(image.pixel_data.data(), pixel_data, image.pixel_data.size()); + stbi_image_free(pixel_data); + pending_callbacks.push_back([callback, user_data, &image](){ + callback(image.pixel_data.data(), image.pixel_data.size(), image.width, image.height, user_data); + }); + }); + #ifndef EMSCRIPTEN + }); + thread.detach(); + #endif +} + +void ra_run_pending_callbacks() +{ + std::lock_guard lock(image_cache_mutex); + if(pending_callbacks.empty()) + return; + + for (auto& callback : pending_callbacks) + { + callback(); + } + pending_callbacks.clear(); +} + +rc_client_t* ra_get_client() +{ + return ra_client; +} \ No newline at end of file diff --git a/src/retro_achievements.h b/src/retro_achievements.h new file mode 100644 index 000000000..2fecab155 --- /dev/null +++ b/src/retro_achievements.h @@ -0,0 +1,23 @@ +#ifndef RETRO_ACHIEVEMENTS +#define RETRO_ACHIEVEMENTS +#include +#include +#include + +struct rc_client_t; +typedef struct rc_client_t rc_client_t; + +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); +typedef void(*get_image_callback_t)(const uint8_t* buffer, size_t buffer_size, int width, int height, void* userdata); + +void ra_initialize_client(rc_client_read_memory_func_t memory_read_func); +void ra_login_credentials(const char* username, const char* password, rc_client_callback_t login_callback); +void ra_login_token(const char* username, const char* token, rc_client_callback_t login_callback); +void ra_poll_requests(); +void ra_shutdown_client(); +void ra_load_game(const uint8_t* rom, size_t rom_size, int console_id, rc_client_callback_t callback); +void ra_get_image(const char* url, get_image_callback_t callback, void* userdata); +void ra_run_pending_callbacks(); +rc_client_t* ra_get_client(); +#endif \ No newline at end of file