diff --git a/.gitignore b/.gitignore index 0c5d9a3e05c2..d303bffb741a 100644 --- a/.gitignore +++ b/.gitignore @@ -154,6 +154,7 @@ wiiu/wut/elf2rpl/elf2rpl /media/shaders_cg/ /media/libretrodb/ +compile_commands.json pkg/apple/iOS/build/ pkg/apple/build/ ui/drivers/qt/moc_* diff --git a/Makefile.common b/Makefile.common index 2587d2c28420..2165cd4ed996 100644 --- a/Makefile.common +++ b/Makefile.common @@ -1050,6 +1050,10 @@ ifeq ($(HAVE_LAKKA_NIGHTLY), 1) DEFINES += -DHAVE_LAKKA_NIGHTLY endif +ifneq ($(HAVE_LAKKA_CANARY), "") + DEFINES += -DHAVE_LAKKA_CANARY=\"${HAVE_LAKKA_CANARY}\" +endif + ifeq ($(HAVE_MENU_COMMON), 1) OBJ += menu/menu_setting.o \ menu/menu_driver.o \ @@ -2097,12 +2101,24 @@ ifeq ($(HAVE_NETWORKING), 1) DEFINES += -DHAVE_CHEEVOS INCLUDE_DIRS += -Ideps/rcheevos/include + ifneq ($(HAVE_THREADS), 1) + DEFINES += -DRC_NO_THREADS + else ifneq (,$(filter GEKKO,$(CFLAGS))) + # Gekko (Wii) and 3DS use custom pthread wrappers (see rthreads.c) + DEFINES += -DRC_NO_THREADS + else ifneq (,$(filter _3DS,$(CFLAGS))) + DEFINES += -DRC_NO_THREADS + endif + OBJ += cheevos/cheevos.o \ cheevos/cheevos_client.o \ cheevos/cheevos_menu.o \ $(LIBRETRO_COMM_DIR)/formats/cdfs/cdfs.o \ + deps/rcheevos/src/rc_client.o \ + deps/rcheevos/src/rc_compat.o \ + deps/rcheevos/src/rc_libretro.o \ + deps/rcheevos/src/rc_util.o \ deps/rcheevos/src/rcheevos/alloc.o \ - deps/rcheevos/src/rcheevos/compat.o \ deps/rcheevos/src/rcheevos/condition.o \ deps/rcheevos/src/rcheevos/condset.o \ deps/rcheevos/src/rcheevos/consoleinfo.o \ @@ -2110,7 +2126,6 @@ ifeq ($(HAVE_NETWORKING), 1) deps/rcheevos/src/rcheevos/lboard.o \ deps/rcheevos/src/rcheevos/memref.o \ deps/rcheevos/src/rcheevos/operand.o \ - deps/rcheevos/src/rcheevos/rc_libretro.o \ deps/rcheevos/src/rcheevos/richpresence.o \ deps/rcheevos/src/rcheevos/runtime.o \ deps/rcheevos/src/rcheevos/runtime_progress.o \ @@ -2119,6 +2134,7 @@ ifeq ($(HAVE_NETWORKING), 1) deps/rcheevos/src/rhash/cdreader.o \ deps/rcheevos/src/rhash/hash.o \ deps/rcheevos/src/rapi/rc_api_common.o \ + deps/rcheevos/src/rapi/rc_api_info.o \ deps/rcheevos/src/rapi/rc_api_runtime.o \ deps/rcheevos/src/rapi/rc_api_user.o \ diff --git a/Makefile.emscripten b/Makefile.emscripten index fa5d5b88e703..af1c122f7add 100644 --- a/Makefile.emscripten +++ b/Makefile.emscripten @@ -2,6 +2,7 @@ HAVE_STATIC_DUMMY ?= 0 ifeq ($(TARGET),) ifeq ($(LIBRETRO),) TARGET := retroarch.js +LIBRETRO = dummy else TARGET := $(LIBRETRO)_libretro.js endif @@ -48,7 +49,6 @@ HAVE_7ZIP = 1 HAVE_BSV_MOVIE = 1 HAVE_AL = 1 - # WARNING -- READ BEFORE ENABLING # The rwebaudio driver is known to have several audio bugs, such as # minor crackling, or the entire page freezing/crashing. @@ -78,8 +78,11 @@ OBJDIR := obj-emscripten #if you compile with SDL2 flag add this Emscripten flag "-s USE_SDL=2" to LDFLAGS: LIBS := -s USE_ZLIB=1 -LDFLAGS := -L. --no-heap-copy -s $(LIBS) -s TOTAL_MEMORY=$(MEMORY) -s NO_EXIT_RUNTIME=0 -s FULL_ES2=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']" \ - -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS="['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_load_state', '_cmd_take_screenshot']" \ +LDFLAGS := -L. --no-heap-copy -s $(LIBS) -s TOTAL_MEMORY=$(MEMORY) -s NO_EXIT_RUNTIME=0 -s FULL_ES2=1 \ + -s "EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH', 'ERRNO_CODES']" \ + -s ALLOW_MEMORY_GROWTH=1 -s "EXPORTED_FUNCTIONS=['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_load_state', '_cmd_take_screenshot']" \ + -s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORT_NAME="$(LIBRETRO)" \ + -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 \ --js-library emscripten/library_errno_codes.js \ --js-library emscripten/library_rwebcam.js @@ -102,7 +105,10 @@ else endif ifeq ($(ASYNC), 1) - LDFLAGS += -s ASYNCIFY=$(ASYNC) + LDFLAGS += -s ASYNCIFY=$(ASYNC) -s ASYNCIFY_STACK_SIZE=8192 + ifeq ($(DEBUG), 1) + LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE + endif endif ifeq ($(HAVE_SDL2), 1) @@ -127,8 +133,8 @@ ifneq ($(V), 1) endif ifeq ($(DEBUG), 1) - LDFLAGS += -O0 -g - CFLAGS += -O0 -g + LDFLAGS += -O0 -g -gsource-map -s SAFE_HEAP=1 -s STACK_OVERFLOW_CHECK=2 -s ASSERTIONS=1 + CFLAGS += -O0 -g -gsource-map -s SAFE_HEAP=1 -s SAFE_HEAP_LOG=1 -s STACK_OVERFLOW_CHECK=2 -s ASSERTIONS=1 else LDFLAGS += -O3 -s WASM=1 # WARNING: some optimizations can break some cores (ex: LTO breaks tyrquake) @@ -139,7 +145,12 @@ else CFLAGS += -O3 endif -CFLAGS += -Wall -I. -Ilibretro-common/include -std=gnu99 $(LIBS) #\ +# 128 * 1024, double the usual emscripten stack size +LDFLAGS += -s STACK_SIZE=131072 + +LDFLAGS += --extern-pre-js emscripten/pre.js + +CFLAGS += -Wall -I. -Ilibretro-common/include -std=gnu99 #\ # -s EXPORTED_FUNCTIONS="['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_take_screenshot']" RARCH_OBJ := $(addprefix $(OBJDIR)/,$(OBJ)) diff --git a/accessibility.h b/accessibility.h index df965df2bf50..f52e6f4676e7 100644 --- a/accessibility.h +++ b/accessibility.h @@ -31,11 +31,36 @@ #endif #include "configuration.h" +#include "tasks/tasks_internal.h" + +#ifdef HAVE_THREADS +#include "rthreads/rthreads.h" +#endif typedef struct { + /* The last request task, used to prepare and send the translation */ + retro_task_t *request_task; + + /* The last response task, used to parse costly translation data */ + retro_task_t *response_task; + + /* Timestamp of the last translation request */ + retro_time_t last_call; + + #ifdef HAVE_THREADS + /* Necessary because last_image is manipulated by task handlers */ + slock_t *image_lock; + #endif + + /* Frame captured during the last call to the translation service */ + uint8_t *last_image; + int last_image_size; + + /* 1 if the automatic mode has been enabled, 0 otherwise */ int ai_service_auto; - /* Is text-to-speech accessibility turned on? */ + + /* Text-to-speech narrator override flag */ bool enabled; } access_state_t; @@ -46,42 +71,73 @@ bool is_narrator_running(bool accessibility_enable); #endif /* - This function does all the stuff needed to translate the game screen, - using the URL given in the settings. Once the image from the frame - buffer is sent to the server, the callback will write the translated - image to the screen. - - Supported client/services (thus far) - -VGTranslate client ( www.gitlab.com/spherebeaker/vg_translate ) - -Ztranslate client/service ( www.ztranslate.net/docs/service ) - - To use a client, download the relevant code/release, configure - them, and run them on your local machine, or network. Set the - retroarch configuration to point to your local client (usually - listening on localhost:4404 ) and enable translation service. - - If you don't want to run a client, you can also use a service, - which is basically like someone running a client for you. The - downside here is that your retroarch device will have to have - an internet connection, and you may have to sign up for it. - - To make your own server, it must listen for a POST request, which - will consist of a JSON body, with the "image" field as a base64 - encoded string of a 24bit-BMP/PNG that the will be translated. - The server must output the translated image in the form of a - JSON body, with the "image" field also as a base64 encoded - 24bit-BMP, or as an alpha channel png. - - "paused" boolean is passed in to indicate if the current call - was made during a paused frame. Due to how the menu widgets work, - if the ai service is called in "auto" mode, then this call will - be made while the menu widgets unpause the core for a frame to update - the on-screen widgets. To tell the ai service what the pause - mode is honestly, we store the runloop_paused variable from before - the handle_translation_cb wipes the widgets, and pass that in here. + Invoke this method to send a request to the AI service. + It makes the following POST request using URL params: + – source_lang (optional): language code of the content currently running. + – target_lang (optional): language of the content to return. + – output: comma-separated list of formats that must be provided by the + service. Also lists supported sub-formats. + + The currently supported formats are: + – sound: raw audio to playback. (wav) + – text: text to be read through internal text-to-speech capabilities. + 'subs' can be specified on top of that to explain that we are looking + for short text response in the manner of subtitles. + – image: image to display on top of the video feed. Widgets will be used + first if possible, otherwise we'll try to draw it directly on the + video buffer. (bmp, png, png-a) [All in 24-bits BGR formats] + + In addition, the request contains a JSON payload, formatted as such: + – image: captured frame from the currently running content (in base64). + – format: format of the captured frame ("png", or "bmp"). + – coords: array describing the coordinates of the image within the + viewport space (x, y, width, height). + – viewport: array describing the size of the viewport (width, height). + – label: a text string describing the content (__). + – state: a JSON object describing the state of the frontend, containing: + – paused: 1 if the content has been paused, 0 otherwise. + – : the name of a retropad input, valued 1 if pressed. + (a, b, x, y, l, r, l2, r2, l3, r3) + (up, down, left, right, start, select) + + The translation component then expects a response from the AI service in the + form of a JSON payload, formatted as such: + – image: base64 representation of an image in a supported format. + – sound: base64 representation of a sound byte in a supported format. + – text: results from the service as a string. + – text_position: hint for the position of the text when the service is + running in text mode (ie subtitles). Position is a number, + 1 for Bottom or 2 for Top (defaults to bottom). + – press: a list of retropad input to forcibly press. On top of the + expected keys (cf. 'state' above) values 'pause' and 'unpause' can be + specified to control the flow of the content. + – error: any error encountered with the request. + – auto: either 'auto' or 'continue' to control automatic requests. + + All fields are optional, but at least one of them must be present. + If 'error' is set, the error is shown to the user and everything else is + ignored, even 'auto' settings. + + With 'auto' on 'auto', RetroArch will automatically send a new request + (with a minimum delay enforced by uints.ai_service_poll_delay), with a value + of 'continue', RetroArch will ignore the returned content and skip to the + next automatic request. This allows the service to specify that the returned + content is the same as the one previously sent, so RetroArch does not need to + update its display unless necessary. With 'continue' the service *must* + still send the content, as we may need to display it if the user paused the + AI service for instance. + + {paused} boolean is passed in to indicate if the current call was made + during a paused frame. Due to how the menu widgets work, if the AI service + is called in 'auto' mode, then this call will be made while the menu widgets + unpause the core for a frame to update the on-screen widgets. To tell the AI + service what the pause mode is honestly, we store the runloop_paused + variable from before the service wipes the widgets, and pass that in here. */ bool run_translation_service(settings_t *settings, bool paused); +void translation_release(bool inform); + bool accessibility_speak_priority( bool accessibility_enable, unsigned accessibility_narrator_speech_speed, diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index db08fb874672..4ab541553d76 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -75,7 +75,7 @@ #include "../deps/rcheevos/include/rc_runtime.h" #include "../deps/rcheevos/include/rc_runtime_types.h" #include "../deps/rcheevos/include/rc_hash.h" -#include "../deps/rcheevos/src/rcheevos/rc_libretro.h" +#include "../deps/rcheevos/src/rc_libretro.h" /* Define this macro to prevent cheevos from being deactivated when they trigger. */ #undef CHEEVOS_DONT_DEACTIVATE @@ -163,7 +163,7 @@ static void rcheevos_handle_log_message(const char* message) CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", message); } -static void rcheevos_get_core_memory_info(unsigned id, +static void rcheevos_get_core_memory_info(uint32_t id, rc_libretro_core_memory_info_t* info) { retro_ctx_memory_info_t ctx_info; @@ -220,10 +220,10 @@ uint8_t* rcheevos_patch_address(unsigned address) return rc_libretro_memory_find(&rcheevos_locals.memory, address); } -static unsigned rcheevos_peek(unsigned address, - unsigned num_bytes, void* ud) +static uint32_t rcheevos_peek(uint32_t address, + uint32_t num_bytes, void* ud) { - unsigned avail; + uint32_t avail; uint8_t* data = rc_libretro_memory_find_avail( &rcheevos_locals.memory, address, &avail); @@ -1321,7 +1321,7 @@ static void rcheevos_runtime_event_handler( } } -static int rcheevos_runtime_address_validator(unsigned address) +static int rcheevos_runtime_address_validator(uint32_t address) { return rc_libretro_memory_find( &rcheevos_locals.memory, address) != NULL; @@ -2066,7 +2066,7 @@ static void rcheevos_identify_game_callback(void* userdata) rcheevos_fetch_game_data(); } -static int rcheevos_get_image_path(unsigned index, char* buffer, size_t buffer_size) +static int rcheevos_get_image_path(uint32_t index, char* buffer, size_t buffer_size) { rarch_system_info_t *sys_info = &runloop_state_get_ptr()->system; if (!sys_info->disk_control.cb.get_image_path) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 576979403b7e..d02d0f1dca7f 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -1756,7 +1756,7 @@ static void rcheevos_async_award_achievement_callback( { if ((int)api_response.awarded_achievement_id != request->id) snprintf(buffer, buffer_size, "Achievement %u awarded instead", - api_response.awarded_achievement_id); + (unsigned)api_response.awarded_achievement_id); else if (api_response.response.error_message) { /* previously unlocked achievements are returned as a "successful" error */ diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 3ca72ef073f6..97cefb7f75c4 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -18,7 +18,7 @@ #define __RARCH_CHEEVOS_LOCALS_H #include "../deps/rcheevos/include/rc_runtime.h" -#include "../deps/rcheevos/src/rcheevos/rc_libretro.h" +#include "../deps/rcheevos/src/rc_libretro.h" #include #include diff --git a/command.c b/command.c index 5c07767f7171..f95084407cc1 100644 --- a/command.c +++ b/command.c @@ -18,6 +18,7 @@ #include #include #include +#include #ifdef HAVE_NETWORKING #include #include @@ -1166,6 +1167,10 @@ bool command_event_save_config( const char *str = path_exists ? config_path : path_get(RARCH_PATH_CONFIG); + /* Workaround for libdecor 0.2.0 setting unwanted locale */ +#if defined(HAVE_WAYLAND) && defined(HAVE_DYNAMIC) + setlocale(LC_NUMERIC,"C"); +#endif if (path_exists && config_save_file(config_path)) { snprintf(s, len, "%s \"%s\".", diff --git a/config.def.h b/config.def.h index 2d87abc07092..a59e56441b15 100644 --- a/config.def.h +++ b/config.def.h @@ -317,7 +317,7 @@ /* Number of threads to use for video recording */ #define DEFAULT_VIDEO_RECORD_THREADS 2 -#if defined(RARCH_CONSOLE) || defined(__APPLE__) +#if defined(RARCH_CONSOLE) #define DEFAULT_LOAD_DUMMY_ON_CORE_SHUTDOWN false #else #define DEFAULT_LOAD_DUMMY_ON_CORE_SHUTDOWN true @@ -391,6 +391,9 @@ #define MAXIMUM_FRAME_DELAY 19 #define DEFAULT_FRAME_DELAY_AUTO false +/* Try to sleep the spare time after frame is presented in order to reduce vsync CPU usage. */ +#define DEFAULT_FRAME_REST false + /* Inserts black frame(s) inbetween frames. * Useful for Higher Hz monitors (set to multiples of 60 Hz) who want to play 60 Hz * material with eliminated ghosting. video_refresh_rate should still be configured @@ -1749,8 +1752,14 @@ #define DEFAULT_AI_SERVICE_MODE 1 +#define DEFAULT_AI_SERVICE_TEXT_POSITION 0 +#define DEFAULT_AI_SERVICE_TEXT_PADDING 5 + #define DEFAULT_AI_SERVICE_URL "http://localhost:4404/" +#define DEFAULT_AI_SERVICE_POLL_DELAY 0 +#define MAXIMUM_AI_SERVICE_POLL_DELAY 500 + #if defined(HAVE_FFMPEG) || defined(HAVE_MPV) #define DEFAULT_BUILTIN_MEDIAPLAYER_ENABLE true #else diff --git a/configuration.c b/configuration.c index 43d6a145d0b4..89e9fe44f791 100644 --- a/configuration.c +++ b/configuration.c @@ -1619,9 +1619,10 @@ static struct config_path_setting *populate_settings_path( SETTING_PATH("menu_wallpaper", settings->paths.path_menu_wallpaper, false, NULL, true); #ifdef HAVE_RGUI SETTING_PATH("rgui_menu_theme_preset", settings->paths.path_rgui_theme_preset, false, NULL, true); +#endif + /* Browser and config directories are not RGUI dependent, but name is kept to avoid config file change */ SETTING_PATH("rgui_browser_directory", settings->paths.directory_menu_content, true, NULL, true); SETTING_PATH("rgui_config_directory", settings->paths.directory_menu_config, true, NULL, true); -#endif #ifdef HAVE_XMB SETTING_PATH("xmb_font", settings->paths.path_menu_xmb_font, false, NULL, true); #endif @@ -1809,6 +1810,7 @@ static struct config_bool_setting *populate_settings_bool( SETTING_BOOL("video_ctx_scaling", &settings->bools.video_ctx_scaling, true, DEFAULT_VIDEO_CTX_SCALING, false); SETTING_BOOL("video_force_aspect", &settings->bools.video_force_aspect, true, DEFAULT_FORCE_ASPECT, false); SETTING_BOOL("video_frame_delay_auto", &settings->bools.video_frame_delay_auto, true, DEFAULT_FRAME_DELAY_AUTO, false); + SETTING_BOOL("video_frame_rest", &settings->bools.video_frame_rest, true, DEFAULT_FRAME_REST, false); #if defined(DINGUX) SETTING_BOOL("video_dingux_ipu_keep_aspect", &settings->bools.video_dingux_ipu_keep_aspect, true, DEFAULT_DINGUX_IPU_KEEP_ASPECT, false); #endif @@ -1861,7 +1863,6 @@ static struct config_bool_setting *populate_settings_bool( SETTING_BOOL("menu_unified_controls", &settings->bools.menu_unified_controls, true, false, false); SETTING_BOOL("menu_disable_info_button", &settings->bools.menu_disable_info_button, true, false, false); SETTING_BOOL("menu_disable_search_button", &settings->bools.menu_disable_search_button, true, false, false); - SETTING_BOOL("menu_throttle_framerate", &settings->bools.menu_throttle_framerate, true, true, false); SETTING_BOOL("menu_linear_filter", &settings->bools.menu_linear_filter, true, DEFAULT_VIDEO_SMOOTH, false); SETTING_BOOL("menu_horizontal_animation", &settings->bools.menu_horizontal_animation, true, DEFAULT_MENU_HORIZONTAL_ANIMATION, false); SETTING_BOOL("menu_pause_libretro", &settings->bools.menu_pause_libretro, true, true, false); @@ -2476,11 +2477,13 @@ static struct config_uint_setting *populate_settings_uint( SETTING_UINT("cheevos_appearance_anchor", &settings->uints.cheevos_appearance_anchor, true, DEFAULT_CHEEVOS_APPEARANCE_ANCHOR, false); SETTING_UINT("cheevos_visibility_summary", &settings->uints.cheevos_visibility_summary, true, DEFAULT_CHEEVOS_VISIBILITY_SUMMARY, false); #endif - SETTING_UINT("accessibility_narrator_speech_speed", &settings->uints.accessibility_narrator_speech_speed, true, DEFAULT_ACCESSIBILITY_NARRATOR_SPEECH_SPEED, false); - SETTING_UINT("ai_service_mode", &settings->uints.ai_service_mode, true, DEFAULT_AI_SERVICE_MODE, false); - SETTING_UINT("ai_service_target_lang", &settings->uints.ai_service_target_lang, true, 0, false); - SETTING_UINT("ai_service_source_lang", &settings->uints.ai_service_source_lang, true, 0, false); + SETTING_UINT("ai_service_mode", &settings->uints.ai_service_mode, true, DEFAULT_AI_SERVICE_MODE, false); + SETTING_UINT("ai_service_target_lang", &settings->uints.ai_service_target_lang, true, 0, false); + SETTING_UINT("ai_service_source_lang", &settings->uints.ai_service_source_lang, true, 0, false); + SETTING_UINT("ai_service_poll_delay", &settings->uints.ai_service_poll_delay, true, DEFAULT_AI_SERVICE_POLL_DELAY, false); + SETTING_UINT("ai_service_text_position", &settings->uints.ai_service_text_position, true, DEFAULT_AI_SERVICE_TEXT_POSITION, false); + SETTING_UINT("ai_service_text_padding", &settings->uints.ai_service_text_padding, true, DEFAULT_AI_SERVICE_TEXT_PADDING, false); #ifdef HAVE_LIBNX SETTING_UINT("libnx_overclock", &settings->uints.libnx_overclock, true, SWITCH_DEFAULT_CPU_PROFILE, false); diff --git a/configuration.h b/configuration.h index b951533465f1..0a8613b5319e 100644 --- a/configuration.h +++ b/configuration.h @@ -334,6 +334,9 @@ typedef struct settings unsigned ai_service_mode; unsigned ai_service_target_lang; unsigned ai_service_source_lang; + unsigned ai_service_poll_delay; + unsigned ai_service_text_position; + unsigned ai_service_text_padding; unsigned core_updater_auto_backup_history_size; unsigned video_black_frame_insertion; @@ -575,6 +578,7 @@ typedef struct settings bool video_ctx_scaling; bool video_force_aspect; bool video_frame_delay_auto; + bool video_frame_rest; bool video_crop_overscan; bool video_aspect_ratio_auto; bool video_dingux_ipu_keep_aspect; @@ -702,13 +706,11 @@ typedef struct settings bool menu_core_enable; bool menu_show_sublabels; bool menu_dynamic_wallpaper_enable; - bool menu_throttle; bool menu_mouse_enable; bool menu_pointer_enable; bool menu_navigation_wraparound_enable; bool menu_navigation_browser_filter_supported_extensions_enable; bool menu_show_advanced_settings; - bool menu_throttle_framerate; bool menu_linear_filter; bool menu_horizontal_animation; bool menu_scroll_fast; diff --git a/cores/libretro-video-processor/video_processor_v4l2.c b/cores/libretro-video-processor/video_processor_v4l2.c index af6552c9a613..1eab78feed60 100644 --- a/cores/libretro-video-processor/video_processor_v4l2.c +++ b/cores/libretro-video-processor/video_processor_v4l2.c @@ -270,6 +270,7 @@ enumerate_audio_devices(char *buf, size_t buflen) RETRO_API void VIDEOPROC_CORE_PREFIX(retro_set_environment)(retro_environment_t cb) { + bool no_content = true; char video_devices[ENVVAR_BUFLEN]; char audio_devices[ENVVAR_BUFLEN]; struct retro_variable envvars[] = { @@ -283,7 +284,6 @@ RETRO_API void VIDEOPROC_CORE_PREFIX(retro_set_environment)(retro_environment_t VIDEOPROC_CORE_PREFIX(environment_cb) = cb; - bool no_content = true; cb(RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME, &no_content); /* Allows retroarch to seed the previous values */ diff --git a/deps/rcheevos/CHANGELOG.md b/deps/rcheevos/CHANGELOG.md index 150f90e12b14..9cced4c5d231 100644 --- a/deps/rcheevos/CHANGELOG.md +++ b/deps/rcheevos/CHANGELOG.md @@ -1,3 +1,25 @@ +# v11.0.0 +* add rc_client_t and related functions +* add RC_MEMSIZE_FLOAT_BE +* add Game Pak SRAM to GBA memory map +* add hash method for Super Cassettevision +* add PSP to potential consoles for chd iterator +* add content_type to rc_api_request_t for client to pass to server +* add rc_api_process_X_server_response methods to pass status_code and body_length to response processing functions +* add additional error codes to rc_api_process_login_response: RC_INVALID_CREDENTIALS, RC_EXPIRED_TOKEN, RC_ACCESS_DENIED +* rc_api_start_session now also returns unlocks without having to explicitly call rc_api_fetch_user_unlocks separately +* add validation warning for using hit target of 1 on ResetIf condition +* move compat.c up a directory and rename to rc_compat.c as it's shared by all subfolders +* move rc_libretro.c up a directory as it uses files from all subfolders +* convert loosely sized types to strongly sized types (unsigned -> uint32t, unsigned char -> uint8_t, etc) + +# v10.7.1 +* add rc_runtime_alloc +* add rc_libretro_memory_find_avail +* extract nginx errors from HTML returned for JSON endpoints +* fix real address for 32X extension RAM +* fix crash attempting to calculate gamecube hash for non-existent file + # v10.7.0 * add hash method and memory map for Gamecube * add console enum, hash method, and memory map for DSi diff --git a/deps/rcheevos/README.md b/deps/rcheevos/README.md index 7148206eccc6..b0b1a8c602ee 100644 --- a/deps/rcheevos/README.md +++ b/deps/rcheevos/README.md @@ -6,13 +6,11 @@ Keep in mind that **rcheevos** does *not* provide HTTP network connections. Clie Not all structures defined by **rcheevos** can be created via the public API, but are exposed to allow interactions beyond just creation, destruction, and testing, such as the ones required by UI code that helps to create them. -Finally, **rcheevos** does *not* allocate or manage memory by itself. All structures that can be returned by it have a function to determine the number of bytes needed to hold the structure, and another one that actually builds the structure using a caller-provided buffer to bake it. - ## Lua -RetroAchievements is considering the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. The current expression-based implementation is often limiting on newer systems. +RetroAchievements previously considered the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. -At this point, to enable Lua support, you must compile with an additional compilation flag: `HAVE_LUA`, as neither the backend nor the UI for editing achievements are currently Lua-enabled. +To enable Lua support, you must compile with an additional compilation flag: `HAVE_LUA`, as neither the backend nor the UI for editing achievements are currently Lua-enabled. We do not foresee enabling it any time soon, but the code has not yet been completely eliminated as many of the low-level API fuctions have parameters for LUA data. > **rcheevos** does *not* create or maintain a Lua state, you have to create your own state and provide it to **rcheevos** to be used when Lua-coded achievements are found. Calls to **rcheevos** may allocate and/or free additional memory as part of the Lua runtime. @@ -28,49 +26,9 @@ An understanding about how achievements are developed may be useful, you can rea Most of the exposed APIs are documented [here](https://github.com/RetroAchievements/rcheevos/wiki) -### User Configuration - -There's only one thing that can be configured by users of **rcheevos**: `RC_ALIGNMENT`. This macro holds the alignment of allocations made in the buffer provided to the parsing functions, and the default value is `sizeof(void*)`. - -If your platform will benefit from a different value, define a new value for it on your compiler flags before compiling the code. It has to be a power of 2, but no checking is done. - ### Return values -Any function in the rcheevos library that returns a success indicator will return one of the following values. - -These are in `rc_error.h`. - -```c -enum { - RC_OK = 0, - RC_INVALID_LUA_OPERAND = -1, - RC_INVALID_MEMORY_OPERAND = -2, - RC_INVALID_CONST_OPERAND = -3, - RC_INVALID_FP_OPERAND = -4, - RC_INVALID_CONDITION_TYPE = -5, - RC_INVALID_OPERATOR = -6, - RC_INVALID_REQUIRED_HITS = -7, - RC_DUPLICATED_START = -8, - RC_DUPLICATED_CANCEL = -9, - RC_DUPLICATED_SUBMIT = -10, - RC_DUPLICATED_VALUE = -11, - RC_DUPLICATED_PROGRESS = -12, - RC_MISSING_START = -13, - RC_MISSING_CANCEL = -14, - RC_MISSING_SUBMIT = -15, - RC_MISSING_VALUE = -16, - RC_INVALID_LBOARD_FIELD = -17, - RC_MISSING_DISPLAY_STRING = -18, - RC_OUT_OF_MEMORY = -19, - RC_INVALID_VALUE_FLAG = -20, - RC_MISSING_VALUE_MEASURED = -21, - RC_MULTIPLE_MEASURED = -22, - RC_INVALID_MEASURED_TARGET = -23, - RC_INVALID_COMPARISON = -24, - RC_INVALID_STATE = -25, - RC_INVALID_JSON = -26 -}; -``` +Any function in the rcheevos library that returns a success indicator will return `RC_OK` or one of the constants defined in `rc_error.h`. To convert the return code into something human-readable, pass it to: ```c @@ -79,189 +37,17 @@ const char* rc_error_str(int ret); ### Console identifiers -This enumeration uniquely identifies each of the supported platforms in RetroAchievements. - -These are in `rc_consoles.h`. - -```c -enum { - RC_CONSOLE_MEGA_DRIVE = 1, - RC_CONSOLE_NINTENDO_64 = 2, - RC_CONSOLE_SUPER_NINTENDO = 3, - RC_CONSOLE_GAMEBOY = 4, - RC_CONSOLE_GAMEBOY_ADVANCE = 5, - RC_CONSOLE_GAMEBOY_COLOR = 6, - RC_CONSOLE_NINTENDO = 7, - RC_CONSOLE_PC_ENGINE = 8, - RC_CONSOLE_SEGA_CD = 9, - RC_CONSOLE_SEGA_32X = 10, - RC_CONSOLE_MASTER_SYSTEM = 11, - RC_CONSOLE_PLAYSTATION = 12, - RC_CONSOLE_ATARI_LYNX = 13, - RC_CONSOLE_NEOGEO_POCKET = 14, - RC_CONSOLE_GAME_GEAR = 15, - RC_CONSOLE_GAMECUBE = 16, - RC_CONSOLE_ATARI_JAGUAR = 17, - RC_CONSOLE_NINTENDO_DS = 18, - RC_CONSOLE_WII = 19, - RC_CONSOLE_WII_U = 20, - RC_CONSOLE_PLAYSTATION_2 = 21, - RC_CONSOLE_XBOX = 22, - RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, - RC_CONSOLE_POKEMON_MINI = 24, - RC_CONSOLE_ATARI_2600 = 25, - RC_CONSOLE_MS_DOS = 26, - RC_CONSOLE_ARCADE = 27, - RC_CONSOLE_VIRTUAL_BOY = 28, - RC_CONSOLE_MSX = 29, - RC_CONSOLE_COMMODORE_64 = 30, - RC_CONSOLE_ZX81 = 31, - RC_CONSOLE_ORIC = 32, - RC_CONSOLE_SG1000 = 33, - RC_CONSOLE_VIC20 = 34, - RC_CONSOLE_AMIGA = 35, - RC_CONSOLE_ATARI_ST = 36, - RC_CONSOLE_AMSTRAD_PC = 37, - RC_CONSOLE_APPLE_II = 38, - RC_CONSOLE_SATURN = 39, - RC_CONSOLE_DREAMCAST = 40, - RC_CONSOLE_PSP = 41, - RC_CONSOLE_CDI = 42, - RC_CONSOLE_3DO = 43, - RC_CONSOLE_COLECOVISION = 44, - RC_CONSOLE_INTELLIVISION = 45, - RC_CONSOLE_VECTREX = 46, - RC_CONSOLE_PC8800 = 47, - RC_CONSOLE_PC9800 = 48, - RC_CONSOLE_PCFX = 49, - RC_CONSOLE_ATARI_5200 = 50, - RC_CONSOLE_ATARI_7800 = 51, - RC_CONSOLE_X68K = 52, - RC_CONSOLE_WONDERSWAN = 53, - RC_CONSOLE_CASSETTEVISION = 54, - RC_CONSOLE_SUPER_CASSETTEVISION = 55, - RC_CONSOLE_NEO_GEO_CD = 56, - RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, - RC_CONSOLE_FM_TOWNS = 58, - RC_CONSOLE_ZX_SPECTRUM = 59, - RC_CONSOLE_GAME_AND_WATCH = 60, - RC_CONSOLE_NOKIA_NGAGE = 61, - RC_CONSOLE_NINTENDO_3DS = 62, - RC_CONSOLE_SUPERVISION = 63, - RC_CONSOLE_SHARPX1 = 64, - RC_CONSOLE_TIC80 = 65, - RC_CONSOLE_THOMSONTO8 = 66 -}; -``` +Platforms supported by RetroAchievements are enumerated in `rc_consoles.h`. Note that some consoles in the enum are not yet fully supported (may require a memory map or some way to uniquely identify games). ## Runtime support -The runtime encapsulates a set of achievements, leaderboards, and rich presence for a game and manages processing them for each frame. When important things occur, events are raised for the caller via a callback. +Provides a set of functions for managing an active game - initializing and processing achievements, leaderboards, and rich presence. When important things occur, events are raised for the caller via a callback. These are in `rc_runtime.h`. -The `rc_runtime_t` structure uses several forward-defines. If you need access to the actual contents of any of the forward-defined structures, those definitions are in `rc_runtime_types.h` +Note: `rc_runtime_t` still requires the client implement all of the logic that calls the APIs to retrieve the data and perform the unlocks. -```c -typedef struct rc_runtime_t { - rc_runtime_trigger_t* triggers; - unsigned trigger_count; - unsigned trigger_capacity; - - rc_runtime_lboard_t* lboards; - unsigned lboard_count; - unsigned lboard_capacity; - - rc_runtime_richpresence_t* richpresence; - - rc_memref_value_t* memrefs; - rc_memref_value_t** next_memref; - - rc_value_t* variables; - rc_value_t** next_variable; -} -rc_runtime_t; -``` - -The runtime must first be initialized. -```c -void rc_runtime_init(rc_runtime_t* runtime); -``` - -Then individual achievements, leaderboards, and even rich presence can be loaded into the runtime. These functions return RC_OK, or one of the negative value error codes listed above. -```c -int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); -``` - -The runtime should be called once per frame to evaluate the state of the active achievements/leaderboards: -```c -void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L); -``` - -The `event_handler` is a callback function that is called for each event that occurs when processing the frame. -```c -typedef struct rc_runtime_event_t { - unsigned id; - int value; - char type; -} -rc_runtime_event_t; - -typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); -``` - -The `event.type` field will be one of the following: -* RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED (id=achievement id) - An achievement starts in the RC_TRIGGER_STATE_WAITING state and cannot trigger until it has been false for at least one frame. This event indicates the achievement is no longer waiting and may trigger on a future frame. -* RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED (id=achievement id) - One or more conditions in the achievement have disabled the achievement. -* RC_RUNTIME_EVENT_ACHIEVEMENT_RESET (id=achievement id) - One or more conditions in the achievement have reset any progress captured in the achievement. -* RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED (id=achievement id) - All conditions for the achievement have been met and the user should be informed. - NOTE: If `rc_runtime_reset` is called without deactivating the achievement, it may trigger again. -* RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED (id=achievement id) - All non-trigger conditions for the achievement have been met. This typically indicates the achievement is a challenge achievement and the challenge is active. -* RC_RUNTIME_EVENT_LBOARD_STARTED (id=leaderboard id, value=leaderboard value) - The leaderboard's start condition has been met and the user should be informed that a leaderboard attempt has started. -* RC_RUNTIME_EVENT_LBOARD_CANCELED (id=leaderboard id, value=leaderboard value) - The leaderboard's cancel condition has been met and the user should be informed that a leaderboard attempt has failed. -* RC_RUNTIME_EVENT_LBOARD_UPDATED (id=leaderboard id, value=leaderboard value) - The leaderboard value has changed. -* RC_RUNTIME_EVENT_LBOARD_TRIGGERED (id=leaderboard id, value=leaderboard value) - The leaderboard's submit condition has been met and the user should be informed that a leaderboard attempt was successful. The value should be submitted. -* RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED (id=achievement id) - The achievement has been disabled by a call to `rc_invalidate_address`. -* RC_RUNTIME_EVENT_LBOARD_DISABLED (id=leaderboard id) - The achievement has been disabled by a call to `rc_invalidate_address`. - -When an achievement triggers, it should be deactivated so it won't trigger again: -```c -void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); -``` -Additionally, the unlock should be submitted to the server. - -When a leaderboard triggers, it should not be deactivated in case the player wants to try again for a better score. The value should be submitted to the server. - -For `RC_RUNTIME_EVENT_LBOARD_UPDATED` and `RC_RUNTIME_EVENT_LBOARD_TRIGGERED` events, there is a helper function to call if you wish to display the leaderboard value on screen. - -```c -int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format); -``` - -`rc_runtime_do_frame` also periodically updates the rich presense string (every 60 frames). To get the current value, call -```c -const char* rc_runtime_get_richpresence(const rc_runtime_t* runtime); -``` - -When the game is reset, the runtime should also be reset: -```c -void rc_runtime_reset(rc_runtime_t* runtime); -``` - -This ensures any active achievements/leaderboards are set back to their initial states and prevents unexpected triggers when the memory changes in atypical way. +The `rc_client_t` functions wrap a `rc_runtime_t` and manage the API calls and other common functionality (like managing the user information, identifying/loading a game, and building the active/inactive achievements list for the UI). Please see [the wiki](https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration) for details on using the `rc_client_t` functions. ## Server Communication @@ -271,6 +57,8 @@ This ensures any active achievements/leaderboards are set back to their initial NOTE: **rapi** is a replacement for **rurl**. **rurl** has been deprecated. +NOTE: `rc_client` is the preferred way to have a client interact with the server. + These are in `rc_api_user.h`, `rc_api_runtime.h` and `rc_api_common.h`. The basic process of making an **rapi** call is to initialize a params object, call a function to convert it to a URL, send that to the server, then pass the response to a function to convert it into a response object, and handle the response values. @@ -291,6 +79,3 @@ These are in `rc_hash.h`. int rc_hash_generate_from_buffer(char hash[33], int console_id, uint8_t* buffer, size_t buffer_size); int rc_hash_generate_from_file(char hash[33], int console_id, const char* path); ``` - - - diff --git a/deps/rcheevos/_config.yml b/deps/rcheevos/_config.yml deleted file mode 100644 index 18854876c67f..000000000000 --- a/deps/rcheevos/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-midnight \ No newline at end of file diff --git a/deps/rcheevos/include/rc_api_info.h b/deps/rcheevos/include/rc_api_info.h index 7979cc391f56..a9586eeb9a23 100644 --- a/deps/rcheevos/include/rc_api_info.h +++ b/deps/rcheevos/include/rc_api_info.h @@ -3,6 +3,7 @@ #include "rc_api_request.h" +#include #include #ifdef __cplusplus @@ -20,13 +21,13 @@ typedef struct rc_api_fetch_achievement_info_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the achievement */ - unsigned achievement_id; + uint32_t achievement_id; /* The 1-based index of the first entry to retrieve */ - unsigned first_entry; + uint32_t first_entry; /* The number of entries to retrieve */ - unsigned count; + uint32_t count; /* Non-zero to only return unlocks earned by the user's friends */ - unsigned friends_only; + uint32_t friends_only; } rc_api_fetch_achievement_info_request_t; @@ -44,18 +45,18 @@ rc_api_achievement_awarded_entry_t; */ typedef struct rc_api_fetch_achievement_info_response_t { /* The unique identifier of the achievement */ - unsigned id; + uint32_t id; /* The unique identifier of the game to which the leaderboard is associated */ - unsigned game_id; + uint32_t game_id; /* The number of times the achievement has been awarded */ - unsigned num_awarded; + uint32_t num_awarded; /* The number of players that have earned at least one achievement for the game */ - unsigned num_players; + uint32_t num_players; /* An array of recently rewarded entries */ rc_api_achievement_awarded_entry_t* recently_awarded; /* The number of items in the recently_awarded array */ - unsigned num_recently_awarded; + uint32_t num_recently_awarded; /* Common server-provided response information */ rc_api_response_t response; @@ -64,6 +65,7 @@ rc_api_fetch_achievement_info_response_t; int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); /* --- Fetch Leaderboard Info --- */ @@ -73,11 +75,11 @@ void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_inf */ typedef struct rc_api_fetch_leaderboard_info_request_t { /* The unique identifier of the leaderboard */ - unsigned leaderboard_id; + uint32_t leaderboard_id; /* The number of entries to retrieve */ - unsigned count; + uint32_t count; /* The 1-based index of the first entry to retrieve */ - unsigned first_entry; + uint32_t first_entry; /* The username of the player around whom the entries should be returned */ const char* username; } @@ -88,11 +90,11 @@ typedef struct rc_api_lboard_info_entry_t { /* The user associated to the entry */ const char* username; /* The rank of the entry */ - unsigned rank; + uint32_t rank; /* The index of the entry */ - unsigned index; + uint32_t index; /* The value of the entry */ - int score; + int32_t score; /* When the entry was submitted */ time_t submitted; } @@ -103,11 +105,11 @@ rc_api_lboard_info_entry_t; */ typedef struct rc_api_fetch_leaderboard_info_response_t { /* The unique identifier of the leaderboard */ - unsigned id; + uint32_t id; /* The format to pass to rc_format_value to format the leaderboard value */ int format; /* If non-zero, indicates that lower scores appear first */ - int lower_is_better; + uint32_t lower_is_better; /* The title of the leaderboard */ const char* title; /* The description of the leaderboard */ @@ -115,7 +117,7 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ const char* definition; /* The unique identifier of the game to which the leaderboard is associated */ - unsigned game_id; + uint32_t game_id; /* The author of the leaderboard */ const char* author; /* When the leaderboard was first uploaded to the server */ @@ -126,7 +128,7 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { /* An array of requested entries */ rc_api_lboard_info_entry_t* entries; /* The number of items in the entries array */ - unsigned num_entries; + uint32_t num_entries; /* Common server-provided response information */ rc_api_response_t response; @@ -135,6 +137,7 @@ rc_api_fetch_leaderboard_info_response_t; int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); /* --- Fetch Games List --- */ @@ -144,14 +147,14 @@ void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_inf */ typedef struct rc_api_fetch_games_list_request_t { /* The unique identifier of the console to query */ - unsigned console_id; + uint32_t console_id; } rc_api_fetch_games_list_request_t; /* A game list entry */ typedef struct rc_api_game_list_entry_t { /* The unique identifier of the game */ - unsigned id; + uint32_t id; /* The name of the game */ const char* name; } @@ -164,7 +167,7 @@ typedef struct rc_api_fetch_games_list_response_t { /* An array of requested entries */ rc_api_game_list_entry_t* entries; /* The number of items in the entries array */ - unsigned num_entries; + uint32_t num_entries; /* Common server-provided response information */ rc_api_response_t response; @@ -173,6 +176,7 @@ rc_api_fetch_games_list_response_t; int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_api_request.h b/deps/rcheevos/include/rc_api_request.h index 64f25ec12abe..578488f3dbdc 100644 --- a/deps/rcheevos/include/rc_api_request.h +++ b/deps/rcheevos/include/rc_api_request.h @@ -2,37 +2,14 @@ #define RC_API_REQUEST_H #include "rc_error.h" +#include "../src/rc_util.h" + +#include #ifdef __cplusplus extern "C" { #endif -/** - * A block of memory for variable length data (like strings and arrays). - */ -typedef struct rc_api_buffer_chunk_t { - /* The current location where data is being written */ - char* write; - /* The first byte past the end of data where writing cannot occur */ - char* end; - /* The first byte of the data */ - char* start; - /* The next block in the allocated memory chain */ - struct rc_api_buffer_chunk_t* next; -} -rc_api_buffer_chunk_t; - -/** - * A preallocated block of memory for variable length data (like strings and arrays). - */ -typedef struct rc_api_buffer_t { - /* The chunk data (will point at the local data member) */ - struct rc_api_buffer_chunk_t chunk; - /* Small chunk of memory pre-allocated for the chunk */ - char data[256]; -} -rc_api_buffer_t; - /** * A constructed request to send to the retroachievements server. */ @@ -41,9 +18,11 @@ typedef struct rc_api_request_t { const char* url; /* Additional query args that should be sent via a POST command. If null, GET may be used */ const char* post_data; + /* The HTTP Content-Type of the POST data. */ + const char* content_type; /* Storage for the url and post_data */ - rc_api_buffer_t buffer; + rc_buffer_t buffer; } rc_api_request_t; @@ -55,9 +34,11 @@ typedef struct rc_api_response_t { int succeeded; /* Server-provided message associated to the failure */ const char* error_message; + /* Server-provided error code associated to the failure */ + const char* error_code; /* Storage for the response data */ - rc_api_buffer_t buffer; + rc_buffer_t buffer; } rc_api_response_t; @@ -66,6 +47,20 @@ void rc_api_destroy_request(rc_api_request_t* request); void rc_api_set_host(const char* hostname); void rc_api_set_image_host(const char* hostname); +typedef struct rc_api_server_response_t { + /* Pointer to the data returned from the server */ + const char* body; + /* Length of data returned from the server (Content-Length) */ + size_t body_length; + /* HTTP status code returned from the server */ + int http_status_code; +} rc_api_server_response_t; + +enum { + RC_API_SERVER_RESPONSE_CLIENT_ERROR = -1, + RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR = -2 +}; + #ifdef __cplusplus } #endif diff --git a/deps/rcheevos/include/rc_api_runtime.h b/deps/rcheevos/include/rc_api_runtime.h index 68f56fd5129a..e6d72a20d6be 100644 --- a/deps/rcheevos/include/rc_api_runtime.h +++ b/deps/rcheevos/include/rc_api_runtime.h @@ -3,6 +3,7 @@ #include "rc_api_request.h" +#include #include #ifdef __cplusplus @@ -19,7 +20,7 @@ typedef struct rc_api_fetch_image_request_t { /* The name of the image to fetch */ const char* image_name; /* The type of image to fetch */ - int image_type; + uint32_t image_type; } rc_api_fetch_image_request_t; @@ -50,7 +51,7 @@ rc_api_resolve_hash_request_t; */ typedef struct rc_api_resolve_hash_response_t { /* The unique identifier of the game, 0 if no match was found */ - unsigned game_id; + uint32_t game_id; /* Common server-provided response information */ rc_api_response_t response; @@ -59,6 +60,7 @@ rc_api_resolve_hash_response_t; int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); /* --- Fetch Game Data --- */ @@ -72,14 +74,14 @@ typedef struct rc_api_fetch_game_data_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; } rc_api_fetch_game_data_request_t; /* A leaderboard definition */ typedef struct rc_api_leaderboard_definition_t { /* The unique identifier of the leaderboard */ - unsigned id; + uint32_t id; /* The format to pass to rc_format_value to format the leaderboard value */ int format; /* The title of the leaderboard */ @@ -89,20 +91,20 @@ typedef struct rc_api_leaderboard_definition_t { /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ const char* definition; /* Non-zero if lower values are better for this leaderboard */ - int lower_is_better; + uint8_t lower_is_better; /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ - int hidden; + uint8_t hidden; } rc_api_leaderboard_definition_t; /* An achievement definition */ typedef struct rc_api_achievement_definition_t { /* The unique identifier of the achievement */ - unsigned id; + uint32_t id; /* The number of points the achievement is worth */ - unsigned points; + uint32_t points; /* The achievement category (core, unofficial) */ - unsigned category; + uint32_t category; /* The title of the achievement */ const char* title; /* The dscription of the achievement */ @@ -128,9 +130,9 @@ rc_api_achievement_definition_t; */ typedef struct rc_api_fetch_game_data_response_t { /* The unique identifier of the game */ - unsigned id; + uint32_t id; /* The console associated to the game */ - unsigned console_id; + uint32_t console_id; /* The title of the game */ const char* title; /* The image name for the game badge */ @@ -141,12 +143,12 @@ typedef struct rc_api_fetch_game_data_response_t { /* An array of achievements for the game */ rc_api_achievement_definition_t* achievements; /* The number of items in the achievements array */ - unsigned num_achievements; + uint32_t num_achievements; /* An array of leaderboards for the game */ rc_api_leaderboard_definition_t* leaderboards; /* The number of items in the leaderboards array */ - unsigned num_leaderboards; + uint32_t num_leaderboards; /* Common server-provided response information */ rc_api_response_t response; @@ -155,6 +157,7 @@ rc_api_fetch_game_data_response_t; int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); /* --- Ping --- */ @@ -168,7 +171,7 @@ typedef struct rc_api_ping_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; /* (optional) The current rich presence evaluation for the user */ const char* rich_presence; } @@ -185,6 +188,7 @@ rc_api_ping_response_t; int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_ping_response(rc_api_ping_response_t* response); /* --- Award Achievement --- */ @@ -198,9 +202,9 @@ typedef struct rc_api_award_achievement_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the achievement */ - unsigned achievement_id; + uint32_t achievement_id; /* Non-zero if the achievement was earned in hardcore */ - int hardcore; + uint32_t hardcore; /* The hash associated to the game being played */ const char* game_hash; } @@ -211,12 +215,14 @@ rc_api_award_achievement_request_t; */ typedef struct rc_api_award_achievement_response_t { /* The unique identifier of the achievement that was awarded */ - unsigned awarded_achievement_id; + uint32_t awarded_achievement_id; /* The updated player score */ - unsigned new_player_score; + uint32_t new_player_score; + /* The updated player softcore score */ + uint32_t new_player_score_softcore; /* The number of achievements the user has not yet unlocked for this game * (in hardcore/non-hardcore per hardcore flag in request) */ - unsigned achievements_remaining; + uint32_t achievements_remaining; /* Common server-provided response information */ rc_api_response_t response; @@ -225,6 +231,7 @@ rc_api_award_achievement_response_t; int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); /* --- Submit Leaderboard Entry --- */ @@ -238,9 +245,9 @@ typedef struct rc_api_submit_lboard_entry_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the leaderboard */ - unsigned leaderboard_id; + uint32_t leaderboard_id; /* The value being submitted */ - int score; + int32_t score; /* The hash associated to the game being played */ const char* game_hash; } @@ -251,9 +258,9 @@ typedef struct rc_api_lboard_entry_t { /* The user associated to the entry */ const char* username; /* The rank of the entry */ - unsigned rank; + uint32_t rank; /* The value of the entry */ - int score; + int32_t score; } rc_api_lboard_entry_t; @@ -262,18 +269,18 @@ rc_api_lboard_entry_t; */ typedef struct rc_api_submit_lboard_entry_response_t { /* The value that was submitted */ - int submitted_score; + int32_t submitted_score; /* The player's best submitted value */ - int best_score; + int32_t best_score; /* The player's new rank within the leaderboard */ - unsigned new_rank; + uint32_t new_rank; /* The total number of entries in the leaderboard */ - unsigned num_entries; + uint32_t num_entries; /* An array of the top entries for the leaderboard */ rc_api_lboard_entry_t* top_entries; /* The number of items in the top_entries array */ - unsigned num_top_entries; + uint32_t num_top_entries; /* Common server-provided response information */ rc_api_response_t response; @@ -282,6 +289,7 @@ rc_api_submit_lboard_entry_response_t; int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_api_user.h b/deps/rcheevos/include/rc_api_user.h index 758842557aeb..9fb348315cdc 100644 --- a/deps/rcheevos/include/rc_api_user.h +++ b/deps/rcheevos/include/rc_api_user.h @@ -3,6 +3,9 @@ #include "rc_api_request.h" +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -32,9 +35,11 @@ typedef struct rc_api_login_response_t { /* The API token to use for all future requests */ const char* api_token; /* The current score of the player */ - unsigned score; + uint32_t score; + /* The current softcore score of the player */ + uint32_t score_softcore; /* The number of unread messages waiting for the player on the web site */ - unsigned num_unread_messages; + uint32_t num_unread_messages; /* The preferred name to display for the player */ const char* display_name; @@ -45,6 +50,7 @@ rc_api_login_response_t; int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_login_response(rc_api_login_response_t* response); /* --- Start Session --- */ @@ -58,14 +64,38 @@ typedef struct rc_api_start_session_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; } rc_api_start_session_request_t; +/** + * Response data for an achievement unlock. + */ +typedef struct rc_api_unlock_entry_t { + /* The unique identifier of the unlocked achievement */ + uint32_t achievement_id; + /* When the achievement was unlocked */ + time_t when; +} +rc_api_unlock_entry_t; + /** * Response data for a start session request. */ typedef struct rc_api_start_session_response_t { + /* An array of hardcore user unlocks */ + rc_api_unlock_entry_t* hardcore_unlocks; + /* An array of user unlocks */ + rc_api_unlock_entry_t* unlocks; + + /* The number of items in the hardcore_unlocks array */ + uint32_t num_hardcore_unlocks; + /* The number of items in the unlocks array */ + uint32_t num_unlocks; + + /* The server timestamp when the response was generated */ + time_t server_now; + /* Common server-provided response information */ rc_api_response_t response; } @@ -73,6 +103,7 @@ rc_api_start_session_response_t; int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); /* --- Fetch User Unlocks --- */ @@ -86,9 +117,9 @@ typedef struct rc_api_fetch_user_unlocks_request_t { /* The API token from the login request */ const char* api_token; /* The unique identifier of the game */ - unsigned game_id; + uint32_t game_id; /* Non-zero to fetch hardcore unlocks, 0 to fetch non-hardcore unlocks */ - int hardcore; + uint32_t hardcore; } rc_api_fetch_user_unlocks_request_t; @@ -97,9 +128,9 @@ rc_api_fetch_user_unlocks_request_t; */ typedef struct rc_api_fetch_user_unlocks_response_t { /* An array of achievement IDs previously unlocked by the user */ - unsigned* achievement_ids; + uint32_t* achievement_ids; /* The number of items in the achievement_ids array */ - unsigned num_achievement_ids; + uint32_t num_achievement_ids; /* Common server-provided response information */ rc_api_response_t response; @@ -108,6 +139,7 @@ rc_api_fetch_user_unlocks_response_t; int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_client.h b/deps/rcheevos/include/rc_client.h new file mode 100644 index 000000000000..da0c40bdf1d0 --- /dev/null +++ b/deps/rcheevos/include/rc_client.h @@ -0,0 +1,659 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +/* implementation abstracted in rc_client_internal.h */ +typedef struct rc_client_t rc_client_t; +typedef struct rc_client_async_handle_t rc_client_async_handle_t; + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address into buffer. + * Returns the number of bytes read. A return value of 0 indicates the address was invalid. + */ +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); + +/** + * Internal method passed to rc_client_server_call_t to process the server response. + */ +typedef void (*rc_client_server_callback_t)(const rc_api_server_response_t* server_response, void* callback_data); + +/** + * Callback used to issue a request to the server. + */ +typedef void (*rc_client_server_call_t)(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +/** + * Generic callback for asynchronous eventing. + */ +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +/** + * Callback for logging or displaying a message. + */ +typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function); + +/** + * Releases resources associated to a rc_client_t object. + * Pointer will no longer be valid after making this call. + */ +void rc_client_destroy(rc_client_t* client); + +/** + * Sets whether hardcore is enabled (on by default). + * Can be called with a game loaded. + * Enabling hardcore with a game loaded will raise an RC_CLIENT_EVENT_RESET + * event. Processing will be disabled until rc_client_reset is called. + */ +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +int rc_client_get_hardcore_enabled(const rc_client_t* client); + +/** + * Sets whether encore mode is enabled (off by default). + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +int rc_client_get_encore_mode_enabled(const rc_client_t* client); + +/** + * Sets whether unofficial achievements should be loaded. + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +int rc_client_get_unofficial_enabled(const rc_client_t* client); + +/** + * Sets whether spectator mode is enabled (off by default). + * If enabled, events for achievement unlocks and leaderboard submissions will be + * raised, but server calls to actually perform the unlock/submit will not occur. + * Can be modified while a game is loaded. Evaluated at unlock/submit time. + * Cannot be modified if disabled before a game is loaded. + */ +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +int rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +void rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +void* rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +void rc_client_set_host(const rc_client_t* client, const char* hostname); + +typedef uint64_t rc_clock_t; +typedef rc_clock_t (*rc_get_time_millisecs_func_t)(const rc_client_t* client); + +/** + * Specifies a function that returns a value that increases once per millisecond. + */ +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler); + +/** + * Marks an async process as aborted. The associated callback will not be called. + */ +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback); +enum { + RC_CLIENT_LOG_LEVEL_NONE = 0, + RC_CLIENT_LOG_LEVEL_ERROR = 1, + RC_CLIENT_LOG_LEVEL_WARN = 2, + RC_CLIENT_LOG_LEVEL_INFO = 3, + RC_CLIENT_LOG_LEVEL_VERBOSE = 4, + NUM_RC_CLIENT_LOG_LEVELS = 5 +}; + +/*****************************************************************************\ +| User | +\*****************************************************************************/ + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Logout the user. + */ +void rc_client_logout(rc_client_t* client); + +typedef struct rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; +} rc_client_user_t; + +/** + * Gets information about the logged in user. Will return NULL if the user is not logged in. + */ +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size); + +typedef struct rc_client_user_game_summary_t +{ + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + + uint32_t points_core; + uint32_t points_unlocked; +} rc_client_user_game_summary_t; + +/** + * Gets a breakdown of the number of achievements in the game, and how many the user has unlocked. + * Used for the "You have unlocked X of Y achievements" message shown when the game starts. + */ +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +/** + * Start loading an unidentified game. + */ +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Start loading a game. + */ +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Unloads the current game. + */ +void rc_client_unload_game(rc_client_t* client); + +typedef struct rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} rc_client_game_t; + +/** + * Get information about the current game. Returns NULL if no game is loaded. + */ +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size); + +/** + * Changes the active disc in a multi-disc game. + */ +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +typedef struct rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + + uint32_t num_achievements; + uint32_t num_leaderboards; +} rc_client_subset_t; + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE = 0, /* unprocessed */ + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE = 1, /* eligible to trigger */ + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED = 2, /* earned by user */ + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED = 3, /* not supported by this version of the runtime */ + NUM_RC_CLIENT_ACHIEVEMENT_STATES = 4 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE = 0, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL = (1 << 1), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE | RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL +}; + +enum { + RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0, + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7, + NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 8 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE = 0, + RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE = (1 << 1), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE | RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE +}; + +typedef struct rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size); + +typedef struct rc_client_achievement_bucket_t { + rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_achievement_bucket_t; + +typedef struct rc_client_achievement_list_t { + rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_achievement_list_t; + +enum { + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE = 0, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS = 1 +}; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_get_achievement_list. + */ +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list); + +/** + * Returns non-zero if there are any achievements that can be queried through rc_client_create_achievement_list(). + */ +int rc_client_has_achievements(rc_client_t* client); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_STATE_INACTIVE = 0, + RC_CLIENT_LEADERBOARD_STATE_ACTIVE = 1, + RC_CLIENT_LEADERBOARD_STATE_TRACKING = 2, + RC_CLIENT_LEADERBOARD_STATE_DISABLED = 3, + NUM_RC_CLIENT_LEADERBOARD_STATES = 4 +}; + +enum { + RC_CLIENT_LEADERBOARD_FORMAT_TIME = 0, + RC_CLIENT_LEADERBOARD_FORMAT_SCORE = 1, + RC_CLIENT_LEADERBOARD_FORMAT_VALUE = 2, + NUM_RC_CLIENT_LEADERBOARD_FORMATS = 3 +}; + +#define RC_CLIENT_LEADERBOARD_DISPLAY_SIZE 24 + +typedef struct rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t format; + uint8_t lower_is_better; +} rc_client_leaderboard_t; + +/** + * Get information about a leaderboard. Returns NULL if not found. + */ +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id); + +typedef struct rc_client_leaderboard_tracker_t { + char display[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + uint32_t id; +} rc_client_leaderboard_tracker_t; + +typedef struct rc_client_leaderboard_bucket_t { + rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_leaderboard_bucket_t; + +typedef struct rc_client_leaderboard_list_t { + rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_leaderboard_list_t; + +enum { + RC_CLIENT_LEADERBOARD_BUCKET_UNKNOWN = 0, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE = 1, + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4, + NUM_RC_CLIENT_LEADERBOARD_BUCKETS = 5 +}; + +enum { + RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE = 0, + RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING = 1 +}; + +/** + * Creates a list of leaderboards matching the specified grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_get_leaderboard_list. + */ +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +/** + * Returns non-zero if the current game has any leaderboards. + */ +int rc_client_has_leaderboards(rc_client_t* client); + +typedef struct rc_client_leaderboard_entry_t { + const char* user; + char display[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + time_t submitted; + uint32_t rank; + uint32_t index; +} rc_client_leaderboard_entry_t; + +typedef struct rc_client_leaderboard_entry_list_t { + rc_client_leaderboard_entry_t* entries; + uint32_t num_entries; + int32_t user_index; +} rc_client_leaderboard_entry_list_t; + +typedef void (*rc_client_fetch_leaderboard_entries_callback_t)(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server containing the logged-in user. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Gets the URL for the profile picture of the user associated to a leaderboard entry. + * Returns RC_OK on success. + */ +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size); + +/** + * Destroys a list allocated by rc_client_begin_fetch_leaderboard_entries or rc_client_begin_fetch_leaderboard_entries_around_user. + */ +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list); + +/** + * Used for scoreboard events. Contains the response from the server when a leaderboard entry is submitted. + * NOTE: This structure is only valid within the event callback. If you want to make use of the data outside + * of the callback, you should create copies of both the top entries and usernames within. + */ +typedef struct rc_client_leaderboard_scoreboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + uint32_t rank; + /* The value of the entry */ + char score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; +} rc_client_leaderboard_scoreboard_entry_t; + +typedef struct rc_client_leaderboard_scoreboard_t { + /* The ID of the leaderboard which was submitted */ + uint32_t leaderboard_id; + /* The value that was submitted */ + char submitted_score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + /* The player's best submitted value */ + char best_score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + /* The player's new rank within the leaderboard */ + uint32_t new_rank; + /* The total number of entries in the leaderboard */ + uint32_t num_entries; + + /* An array of the top entries for the leaderboard */ + rc_client_leaderboard_scoreboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + uint32_t num_top_entries; +} rc_client_leaderboard_scoreboard_t; + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +/** + * Returns non-zero if the current game supports rich presence. + */ +int rc_client_has_rich_presence(rc_client_t* client); + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Processing | +\*****************************************************************************/ + +enum { + RC_CLIENT_EVENT_TYPE_NONE = 0, + RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED = 1, /* [achievement] was earned by the player */ + RC_CLIENT_EVENT_LEADERBOARD_STARTED = 2, /* [leaderboard] attempt has started */ + RC_CLIENT_EVENT_LEADERBOARD_FAILED = 3, /* [leaderboard] attempt failed */ + RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED = 4, /* [leaderboard] attempt submitted */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW = 5, /* [achievement] challenge indicator should be shown */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE = 6, /* [achievement] challenge indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW = 7, /* progress indicator should be shown for [achievement] */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE = 8, /* progress indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE = 9, /* progress indicator should be updated to reflect new badge/progress for [achievement] */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 10, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 11, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD = 13, /* [leaderboard_scoreboard] possibly-new ranking received for [leaderboard] */ + RC_CLIENT_EVENT_RESET = 14, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 15, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 16, /* an API response returned a [server_error] and will not be retried */ + RC_CLIENT_EVENT_DISCONNECTED = 17, /* an unlock request could not be completed and is pending */ + RC_CLIENT_EVENT_RECONNECTED = 18 /* all pending unlocks have been completed */ +}; + +typedef struct rc_client_server_error_t +{ + const char* error_message; + const char* api; + int result; + uint32_t related_id; +} rc_client_server_error_t; + +typedef struct rc_client_event_t +{ + uint32_t type; + + rc_client_achievement_t* achievement; + rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_tracker_t* leaderboard_tracker; + rc_client_leaderboard_scoreboard_t* leaderboard_scoreboard; + rc_client_server_error_t* server_error; + +} rc_client_event_t; + +/** + * Callback used to notify the client when certain events occur. + */ +typedef void (*rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); + +/** + * Determines if there are any active achievements/leaderboards/rich presence that need processing. + */ +int rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +void rc_client_do_frame(rc_client_t* client); + +/** + * Processes the periodic queue. + * Called internally by rc_client_do_frame. + * Should be explicitly called if rc_client_do_frame is not being called because emulation is paused. + */ +void rc_client_idle(rc_client_t* client); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +void rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +size_t rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer); + +/** + * Deserializes the runtime state from a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/deps/rcheevos/include/rc_consoles.h b/deps/rcheevos/include/rc_consoles.h index 71e67764a349..e416bc9e08b0 100644 --- a/deps/rcheevos/include/rc_consoles.h +++ b/deps/rcheevos/include/rc_consoles.h @@ -5,11 +5,14 @@ extern "C" { #endif +#include + /*****************************************************************************\ | Console identifiers | \*****************************************************************************/ enum { + RC_CONSOLE_UNKNOWN = 0, RC_CONSOLE_MEGA_DRIVE = 1, RC_CONSOLE_NINTENDO_64 = 2, RC_CONSOLE_SUPER_NINTENDO = 3, @@ -112,21 +115,21 @@ enum { }; typedef struct rc_memory_region_t { - unsigned start_address; /* first address of block as queried by RetroAchievements */ - unsigned end_address; /* last address of block as queried by RetroAchievements */ - unsigned real_address; /* real address for first address of block */ - char type; /* RC_MEMORY_TYPE_ for block */ + uint32_t start_address; /* first address of block as queried by RetroAchievements */ + uint32_t end_address; /* last address of block as queried by RetroAchievements */ + uint32_t real_address; /* real address for first address of block */ + uint8_t type; /* RC_MEMORY_TYPE_ for block */ const char* description; /* short description of block */ } rc_memory_region_t; typedef struct rc_memory_regions_t { const rc_memory_region_t* region; - unsigned num_regions; + uint32_t num_regions; } rc_memory_regions_t; -const rc_memory_regions_t* rc_console_memory_regions(int console_id); +const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id); #ifdef __cplusplus diff --git a/deps/rcheevos/include/rc_error.h b/deps/rcheevos/include/rc_error.h index 91a98c11cd98..89d762708805 100644 --- a/deps/rcheevos/include/rc_error.h +++ b/deps/rcheevos/include/rc_error.h @@ -36,7 +36,16 @@ enum { RC_INVALID_MEASURED_TARGET = -23, RC_INVALID_COMPARISON = -24, RC_INVALID_STATE = -25, - RC_INVALID_JSON = -26 + RC_INVALID_JSON = -26, + RC_API_FAILURE = -27, + RC_LOGIN_REQUIRED = -28, + RC_NO_GAME_LOADED = -29, + RC_HARDCORE_DISABLED = -30, + RC_ABORTED = -31, + RC_NO_RESPONSE = -32, + RC_ACCESS_DENIED = -33, + RC_INVALID_CREDENTIALS = -34, + RC_EXPIRED_TOKEN = -35 }; const char* rc_error_str(int ret); diff --git a/deps/rcheevos/include/rc_hash.h b/deps/rcheevos/include/rc_hash.h index 6d04536e9fec..ba9ea1c027b4 100644 --- a/deps/rcheevos/include/rc_hash.h +++ b/deps/rcheevos/include/rc_hash.h @@ -27,20 +27,20 @@ extern "C" { /* data for rc_hash_iterate */ - struct rc_hash_iterator + typedef struct rc_hash_iterator { - uint8_t* buffer; + const uint8_t* buffer; size_t buffer_size; uint8_t consoles[12]; int index; const char* path; - }; + } rc_hash_iterator_t; /* initializes a rc_hash_iterator * - path must be provided * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) */ - void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size); + void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); /* releases resources associated to a rc_hash_iterator */ @@ -92,12 +92,12 @@ extern "C" { /* ===================================================== */ - #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) - #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) - #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) - #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ + #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) /* the first data/audio track of the second session */ - /* opens a track from the specified file. track 0 indicates the largest data track should be opened. + /* opens a track from the specified file. see the RC_HASH_CDTRACK_ defines for special tracks. * returns a handle to be passed to the other functions, or NULL if the track could not be opened. */ typedef void* (*rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); diff --git a/deps/rcheevos/include/rc_runtime.h b/deps/rcheevos/include/rc_runtime.h index 92b429635101..2804cc97d952 100644 --- a/deps/rcheevos/include/rc_runtime.h +++ b/deps/rcheevos/include/rc_runtime.h @@ -8,6 +8,7 @@ extern "C" { #include "rc_error.h" #include +#include /*****************************************************************************\ | Forward Declarations (defined in rc_runtime_types.h) | @@ -34,32 +35,32 @@ typedef struct rc_value_t rc_value_t; * num_bytes is greater than 1, the value is read in little-endian from * memory. */ -typedef unsigned (*rc_runtime_peek_t)(unsigned address, unsigned num_bytes, void* ud); +typedef uint32_t(*rc_runtime_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); /*****************************************************************************\ | Runtime | \*****************************************************************************/ typedef struct rc_runtime_trigger_t { - unsigned id; + uint32_t id; rc_trigger_t* trigger; void* buffer; rc_memref_t* invalid_memref; - unsigned char md5[16]; - int serialized_size; - char owns_memrefs; + uint8_t md5[16]; + int32_t serialized_size; + uint8_t owns_memrefs; } rc_runtime_trigger_t; typedef struct rc_runtime_lboard_t { - unsigned id; - int value; + uint32_t id; + int32_t value; rc_lboard_t* lboard; void* buffer; rc_memref_t* invalid_memref; - unsigned char md5[16]; - int serialized_size; - char owns_memrefs; + uint8_t md5[16]; + uint32_t serialized_size; + uint8_t owns_memrefs; } rc_runtime_lboard_t; @@ -67,19 +68,19 @@ typedef struct rc_runtime_richpresence_t { rc_richpresence_t* richpresence; void* buffer; struct rc_runtime_richpresence_t* previous; - unsigned char md5[16]; - char owns_memrefs; + uint8_t md5[16]; + uint8_t owns_memrefs; } rc_runtime_richpresence_t; typedef struct rc_runtime_t { rc_runtime_trigger_t* triggers; - unsigned trigger_count; - unsigned trigger_capacity; + uint32_t trigger_count; + uint32_t trigger_capacity; rc_runtime_lboard_t* lboards; - unsigned lboard_count; - unsigned lboard_capacity; + uint32_t lboard_count; + uint32_t lboard_capacity; rc_runtime_richpresence_t* richpresence; @@ -88,26 +89,29 @@ typedef struct rc_runtime_t { rc_value_t* variables; rc_value_t** next_variable; + + uint8_t owns_self; } rc_runtime_t; +rc_runtime_t* rc_runtime_alloc(void); void rc_runtime_init(rc_runtime_t* runtime); void rc_runtime_destroy(rc_runtime_t* runtime); -int rc_runtime_activate_achievement(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, unsigned id); -rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* runtime, unsigned id); -int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id, unsigned* measured_value, unsigned* measured_target); -int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned id, char *buffer, size_t buffer_size); +int rc_runtime_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_achievement(rc_runtime_t* runtime, uint32_t id); +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* runtime, uint32_t id); +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target); +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char *buffer, size_t buffer_size); -int rc_runtime_activate_lboard(rc_runtime_t* runtime, unsigned id, const char* memaddr, lua_State* L, int funcs_idx); -void rc_runtime_deactivate_lboard(rc_runtime_t* runtime, unsigned id); -rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* runtime, unsigned id); -int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format); +int rc_runtime_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +void rc_runtime_deactivate_lboard(rc_runtime_t* runtime, uint32_t id); +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* runtime, uint32_t id); +int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format); int rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); -int rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, unsigned buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L); +int rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L); enum { RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, /* from WAITING, PAUSED, or PRIMED to ACTIVE */ @@ -126,9 +130,9 @@ enum { }; typedef struct rc_runtime_event_t { - unsigned id; - int value; - char type; + uint32_t id; + int32_t value; + uint8_t type; } rc_runtime_event_t; @@ -137,13 +141,13 @@ typedef void (*rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_eve void rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L); void rc_runtime_reset(rc_runtime_t* runtime); -typedef int (*rc_runtime_validate_address_t)(unsigned address); +typedef int (*rc_runtime_validate_address_t)(uint32_t address); void rc_runtime_validate_addresses(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); -void rc_runtime_invalidate_address(rc_runtime_t* runtime, unsigned address); +void rc_runtime_invalidate_address(rc_runtime_t* runtime, uint32_t address); int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L); int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L); -int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* serialized, lua_State* L); +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L); #ifdef __cplusplus } diff --git a/deps/rcheevos/include/rc_runtime_types.h b/deps/rcheevos/include/rc_runtime_types.h index dc3ab60c920d..04a0c931ca37 100644 --- a/deps/rcheevos/include/rc_runtime_types.h +++ b/deps/rcheevos/include/rc_runtime_types.h @@ -7,6 +7,9 @@ extern "C" { #include "rc_error.h" +#include +#include + #ifndef RC_RUNTIME_H /* prevents pedantic redefiniton error */ typedef struct lua_State lua_State; @@ -28,7 +31,7 @@ typedef struct rc_value_t rc_value_t; * num_bytes is greater than 1, the value is read in little-endian from * memory. */ -typedef unsigned (*rc_peek_t)(unsigned address, unsigned num_bytes, void* ud); +typedef uint32_t(*rc_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); /*****************************************************************************\ | Memory References | @@ -57,24 +60,25 @@ enum { RC_MEMSIZE_FLOAT, RC_MEMSIZE_MBF32, RC_MEMSIZE_MBF32_LE, + RC_MEMSIZE_FLOAT_BE, RC_MEMSIZE_VARIABLE }; typedef struct rc_memref_value_t { /* The current value of this memory reference. */ - unsigned value; + uint32_t value; /* The last differing value of this memory reference. */ - unsigned prior; + uint32_t prior; /* The size of the value. */ - char size; + uint8_t size; /* True if the value changed this frame. */ - char changed; + uint8_t changed; /* The value type of the value (for variables) */ - char type; + uint8_t type; /* True if the reference will be used in indirection. * NOTE: This is actually a property of the rc_memref_t, but we put it here to save space */ - char is_indirect; + uint8_t is_indirect; } rc_memref_value_t; @@ -83,7 +87,7 @@ struct rc_memref_t { rc_memref_value_t value; /* The memory address of this variable. */ - unsigned address; + uint32_t address; /* The next memory reference in the chain. */ rc_memref_t* next; @@ -111,7 +115,7 @@ typedef struct rc_operand_t { rc_memref_t* memref; /* An integer value. */ - unsigned num; + uint32_t num; /* A floating point value. */ double dbl; @@ -121,10 +125,10 @@ typedef struct rc_operand_t { } value; /* specifies which member of the value union is being used */ - char type; + uint8_t type; /* the actual RC_MEMSIZE of the operand - memref.size may differ */ - char size; + uint8_t size; } rc_operand_t; @@ -182,27 +186,27 @@ struct rc_condition_t { rc_operand_t operand2; /* Required hits to fire this condition. */ - unsigned required_hits; + uint32_t required_hits; /* Number of hits so far. */ - unsigned current_hits; + uint32_t current_hits; /* The next condition in the chain. */ rc_condition_t* next; - /* The type of the condition. */ - char type; + /* The type of the condition. (RC_CONDITION_*) */ + uint8_t type; - /* The comparison operator to use. */ - char oper; /* operator is a reserved word in C++. */ + /* The comparison operator to use. (RC_OPERATOR_*) */ + uint8_t oper; /* operator is a reserved word in C++. */ - /* Set if the condition needs to processed as part of the "check if paused" pass. */ - char pause; + /* Set if the condition needs to processed as part of the "check if paused" pass. (bool) */ + uint8_t pause; - /* Whether or not the condition evaluated true on the last check */ - char is_true; + /* Whether or not the condition evaluated true on the last check. (bool) */ + uint8_t is_true; - /* Unique identifier of optimized comparator to use */ - char optimized_comparator; + /* Unique identifier of optimized comparator to use. (RC_PROCESSING_COMPARE_*) */ + uint8_t optimized_comparator; }; /*****************************************************************************\ @@ -219,13 +223,13 @@ struct rc_condset_t { rc_condition_t* conditions; /* True if any condition in the set is a pause condition. */ - char has_pause; + uint8_t has_pause; /* True if the set is currently paused. */ - char is_paused; + uint8_t is_paused; /* True if the set has indirect memory references. */ - char has_indirect_memrefs; + uint8_t has_indirect_memrefs; }; /*****************************************************************************\ @@ -254,22 +258,22 @@ struct rc_trigger_t { rc_memref_t* memrefs; /* The current state of the MEASURED condition. */ - unsigned measured_value; + uint32_t measured_value; /* The target state of the MEASURED condition */ - unsigned measured_target; + uint32_t measured_target; /* The current state of the trigger */ - char state; + uint8_t state; /* True if at least one condition has a non-zero hit count */ - char has_hits; + uint8_t has_hits; /* True if at least one condition has a non-zero required hit count */ - char has_required_hits; + uint8_t has_required_hits; /* True if the measured value should be displayed as a percentage */ - char measured_as_percent; + uint8_t measured_as_percent; }; int rc_trigger_size(const char* memaddr); @@ -301,7 +305,7 @@ struct rc_value_t { int rc_value_size(const char* memaddr); rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -int rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); +int32_t rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); /*****************************************************************************\ | Leaderboards | @@ -326,12 +330,12 @@ struct rc_lboard_t { rc_value_t* progress; rc_memref_t* memrefs; - char state; + uint8_t state; }; int rc_lboard_size(const char* memaddr); rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -int rc_evaluate_lboard(rc_lboard_t* lboard, int* value, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_evaluate_lboard(rc_lboard_t* lboard, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L); void rc_reset_lboard(rc_lboard_t* lboard); /*****************************************************************************\ @@ -356,7 +360,7 @@ enum { }; int rc_parse_format(const char* format_str); -int rc_format_value(char* buffer, int size, int value, int format); +int rc_format_value(char* buffer, int size, int32_t value, int format); /*****************************************************************************\ | Rich Presence | @@ -365,8 +369,8 @@ int rc_format_value(char* buffer, int size, int value, int format); typedef struct rc_richpresence_lookup_item_t rc_richpresence_lookup_item_t; struct rc_richpresence_lookup_item_t { - unsigned first; - unsigned last; + uint32_t first; + uint32_t last; rc_richpresence_lookup_item_t* left; rc_richpresence_lookup_item_t* right; const char* label; @@ -379,7 +383,7 @@ struct rc_richpresence_lookup_t { rc_richpresence_lookup_t* next; const char* name; const char* default_label; - unsigned short format; + uint8_t format; }; typedef struct rc_richpresence_display_part_t rc_richpresence_display_part_t; @@ -389,7 +393,7 @@ struct rc_richpresence_display_part_t { const char* text; rc_richpresence_lookup_t* lookup; rc_memref_value_t *value; - unsigned short display_type; + uint8_t display_type; }; typedef struct rc_richpresence_display_t rc_richpresence_display_t; @@ -410,9 +414,9 @@ struct rc_richpresence_t { int rc_richpresence_size(const char* script); int rc_richpresence_size_lines(const char* script, int* lines_read); rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx); -int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L); -int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); void rc_reset_richpresence(rc_richpresence_t* self); #ifdef __cplusplus diff --git a/deps/rcheevos/src/rapi/rc_api_common.c b/deps/rcheevos/src/rapi/rc_api_common.c index 42fb40527e8e..481aee07da71 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.c +++ b/deps/rcheevos/src/rapi/rc_api_common.c @@ -2,7 +2,7 @@ #include "rc_api_request.h" #include "rc_api_runtime.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include #include @@ -16,161 +16,176 @@ static char* g_host = NULL; static char* g_imagehost = NULL; -#undef DEBUG_BUFFERS - /* --- rc_json --- */ -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field); +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); + +static int rc_json_match_char(rc_json_iterator_t* iterator, char c) +{ + if (iterator->json < iterator->end && *iterator->json == c) { + ++iterator->json; + return 1; + } + + return 0; +} + +static void rc_json_skip_whitespace(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json)) + ++iterator->json; +} + +static int rc_json_find_closing_quote(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end) { + if (*iterator->json == '"') + return 1; + + if (*iterator->json == '\\') { + ++iterator->json; + if (iterator->json == iterator->end) + return 0; + } + + if (*iterator->json == '\0') + return 0; + + ++iterator->json; + } -static int rc_json_parse_field(const char** json_ptr, rc_json_field_t* field) { + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { int result; - field->value_start = *json_ptr; + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; + + field->value_start = iterator->json; - switch (**json_ptr) + switch (*iterator->json) { case '"': /* quoted string */ - ++(*json_ptr); - while (**json_ptr != '"') { - if (**json_ptr == '\\') - ++(*json_ptr); - - if (**json_ptr == '\0') - return RC_INVALID_JSON; - - ++(*json_ptr); - } - ++(*json_ptr); + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; break; case '-': case '+': /* signed number */ - ++(*json_ptr); + ++iterator->json; /* fallthrough to number */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': /* number */ - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); - if (**json_ptr == '.') { - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + + if (rc_json_match_char(iterator, '.')) { + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; } break; case '[': /* array */ - result = rc_json_parse_array(json_ptr, field); + result = rc_json_parse_array(iterator, field); if (result != RC_OK) - return result; + return result; break; case '{': /* object */ - result = rc_json_parse_object(json_ptr, NULL, 0, &field->array_size); + result = rc_json_parse_object(iterator, NULL, 0, &field->array_size); if (result != RC_OK) return result; break; default: /* non-quoted text [true,false,null] */ - if (!isalpha((unsigned char)**json_ptr)) + if (!isalpha((unsigned char)*iterator->json)) return RC_INVALID_JSON; - do { - ++(*json_ptr); - } while (isalnum((unsigned char)**json_ptr)); + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; break; } - field->value_end = *json_ptr; + field->value_end = iterator->json; return RC_OK; } -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field) { +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { rc_json_field_t unused_field; - const char* json = *json_ptr; int result; - if (*json != '[') + if (!rc_json_match_char(iterator, '[')) return RC_INVALID_JSON; - ++json; field->array_size = 0; - if (*json != ']') { - do - { - while (isspace((unsigned char)*json)) - ++json; - result = rc_json_parse_field(&json, &unused_field); - if (result != RC_OK) - return result; + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; - ++field->array_size; + do + { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; - if (*json != ',') - break; + ++field->array_size; - ++json; - } while (1); + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); - if (*json != ']') - return RC_INVALID_JSON; - } + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; - *json_ptr = ++json; return RC_OK; } -static int rc_json_get_next_field(rc_json_object_field_iterator_t* iterator) { - const char* json = iterator->json; +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; - - if (*json != '"') + if (!rc_json_match_char(iterator, '"')) return RC_INVALID_JSON; - iterator->field.name = ++json; - while (*json != '"') { - if (!*json) + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) return RC_INVALID_JSON; - ++json; + ++iterator->json; } - iterator->name_len = json - iterator->field.name; - ++json; - - while (isspace((unsigned char)*json)) - ++json; - if (*json != ':') + if (iterator->json == iterator->end) return RC_INVALID_JSON; - ++json; + field->name_len = iterator->json - field->name; + ++iterator->json; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ':')) + return RC_INVALID_JSON; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); - if (rc_json_parse_field(&json, &iterator->field) < 0) + if (rc_json_parse_field(iterator, field) < 0) return RC_INVALID_JSON; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); - iterator->json = json; return RC_OK; } -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { - rc_json_object_field_iterator_t iterator; - const char* json = *json_ptr; +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { size_t i; - unsigned num_fields = 0; + uint32_t num_fields = 0; + rc_json_field_t field; int result; if (fields_seen) @@ -179,60 +194,119 @@ static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, for (i = 0; i < field_count; ++i) fields[i].value_start = fields[i].value_end = NULL; - if (*json != '{') + if (!rc_json_match_char(iterator, '{')) return RC_INVALID_JSON; - ++json; - if (*json == '}') { - *json_ptr = ++json; + if (rc_json_match_char(iterator, '}')) /* empty object */ return RC_OK; - } - - memset(&iterator, 0, sizeof(iterator)); - iterator.json = json; do { - result = rc_json_get_next_field(&iterator); + result = rc_json_get_next_field(iterator, &field); if (result != RC_OK) return result; for (i = 0; i < field_count; ++i) { - if (!fields[i].value_start && strncmp(fields[i].name, iterator.field.name, iterator.name_len) == 0 && - fields[i].name[iterator.name_len] == '\0') { - fields[i].value_start = iterator.field.value_start; - fields[i].value_end = iterator.field.value_end; - fields[i].array_size = iterator.field.array_size; + if (!fields[i].value_start && fields[i].name_len == field.name_len && + memcmp(fields[i].name, field.name, field.name_len) == 0) { + fields[i].value_start = field.value_start; + fields[i].value_end = field.value_end; + fields[i].array_size = field.array_size; break; } } ++num_fields; - if (*iterator.json != ',') - break; - ++iterator.json; - } while (1); + } while (rc_json_match_char(iterator, ',')); - if (*iterator.json != '}') + if (!rc_json_match_char(iterator, '}')) return RC_INVALID_JSON; if (fields_seen) *fields_seen = num_fields; - *json_ptr = ++iterator.json; return RC_OK; } -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator) { - if (*iterator->json != ',' && *iterator->json != '{') +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{')) return 0; - ++iterator->json; - return (rc_json_get_next_field(iterator) == RC_OK); + return (rc_json_get_next_field(iterator, field) == RC_OK); +} + +int rc_json_get_object_string_length(const char* json) { + const char* json_start = json; + + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */ + + rc_json_parse_object(&iterator, NULL, 0, NULL); + + return (int)(iterator.json - json_start); } -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count) { +static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) { + const char* json = server_response->body; + const char* end = json; + + const char* title_start = strstr(json, ""); + if (title_start) { + title_start += 7; + if (isdigit((int)*title_start)) { + const char* title_end = strstr(title_start + 7, ""); + if (title_end) { + response->error_message = rc_buffer_strncpy(&response->buffer, title_start, title_end - title_start); + response->succeeded = 0; + return RC_INVALID_JSON; + } + } + } + + while (*end && *end != '\n' && end - json < 200) + ++end; + + if (end > json && end[-1] == '\r') + --end; + + if (end > json) + response->error_message = rc_buffer_strncpy(&response->buffer, json, end - json); + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +static int rc_json_convert_error_code(const char* server_error_code) +{ + switch (server_error_code[0]) { + case 'a': + if (strcmp(server_error_code, "access_denied") == 0) + return RC_ACCESS_DENIED; + break; + + case 'e': + if (strcmp(server_error_code, "expired_token") == 0) + return RC_EXPIRED_TOKEN; + break; + + case 'i': + if (strcmp(server_error_code, "invalid_credentials") == 0) + return RC_INVALID_CREDENTIALS; + break; + + default: + break; + } + + return RC_API_FAILURE; +} + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) { + int result; + #ifndef NDEBUG if (field_count < 2) return RC_INVALID_STATE; @@ -242,37 +316,66 @@ int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_jso return RC_INVALID_STATE; #endif - if (*json == '{') { - int result = rc_json_parse_object(&json, fields, field_count, NULL); + response->error_message = NULL; - rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); - rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + if (!server_response) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } - return result; + if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR || + server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) { + /* client provided error message is passed as the response body */ + response->error_message = server_response->body; + response->succeeded = 0; + return RC_NO_RESPONSE; } - response->error_message = NULL; + if (!server_response->body || !*server_response->body) { + /* expect valid HTTP status codes to have bodies that we can extract the message from, + * but provide some default messages in case they don't. */ + switch (server_response->http_status_code) { + case 504: /* 504 Gateway Timeout */ + case 522: /* 522 Connection Timed Out */ + case 524: /* 524 A Timeout Occurred */ + response->error_message = "Request has timed out."; + break; - if (*json) { - const char* end = json; - while (*end && *end != '\n' && end - json < 200) - ++end; - - if (end > json && end[-1] == '\r') - --end; - - if (end > json) { - char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); - response->error_message = dst; - memcpy(dst, json, end - json); - dst += (end - json); - *dst++ = '\0'; - rc_buf_consume(&response->buffer, response->error_message, dst); + case 521: /* 521 Web Server is Down */ + case 523: /* 523 Origin is Unreachable */ + response->error_message = "Could not connect to server."; + break; + + default: + break; } + + response->succeeded = 0; + return RC_NO_RESPONSE; } - response->succeeded = 0; - return RC_INVALID_JSON; + if (*server_response->body != '{') { + result = rc_json_extract_html_error(response, server_response); + } + else { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + result = rc_json_parse_object(&iterator, fields, field_count, NULL); + + rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); + rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + + /* Code will be the third field in the fields array, but may not always be present */ + if (field_count > 2 && strcmp(fields[2].name, "Code") == 0) { + rc_json_get_optional_string(&response->error_code, response, &fields[2], "Code", NULL); + if (response->error_code != NULL) + result = rc_json_convert_error_code(response->error_code); + } + } + + return result; } static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { @@ -280,14 +383,14 @@ static int rc_json_missing_field(rc_api_response_t* response, const rc_json_fiel const size_t not_found_len = strlen(not_found); const size_t field_len = strlen(field->name); - char* write = rc_buf_reserve(&response->buffer, field_len + not_found_len + 1); + uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1); if (write) { - response->error_message = write; + response->error_message = (char*)write; memcpy(write, field->name, field_len); write += field_len; memcpy(write, not_found, not_found_len + 1); write += not_found_len + 1; - rc_buf_consume(&response->buffer, response->error_message, write); + rc_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write); } response->succeeded = 0; @@ -295,7 +398,8 @@ static int rc_json_missing_field(rc_api_response_t* response, const rc_json_fiel } int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { - const char* json = field->value_start; + rc_json_iterator_t iterator; + #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; @@ -303,45 +407,53 @@ int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_ (void)field_name; #endif - if (!json) + if (!field->value_start) return rc_json_missing_field(response, field); - return (rc_json_parse_object(&json, fields, field_count, &field->array_size) == RC_OK); + memset(&iterator, 0, sizeof(iterator)); + iterator.json = field->value_start; + iterator.end = field->value_end; + return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK); } -static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_field_t* iterator) { - if (!iterator->array_size) - return 0; +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (iterator->json >= iterator->end) + return 0; - rc_json_parse_field(&iterator->value_start, field); + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + rc_json_skip_whitespace(iterator); - ++iterator->value_start; /* skip , or ] */ + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - --iterator->array_size; return 1; } -int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { - rc_json_field_t iterator; +int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_iterator_t iterator; + rc_json_field_t array; rc_json_field_t value; unsigned* entry; - if (!rc_json_get_required_array(num_entries, &iterator, response, field, field_name)) + memset(&array, 0, sizeof(array)); + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) return RC_MISSING_VALUE; if (*num_entries) { - *entries = (unsigned*)rc_buf_alloc(&response->buffer, *num_entries * sizeof(unsigned)); + *entries = (unsigned*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(unsigned)); if (!*entries) return RC_OUT_OF_MEMORY; value.name = field_name; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array.value_start; + iterator.end = array.value_end; + entry = *entries; while (rc_json_get_array_entry_value(&value, &iterator)) { if (!rc_json_get_unum(entry, &value, field_name)) @@ -357,7 +469,19 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r return RC_OK; } -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!rc_json_get_optional_array(num_entries, array_field, response, field, field_name)) + return rc_json_missing_field(response, field); + + return 1; +} + +int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; @@ -367,44 +491,43 @@ int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, if (!field->value_start || *field->value_start != '[') { *num_entries = 0; - return rc_json_missing_field(response, field); + return 0; } - memcpy(iterator, field, sizeof(*iterator)); - ++iterator->value_start; /* skip [ */ + memcpy(array_field, field, sizeof(*array_field)); + ++array_field->value_start; /* skip [ */ *num_entries = field->array_size; return 1; } -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator) { - if (!iterator->array_size) - return 0; +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (iterator->json >= iterator->end) + return 0; - rc_json_parse_object(&iterator->value_start, fields, field_count, NULL); + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + rc_json_skip_whitespace(iterator); - ++iterator->value_start; /* skip , or ] */ + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - --iterator->array_size; return 1; } -static unsigned rc_json_decode_hex4(const char* input) { +static uint32_t rc_json_decode_hex4(const char* input) { char hex[5]; memcpy(hex, input, 4); hex[4] = '\0'; - return (unsigned)strtoul(hex, NULL, 16); + return (uint32_t)strtoul(hex, NULL, 16); } -static int rc_json_ucs32_to_utf8(unsigned char* dst, unsigned ucs32_char) { +static int rc_json_ucs32_to_utf8(uint8_t* dst, uint32_t ucs32_char) { if (ucs32_char < 0x80) { dst[0] = (ucs32_char & 0x7F); return 1; @@ -449,7 +572,7 @@ static int rc_json_ucs32_to_utf8(unsigned char* dst, unsigned ucs32_char) { return 6; } -int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; size_t len = field->value_end - field->value_start; char* dst; @@ -480,7 +603,7 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ return 1; } - *out = dst = rc_buf_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ + *out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ do { if (*src == '\\') { @@ -501,13 +624,13 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ if (*src == 'u') { /* unicode character */ - unsigned ucs32_char = rc_json_decode_hex4(src + 1); + uint32_t ucs32_char = rc_json_decode_hex4(src + 1); src += 5; if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) { /* surrogate lead - look for surrogate tail */ if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') { - const unsigned surrogate = rc_json_decode_hex4(src + 2); + const uint32_t surrogate = rc_json_decode_hex4(src + 2); src += 6; if (surrogate >= 0xDC00 && surrogate < 0xE000) { @@ -540,13 +663,13 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ } while (*src != '\"'); } else { - *out = dst = rc_buf_reserve(buffer, len + 1); /* +1 for null terminator */ + *out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */ memcpy(dst, src, len); dst += len; } *dst++ = '\0'; - rc_buf_consume(buffer, *out, dst); + rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst); return 1; } @@ -562,9 +685,9 @@ int rc_json_get_required_string(const char** out, rc_api_response_t* response, c return rc_json_missing_field(response, field); } -int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; - int value = 0; + int32_t value = 0; int negative = 0; #ifndef NDEBUG @@ -604,21 +727,21 @@ int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_na return 1; } -void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { +void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value) { if (!rc_json_get_num(out, field, field_name)) *out = default_value; } -int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { if (rc_json_get_num(out, field, field_name)) return 1; return rc_json_missing_field(response, field); } -int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; - int value = 0; + uint32_t value = 0; #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) @@ -648,12 +771,12 @@ int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* fi return 1; } -void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value) { +void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value) { if (!rc_json_get_unum(out, field, field_name)) *out = default_value; } -int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { if (rc_json_get_unum(out, field, field_name)) return 1; @@ -746,121 +869,27 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js return rc_json_missing_field(response, field); } -/* --- rc_buf --- */ - -void rc_buf_init(rc_api_buffer_t* buffer) { - buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; - buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; - buffer->chunk.next = NULL; -} - -void rc_buf_destroy(rc_api_buffer_t* buffer) { - rc_api_buffer_chunk_t *chunk; -#ifdef DEBUG_BUFFERS - int count = 0; - int wasted = 0; - int total = 0; -#endif - - /* first chunk is not allocated. skip it. */ - chunk = buffer->chunk.next; - - /* deallocate any additional buffers */ - while (chunk) { - rc_api_buffer_chunk_t* next = chunk->next; -#ifdef DEBUG_BUFFERS - total += (int)(chunk->end - chunk->data); - wasted += (int)(chunk->end - chunk->write); - ++count; -#endif - free(chunk); - chunk = next; - } - -#ifdef DEBUG_BUFFERS - printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, - total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); -#endif -} - -char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount) { - rc_api_buffer_chunk_t* chunk = &buffer->chunk; - size_t remaining; - while (chunk) { - remaining = chunk->end - chunk->write; - if (remaining >= amount) - return chunk->write; - - if (!chunk->next) { - /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated - * to the chunk header, and the remaining will be used for data. - */ - const size_t chunk_header_size = sizeof(rc_api_buffer_chunk_t); - const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; - chunk->next = (rc_api_buffer_chunk_t*)malloc(alloc_size); - if (!chunk->next) - break; - - chunk->next->start = (char*)chunk->next + chunk_header_size; - chunk->next->write = chunk->next->start; - chunk->next->end = (char*)chunk->next + alloc_size; - chunk->next->next = NULL; - } - - chunk = chunk->next; - } - - return NULL; -} - -void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end) { - rc_api_buffer_chunk_t* chunk = &buffer->chunk; - do { - if (chunk->write == start) { - size_t offset = (end - chunk->start); - offset = (offset + 7) & ~7; - chunk->write = &chunk->start[offset]; - - if (chunk->write > chunk->end) - chunk->write = chunk->end; - break; - } - - chunk = chunk->next; - } while (chunk); -} - -void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { - char* ptr = rc_buf_reserve(buffer, amount); - rc_buf_consume(buffer, ptr, ptr + amount); - return (void*)ptr; -} - -void rc_api_destroy_request(rc_api_request_t* request) { - rc_buf_destroy(&request->buffer); -} +/* --- rc_api_request --- */ -void rc_api_format_md5(char checksum[33], const unsigned char digest[16]) { - snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", - digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], - digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] - ); +void rc_api_destroy_request(rc_api_request_t* request) +{ + rc_buffer_destroy(&request->buffer); } /* --- rc_url_builder --- */ -void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size) { - rc_api_buffer_chunk_t* used_buffer; +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size) { + rc_buffer_chunk_t* used_buffer; memset(builder, 0, sizeof(*builder)); builder->buffer = buffer; - builder->write = builder->start = rc_buf_reserve(buffer, estimated_size); + builder->write = builder->start = (char*)rc_buffer_reserve(buffer, estimated_size); used_buffer = &buffer->chunk; - while (used_buffer && used_buffer->write != builder->write) + while (used_buffer && used_buffer->write != (uint8_t*)builder->write) used_buffer = used_buffer->next; - builder->end = (used_buffer) ? used_buffer->end : builder->start + estimated_size; + builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size; } const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { @@ -869,7 +898,7 @@ const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { if (builder->result != RC_OK) return NULL; - rc_buf_consume(builder->buffer, builder->start, builder->write); + rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write); return builder->start; } @@ -879,7 +908,7 @@ static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) if (remaining < amount) { const size_t used = builder->write - builder->start; const size_t current_size = builder->end - builder->start; - const size_t buffer_prefix_size = sizeof(rc_api_buffer_chunk_t); + const size_t buffer_prefix_size = sizeof(rc_buffer_chunk_t); char* new_start; size_t new_size = (current_size < 256) ? 256 : current_size * 2; do { @@ -890,11 +919,11 @@ static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) new_size *= 2; } while (1); - /* rc_buf_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ + /* rc_buffer_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ if ((remaining - amount) > buffer_prefix_size) new_size -= buffer_prefix_size; - new_start = rc_buf_reserve(builder->buffer, new_size); + new_start = (char*)rc_buffer_reserve(builder->buffer, new_size); if (!new_start) { builder->result = RC_OUT_OF_MEMORY; return RC_OUT_OF_MEMORY; @@ -985,7 +1014,7 @@ static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, con return builder->result; } -void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value) { +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value) { if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { char num[16]; int chars = snprintf(num, sizeof(num), "%u", value); @@ -993,7 +1022,7 @@ void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* } } -void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value) { +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value) { if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { char num[16]; int chars = snprintf(num, sizeof(num), "%d", value); @@ -1008,7 +1037,7 @@ void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* void rc_api_url_build_dorequest_url(rc_api_request_t* request) { #define DOREQUEST_ENDPOINT "/dorequest.php" - rc_buf_init(&request->buffer); + rc_buffer_init(&request->buffer); if (!g_host) { request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; @@ -1017,13 +1046,13 @@ void rc_api_url_build_dorequest_url(rc_api_request_t* request) { const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT); const size_t host_len = strlen(g_host); const size_t url_len = host_len + endpoint_len; - char* url = rc_buf_reserve(&request->buffer, url_len); + uint8_t* url = rc_buffer_reserve(&request->buffer, url_len); memcpy(url, g_host, host_len); memcpy(url + host_len, DOREQUEST_ENDPOINT, endpoint_len); - rc_buf_consume(&request->buffer, url, url + url_len); + rc_buffer_consume(&request->buffer, url, url + url_len); - request->url = url; + request->url = (char*)url; } #undef DOREQUEST_ENDPOINT } @@ -1097,7 +1126,7 @@ void rc_api_set_image_host(const char* hostname) { int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) { rc_api_url_builder_t builder; - rc_buf_init(&request->buffer); + rc_buffer_init(&request->buffer); rc_url_builder_init(&builder, &request->buffer, 64); if (g_imagehost) { diff --git a/deps/rcheevos/src/rapi/rc_api_common.h b/deps/rcheevos/src/rapi/rc_api_common.h index fa18b7798edb..9fbb8a23d069 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.h +++ b/deps/rcheevos/src/rapi/rc_api_common.h @@ -10,72 +10,69 @@ extern "C" { #endif +#define RC_CONTENT_TYPE_URLENCODED "application/x-www-form-urlencoded" + typedef struct rc_api_url_builder_t { char* write; char* start; char* end; - /* pointer to a preallocated rc_api_buffer_t */ - rc_api_buffer_t* buffer; + /* pointer to a preallocated rc_buffer_t */ + rc_buffer_t* buffer; int result; } rc_api_url_builder_t; -void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size); +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size); void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); -#define RC_JSON_NEW_FIELD(n) {n,0,0,0} +#define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} typedef struct rc_json_field_t { - const char* name; const char* value_start; const char* value_end; - unsigned array_size; + const char* name; + size_t name_len; + uint32_t array_size; } rc_json_field_t; -typedef struct rc_json_object_field_iterator_t { - rc_json_field_t field; +typedef struct rc_json_iterator_t { const char* json; - size_t name_len; + const char* end; } -rc_json_object_field_iterator_t; +rc_json_iterator_t; -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count); -int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); -int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name); -int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name); +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count); +int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name); void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value); -void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value); -void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value); +void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value); +void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value); void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); -int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator); -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator); - -void rc_buf_init(rc_api_buffer_t* buffer); -void rc_buf_destroy(rc_api_buffer_t* buffer); -char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount); -void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end); -void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount); +int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); +int rc_json_get_object_string_length(const char* json); void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); -void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value); -void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value); +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value); +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value); void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value); void rc_api_url_build_dorequest_url(rc_api_request_t* request); int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token); -void rc_api_format_md5(char checksum[33], const unsigned char digest[16]); #ifdef __cplusplus } diff --git a/deps/rcheevos/src/rapi/rc_api_info.c b/deps/rcheevos/src/rapi/rc_api_info.c index 774220adcced..2b9f882625b1 100644 --- a/deps/rcheevos/src/rapi/rc_api_info.c +++ b/deps/rcheevos/src/rapi/rc_api_info.c @@ -27,15 +27,27 @@ int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_achievement_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_awarded_entry_t* entry; - rc_json_field_t iterator; - unsigned timet; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; int result; rc_json_field_t fields[] = { @@ -63,9 +75,9 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -81,14 +93,18 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_array(&response->num_recently_awarded, &iterator, &response->response, &response_fields[3], "RecentWinner")) + if (!rc_json_get_required_array(&response->num_recently_awarded, &array_field, &response->response, &response_fields[3], "RecentWinner")) return RC_MISSING_VALUE; if (response->num_recently_awarded) { - response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); + response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); if (!response->recently_awarded) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->recently_awarded; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -106,7 +122,7 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info } void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch Leaderboard Info --- */ @@ -130,14 +146,26 @@ int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_leaderboard_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_info_entry_t* entry; - rc_json_field_t iterator; - unsigned timet; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; int result; size_t len; char format[16]; @@ -178,9 +206,9 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -189,7 +217,7 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info if (!rc_json_get_required_unum(&response->id, &response->response, &leaderboarddata_fields[0], "LBID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_num(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) + if (!rc_json_get_required_unum(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) return RC_MISSING_VALUE; if (!rc_json_get_required_string(&response->title, &response->response, &leaderboarddata_fields[3], "LBTitle")) return RC_MISSING_VALUE; @@ -218,14 +246,18 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info response->format = RC_FORMAT_VALUE; } - if (!rc_json_get_required_array(&response->num_entries, &iterator, &response->response, &leaderboarddata_fields[10], "Entries")) + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &leaderboarddata_fields[10], "Entries")) return RC_MISSING_VALUE; if (response->num_entries) { - response->entries = (rc_api_lboard_info_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); + response->entries = (rc_api_lboard_info_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); if (!response->entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -252,7 +284,7 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info } void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch Games List --- */ @@ -270,13 +302,25 @@ int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_games_list_server_response(response, &response_obj); +} + +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { rc_api_game_list_entry_t* entry; - rc_json_object_field_iterator_t iterator; + rc_json_iterator_t iterator; + rc_json_field_t field; int result; char* end; @@ -287,9 +331,9 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -300,21 +344,22 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* } response->num_entries = fields[2].array_size; - rc_buf_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); + rc_buffer_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); - response->entries = (rc_api_game_list_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); + response->entries = (rc_api_game_list_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); if (!response->entries) return RC_OUT_OF_MEMORY; memset(&iterator, 0, sizeof(iterator)); iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; entry = response->entries; - while (rc_json_get_next_object_field(&iterator)) { - entry->id = strtol(iterator.field.name, &end, 10); + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->id = strtol(field.name, &end, 10); - iterator.field.name = ""; - if (!rc_json_get_string(&entry->name, &response->response.buffer, &iterator.field, "")) + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) return RC_MISSING_VALUE; ++entry; @@ -324,5 +369,5 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* } void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } diff --git a/deps/rcheevos/src/rapi/rc_api_runtime.c b/deps/rcheevos/src/rapi/rc_api_runtime.c index 43e72b79349b..27aeac13ded1 100644 --- a/deps/rcheevos/src/rapi/rc_api_runtime.c +++ b/deps/rcheevos/src/rapi/rc_api_runtime.c @@ -3,7 +3,7 @@ #include "rc_runtime.h" #include "rc_runtime_types.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include "../rhash/md5.h" #include @@ -24,11 +24,22 @@ int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_res rc_url_builder_append_str_param(&builder, "r", "gameid"); rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_resolve_hash_server_response(response, &response_obj); +} + +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), @@ -37,9 +48,9 @@ int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* respons }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -48,7 +59,7 @@ int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* respons } void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch Game Data --- */ @@ -65,21 +76,33 @@ int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_ if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_game_data_server_response(response, &response_obj); +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_definition_t* achievement; rc_api_leaderboard_definition_t* leaderboard; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; const char* last_author = ""; const char* last_author_field = ""; size_t last_author_len = 0; size_t len; - unsigned timet; + uint32_t timet; int result; char format[16]; @@ -127,9 +150,9 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -168,21 +191,25 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* leaderboards */ patchdata_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); - rc_buf_reserve(&response->response.buffer, len); + rc_buffer_reserve(&response->response.buffer, len); /* end estimation */ rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[4], "RichPresencePatch", ""); if (!response->rich_presence_script) response->rich_presence_script = ""; - if (!rc_json_get_required_array(&response->num_achievements, &iterator, &response->response, &patchdata_fields[5], "Achievements")) + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements")) return RC_MISSING_VALUE; if (response->num_achievements) { - response->achievements = (rc_api_achievement_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); + response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); if (!response->achievements) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + achievement = response->achievements; while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID")) @@ -224,14 +251,18 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r } } - if (!rc_json_get_required_array(&response->num_leaderboards, &iterator, &response->response, &patchdata_fields[6], "Leaderboards")) + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards")) return RC_MISSING_VALUE; if (response->num_leaderboards) { - response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); if (!response->leaderboards) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + leaderboard = response->leaderboards; while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID")) @@ -242,8 +273,10 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r return RC_MISSING_VALUE; if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem")) return RC_MISSING_VALUE; - rc_json_get_optional_bool(&leaderboard->lower_is_better, &leaderboard_fields[5], "LowerIsBetter", 0); - rc_json_get_optional_bool(&leaderboard->hidden, &leaderboard_fields[6], "Hidden", 0); + rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0); + leaderboard->lower_is_better = (uint8_t)result; + rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0); + leaderboard->hidden = (uint8_t)result; if (!leaderboard_fields[4].value_end) return RC_MISSING_VALUE; @@ -265,7 +298,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r } void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Ping --- */ @@ -286,25 +319,36 @@ int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_reques rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_ping_server_response(response, &response_obj); +} + +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); } void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Award Achievement --- */ @@ -335,29 +379,41 @@ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_ap snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0); md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); md5_finish(&md5, digest); - rc_api_format_md5(buffer, digest); + rc_format_md5(buffer, digest); rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_award_achievement_server_response(response, &response_obj); +} + +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("AchievementID"), RC_JSON_NEW_FIELD("AchievementsRemaining") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -375,14 +431,15 @@ int rc_api_process_award_achievement_response(rc_api_award_achievement_response_ } rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); - rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[3], "AchievementID", 0); - rc_json_get_optional_unum(&response->achievements_remaining, &fields[4], "AchievementsRemaining", (unsigned)-1); + rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1); return RC_OK; } void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Submit Leaderboard Entry --- */ @@ -414,18 +471,30 @@ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_ snprintf(buffer, sizeof(buffer), "%d", api_params->score); md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); md5_finish(&md5, digest); - rc_api_format_md5(buffer, digest); + rc_format_md5(buffer, digest); rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_submit_lboard_entry_server_response(response, &response_obj); +} + +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; int result; @@ -476,9 +545,9 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -497,14 +566,18 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo return RC_MISSING_VALUE; response->num_entries = (unsigned)atoi(str); - if (!rc_json_get_required_array(&response->num_top_entries, &iterator, &response->response, &response_fields[3], "TopEntries")) + if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries")) return RC_MISSING_VALUE; if (response->num_top_entries) { - response->top_entries = (rc_api_lboard_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); + response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); if (!response->top_entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->top_entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -524,5 +597,5 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo } void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } diff --git a/deps/rcheevos/src/rapi/rc_api_user.c b/deps/rcheevos/src/rapi/rc_api_user.c index 546a8a057806..0fa279c48e14 100644 --- a/deps/rcheevos/src/rapi/rc_api_user.c +++ b/deps/rcheevos/src/rapi/rc_api_user.c @@ -1,7 +1,7 @@ #include "rc_api_user.h" #include "rc_api_common.h" -#include "../rcheevos/rc_version.h" +#include "../rc_version.h" #include @@ -16,7 +16,7 @@ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_requ return RC_INVALID_STATE; rc_url_builder_init(&builder, &request->buffer, 48); - rc_url_builder_append_str_param(&builder, "r", "login"); + rc_url_builder_append_str_param(&builder, "r", "login2"); rc_url_builder_append_str_param(&builder, "u", api_params->username); if (api_params->password && api_params->password[0]) @@ -27,44 +27,58 @@ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_requ return RC_INVALID_STATE; request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_login_server_response(response, &response_obj); +} + +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), RC_JSON_NEW_FIELD("User"), RC_JSON_NEW_FIELD("Token"), RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("Messages"), RC_JSON_NEW_FIELD("DisplayName") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_string(&response->username, &response->response, &fields[2], "User")) + if (!rc_json_get_required_string(&response->username, &response->response, &fields[3], "User")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[3], "Token")) + if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[4], "Token")) return RC_MISSING_VALUE; - rc_json_get_optional_unum(&response->score, &fields[4], "Score", 0); - rc_json_get_optional_unum(&response->num_unread_messages, &fields[5], "Messages", 0); + rc_json_get_optional_unum(&response->score, &fields[5], "Score", 0); + rc_json_get_optional_unum(&response->score_softcore, &fields[6], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[7], "Messages", 0); - rc_json_get_optional_string(&response->display_name, &response->response, &fields[6], "DisplayName", response->username); + rc_json_get_optional_string(&response->display_name, &response->response, &fields[8], "DisplayName", response->username); return RC_OK; } void rc_api_destroy_login_response(rc_api_login_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Start Session --- */ @@ -78,37 +92,103 @@ int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_st return RC_INVALID_STATE; rc_url_builder_init(&builder, &request->buffer, 48); - if (rc_api_url_build_dorequest(&builder, "postactivity", api_params->username, api_params->api_token)) { - /* activity type enum (only 3 is used ) - * 1 = earned achievement - handled by awardachievement - * 2 = logged in - handled by login - * 3 = started playing - * 4 = uploaded achievement - handled by uploadachievement - * 5 = modified achievement - handled by uploadachievement - */ - rc_url_builder_append_unum_param(&builder, "a", 3); - rc_url_builder_append_unum_param(&builder, "m", api_params->game_id); + if (rc_api_url_build_dorequest(&builder, "startsession", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_start_session_server_response(response, &response_obj); +} + +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_unlock_entry_t* unlock; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; + int result; + rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error") + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Unlocks"), + RC_JSON_NEW_FIELD("HardcoreUnlocks"), + RC_JSON_NEW_FIELD("ServerNow") + }; + + rc_json_field_t unlock_entry_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("When") }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (rc_json_get_optional_array(&response->num_unlocks, &array_field, &response->response, &fields[2], "Unlocks") && response->num_unlocks) { + response->unlocks = (rc_api_unlock_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + if (rc_json_get_optional_array(&response->num_hardcore_unlocks, &array_field, &response->response, &fields[3], "HardcoreUnlocks") && response->num_hardcore_unlocks) { + response->hardcore_unlocks = (rc_api_unlock_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_hardcore_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->hardcore_unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->hardcore_unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + rc_json_get_optional_unum(&timet, &fields[4], "ServerNow", 0); + response->server_now = (time_t)timet; + + return RC_OK; } void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } /* --- Fetch User Unlocks --- */ @@ -123,12 +203,23 @@ int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_a rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_user_unlocks_server_response(response, &response_obj); +} + +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), @@ -141,9 +232,9 @@ int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_respons }; memset(response, 0, sizeof(*response)); - rc_buf_init(&response->response.buffer); + rc_buffer_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -152,5 +243,5 @@ int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_respons } void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { - rc_buf_destroy(&response->response.buffer); + rc_buffer_destroy(&response->response.buffer); } diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c new file mode 100644 index 000000000000..98d8c1fe8590 --- /dev/null +++ b/deps/rcheevos/src/rc_client.c @@ -0,0 +1,5097 @@ +#include "rc_client_internal.h" + +#include "rc_api_info.h" +#include "rc_api_runtime.h" +#include "rc_api_user.h" +#include "rc_consoles.h" +#include "rc_hash.h" + +#include "rapi/rc_api_common.h" + +#include "rcheevos/rc_internal.h" + +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif + +#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 +#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +enum { + RC_CLIENT_ASYNC_NOT_ABORTED = 0, + RC_CLIENT_ASYNC_ABORTED = 1, + RC_CLIENT_ASYNC_DESTROYED = 2 +}; + +typedef struct rc_client_generic_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_generic_callback_data_t; + +typedef struct rc_client_pending_media_t +{ + const char* file_path; + uint8_t* data; + size_t data_size; + rc_client_callback_t callback; + void* callback_userdata; +} rc_client_pending_media_t; + +typedef struct rc_client_load_state_t +{ + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + + rc_client_game_info_t* game; + rc_client_subset_info_t* subset; + rc_client_game_hash_t* hash; + + rc_hash_iterator_t hash_iterator; + rc_client_pending_media_t* pending_media; + + rc_api_start_session_response_t *start_session_response; + + rc_client_async_handle_t async_handle; + + uint8_t progress; + uint8_t outstanding_requests; + uint8_t hash_console_id; +} rc_client_load_state_t; + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when); +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +/* ===== Construction/Destruction ===== */ + +static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ +} + +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function) +{ + rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t)); + if (!client) + return NULL; + + client->state.hardcore = 1; + + client->callbacks.read_memory = read_memory_function; + client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_dummy_event_handler; + rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); + rc_client_set_get_time_millisecs_function(client, NULL); + + rc_mutex_init(&client->state.mutex); + + rc_buffer_init(&client->state.buffer); + + return client; +} + +void rc_client_destroy(rc_client_t* client) +{ + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + { + size_t i; + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i]) + client->state.async_handles[i]->aborted = RC_CLIENT_ASYNC_DESTROYED; + } + + if (client->state.load) { + client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_DESTROYED; + client->state.load = NULL; + } + } + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + rc_buffer_destroy(&client->state.buffer); + + rc_mutex_destroy(&client->state.mutex); + + free(client); +} + +/* ===== Logging ===== */ + +static rc_client_t* g_hash_client = NULL; + +static void rc_client_log_hash_message(const char* message) { + rc_client_log_message(g_hash_client, message); +} + +void rc_client_log_message(const rc_client_t* client, const char* message) +{ + if (client->callbacks.log_call) + client->callbacks.log_call(message, client); +} + +static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args) +{ + if (client->callbacks.log_call) { + char buffer[2048]; + +#ifdef __STDC_WANT_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + vsprintf(buffer, format, args); +#endif + + client->callbacks.log_call(buffer, client); + } +} + +#ifdef RC_NO_VARIADIC_MACROS + +void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +#else + +void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...) +{ + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); +} + +#endif /* RC_NO_VARIADIC_MACROS */ + +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + client->callbacks.log_call = callback; + client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE; +} + +/* ===== Common ===== */ + +static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + + /* round nanoseconds to nearest millisecond and add to seconds */ + return ((rc_clock_t)now.tv_sec * 1000 + ((rc_clock_t)now.tv_nsec / 1000000)); +#elif defined(_WIN32) + static LARGE_INTEGER freq; + LARGE_INTEGER ticks; + + /* Frequency is the number of ticks per second and is guaranteed to not change. */ + if (!freq.QuadPart) { + if (!QueryPerformanceFrequency(&freq)) + return 0; + + /* convert to number of ticks per millisecond to simplify later calculations */ + freq.QuadPart /= 1000; + } + + if (!QueryPerformanceCounter(&ticks)) + return 0; + + return (rc_clock_t)(ticks.QuadPart / freq.QuadPart); +#else + const clock_t clock_now = clock(); + if (sizeof(clock_t) == 4) { + static uint32_t clock_wraps = 0; + static clock_t last_clock = 0; + static time_t last_timet = 0; + const time_t time_now = time(NULL); + + if (last_timet != 0) { + const time_t seconds_per_clock_t = (time_t)(((uint64_t)1 << 32) / CLOCKS_PER_SEC); + if (clock_now < last_clock) { + /* clock() has wrapped */ + ++clock_wraps; + } + else if (time_now - last_timet > seconds_per_clock_t) { + /* it's been long enough that clock() has wrapped and is higher than the last time it was read */ + ++clock_wraps; + } + } + + last_timet = time_now; + last_clock = clock_now; + + return (rc_clock_t)((((uint64_t)clock_wraps << 32) | clock_now) / (CLOCKS_PER_SEC / 1000)); + } + else { + return (rc_clock_t)(clock_now / (CLOCKS_PER_SEC / 1000)); + } +#endif +} + +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler) +{ + client->callbacks.get_time_millisecs = handler ? handler : rc_client_clock_get_now_millisecs; +} + +static void rc_client_begin_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + size_t i; + + rc_mutex_lock(&client->state.mutex); + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (!client->state.async_handles[i]) { + client->state.async_handles[i] = async_handle; + break; + } + } + rc_mutex_unlock(&client->state.mutex); +} + +static int rc_client_end_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted = async_handle->aborted; + + /* if client was destroyed, mutex doesn't exist and we don't need to remove the handle from the collection */ + if (aborted != RC_CLIENT_ASYNC_DESTROYED) { + size_t i; + + rc_mutex_lock(&client->state.mutex); + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i] == async_handle) { + client->state.async_handles[i] = NULL; + break; + } + } + aborted = async_handle->aborted; + + rc_mutex_unlock(&client->state.mutex); + } + + return aborted; +} + +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + if (async_handle && client) { + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = RC_CLIENT_ASYNC_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } +} + +static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) +{ + if (!response->succeeded) { + if (*result == RC_OK) { + *result = RC_API_FAILURE; + if (!response->error_message) + return "Unexpected API failure with no error message"; + } + + if (response->error_message) + return response->error_message; + } + + if (*result != RC_OK) + return rc_error_str(*result); + + return NULL; +} + +static void rc_client_raise_server_error_event(rc_client_t* client, + const char* api, uint32_t related_id, int result, const char* error_message) +{ + rc_client_server_error_t server_error; + rc_client_event_t client_event; + + server_error.api = api; + server_error.error_message = error_message; + server_error.result = result; + server_error.related_id = related_id; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_SERVER_ERROR; + client_event.server_error = &server_error; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_update_disconnect_state(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + uint8_t new_state = RC_CLIENT_DISCONNECT_HIDDEN; + + rc_mutex_lock(&client->state.mutex); + + scheduled_callback = client->state.scheduled_callbacks; + for (; scheduled_callback; scheduled_callback = scheduled_callback->next) { + if (scheduled_callback->callback == rc_client_award_achievement_retry || + scheduled_callback->callback == rc_client_submit_leaderboard_entry_retry) { + new_state = RC_CLIENT_DISCONNECT_VISIBLE; + break; + } + } + + if ((client->state.disconnect & RC_CLIENT_DISCONNECT_VISIBLE) != new_state) { + if (new_state == RC_CLIENT_DISCONNECT_VISIBLE) + client->state.disconnect = RC_CLIENT_DISCONNECT_HIDDEN | RC_CLIENT_DISCONNECT_SHOW_PENDING; + else + client->state.disconnect = RC_CLIENT_DISCONNECT_VISIBLE | RC_CLIENT_DISCONNECT_HIDE_PENDING; + } + else { + client->state.disconnect = new_state; + } + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_raise_disconnect_events(rc_client_t* client) +{ + rc_client_event_t client_event; + uint8_t new_state; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.disconnect & RC_CLIENT_DISCONNECT_SHOW_PENDING) + new_state = RC_CLIENT_DISCONNECT_VISIBLE; + else + new_state = RC_CLIENT_DISCONNECT_HIDDEN; + client->state.disconnect = new_state; + + rc_mutex_unlock(&client->state.mutex); + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = (new_state == RC_CLIENT_DISCONNECT_VISIBLE) ? + RC_CLIENT_EVENT_DISCONNECTED : RC_CLIENT_EVENT_RECONNECTED; + client->callbacks.event_handler(&client_event, client); +} + +static int rc_client_should_retry(const rc_api_server_response_t* server_response) +{ + switch (server_response->http_status_code) { + case 502: /* 502 Bad Gateway */ + /* nginx connection pool full */ + return 1; + + case 503: /* 503 Service Temporarily Unavailable */ + /* site is in maintenance mode */ + return 1; + + case 504: /* 504 Gateway Timeout */ + /* timeout between web server and database server */ + return 1; + + case 429: /* 429 Too Many Requests */ + /* too many unlocks occurred at the same time */ + return 1; + + case 521: /* 521 Web Server is Down */ + /* cloudfare could not find the server */ + return 1; + + case 522: /* 522 Connection Timed Out */ + /* timeout connecting to server from cloudfare */ + return 1; + + case 523: /* 523 Origin is Unreachable */ + /* cloudfare cannot find server */ + return 1; + + case 524: /* 524 A Timeout Occurred */ + /* connection to server from cloudfare was dropped before request was completed */ + return 1; + + case RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR: + /* client provided non-HTTP error (explicitly retryable) */ + return 1; + + case RC_API_SERVER_RESPONSE_CLIENT_ERROR: + /* client provided non-HTTP error (implicitly non-retryable) */ + return 0; + + default: + /* assume any error not handled above where no response was received should be retried */ + if (server_response->body_length == 0 || !server_response->body || !server_response->body[0]) + return 1; + + return 0; + } +} + +static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + if (!buffer) + return RC_INVALID_STATE; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result == RC_OK) + snprintf(buffer, buffer_size, "%s", request.url); + + rc_api_destroy_request(&request); + return result; +} + +/* ===== User ===== */ + +static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data; + rc_client_t* client = login_callback_data->client; + rc_api_login_response_t login_response; + rc_client_load_state_t* load_state; + const char* error_message; + int result; + + result = rc_client_end_async(client, &login_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + rc_client_logout(client); /* logout will reset the user state and call the load game callback */ + + free(login_callback_data); + return; + } + + if (client->state.user == RC_CLIENT_USER_STATE_NONE) { + /* logout was called */ + if (login_callback_data->callback) + login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata); + + free(login_callback_data); + /* logout call will immediately abort load game before this callback gets called */ + return; + } + + result = rc_api_process_login_server_response(&login_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response); + if (error_message) { + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_NONE; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message); + if (login_callback_data->callback) + login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + } + else { + client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username); + + if (strcmp(login_response.username, login_response.display_name) == 0) + client->user.display_name = client->user.username; + else + client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name); + + client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token); + client->user.score = login_response.score; + client->user.score_softcore = login_response.score_softcore; + client->user.num_unread_messages = login_response.num_unread_messages; + + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + + if (login_callback_data->callback) + login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); + } + + rc_api_destroy_login_response(&login_response); + free(login_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, + const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_generic_callback_data_t* callback_data; + rc_api_request_t request; + int result = rc_api_init_login_request(&request, login_request); + const char* error_message = rc_error_str(result); + + if (result == RC_OK) { + rc_mutex_lock(&client->state.mutex); + + if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) { + error_message = "Login already in progress"; + result = RC_INVALID_STATE; + } + client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + rc_mutex_unlock(&client->state.mutex); + } + + if (result != RC_OK) { + callback(result, error_message, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!password || !password[0]) { + callback(RC_INVALID_STATE, "password is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.password = password; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!token || !token[0]) { + callback(RC_INVALID_STATE, "token is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.api_token = token; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +void rc_client_logout(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + if (!client) + return; + + switch (client->state.user) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name); + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + RC_CLIENT_LOG_INFO(client, "Aborting login"); + break; + } + + rc_mutex_lock(&client->state.mutex); + + client->state.user = RC_CLIENT_USER_STATE_NONE; + memset(&client->user, 0, sizeof(client->user)); + + load_state = client->state.load; + + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_load_error(load_state, RC_ABORTED, "Login aborted"); +} + +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) +{ + return (client && client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; +} + +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size) +{ + if (!user) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); +} + +static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset, + rc_client_user_game_summary_t* summary, const uint8_t unlock_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + switch (achievement->public_.category) { + case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + ++summary->num_core_achievements; + summary->points_core += achievement->public_.points; + + if (achievement->public_.unlocked & unlock_bit) { + ++summary->num_unlocked_achievements; + summary->points_unlocked += achievement->public_.points; + } + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { + ++summary->num_unsupported_achievements; + } + + break; + + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + ++summary->num_unofficial_achievements; + break; + + default: + continue; + } + } +} + +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) +{ + const uint8_t unlock_bit = (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + if (!summary) + return; + + memset(summary, 0, sizeof(*summary)); + if (!client || !client->game) + return; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit); + + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ +} + +/* ===== Game ===== */ + +static void rc_client_free_game(rc_client_game_info_t* game) +{ + rc_runtime_destroy(&game->runtime); + + rc_buffer_destroy(&game->buffer); + + free(game); +} + +static void rc_client_free_load_state(rc_client_load_state_t* load_state) +{ + if (load_state->game) + rc_client_free_game(load_state->game); + + if (load_state->start_session_response) { + rc_api_destroy_start_session_response(load_state->start_session_response); + free(load_state->start_session_response); + } + + free(load_state); +} + +static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests) +{ + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = state; + load_state->outstanding_requests += num_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); +} + +static int rc_client_end_load_state(rc_client_load_state_t* load_state) +{ + int remaining_requests = 0; + int aborted = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + if (load_state->outstanding_requests > 0) + --load_state->outstanding_requests; + remaining_requests = load_state->outstanding_requests; + + if (load_state->client->state.load != load_state) + aborted = 1; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (aborted) { + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. As they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) { + /* if one of the callbacks called rc_client_load_error, progress will be set to + * RC_CLIENT_LOAD_STATE_UNKNOWN. There's no need to call the callback with RC_ABORTED + * in that case, as it will have already been called with something more appropriate. */ + if (load_state->progress != RC_CLIENT_LOAD_STATE_UNKNOWN_GAME && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + } + + return -1; + } + + return remaining_requests; +} + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message) +{ + int remaining_requests = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + if (load_state->client->state.load == load_state) + load_state->client->state.load = NULL; + + remaining_requests = load_state->outstanding_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(load_state->client, "Load failed (%d): %s", result, error_message); + + if (load_state->callback) + load_state->callback(result, error_message, load_state->client, load_state->callback_userdata); + + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. as they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) + rc_client_free_load_state(load_state); +} + +static void rc_client_load_aborted(rc_client_load_state_t* load_state) +{ + /* prevent callback from being called when manually aborted */ + load_state->callback = NULL; + + /* mark the game as no longer being loaded */ + rc_client_load_error(load_state, RC_ABORTED, NULL); + + /* decrement the async counter and potentially free the load_state object */ + rc_client_end_load_state(load_state); +} + +static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(achievement->trigger, memref)) { + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + + if (achievement->trigger) + achievement->trigger->state = RC_TRIGGER_STATE_DISABLED; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address); + } + } + } +} + +static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_value_contains_memref(&leaderboard->lboard->value, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else + continue; + + if (leaderboard->lboard) + leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address); + } + } +} + +static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id); + const uint32_t max_address = (regions && regions->num_regions > 0) ? + regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF; + uint8_t buffer[8]; + uint32_t total_count = 0; + uint32_t invalid_count = 0; + + rc_memref_t** last_memref = &game->runtime.memrefs; + rc_memref_t* memref = game->runtime.memrefs; + for (; memref; memref = memref->next) { + if (!memref->value.is_indirect) { + total_count++; + + if (memref->address > max_address || + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + /* invalid address, remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch 0. */ + *last_memref = memref->next; + + rc_client_invalidate_memref_achievements(game, client, memref); + rc_client_invalidate_memref_leaderboards(game, client, memref); + + invalid_count++; + continue; + } + } + + last_memref = &memref->next; + } + + game->max_valid_address = max_address; + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count); +} + +static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_runtime_trigger_t* trigger; + rc_client_subset_info_t* subset; + + if (active_count <= game->runtime.trigger_capacity) { + if (active_count != 0) + memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t)); + } else { + if (game->runtime.triggers) + free(game->runtime.triggers); + + game->runtime.trigger_capacity = active_count; + game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t)); + } + + trigger = game->runtime.triggers; + if (!trigger) { + /* malloc failed, no way to report error, just bail */ + game->runtime.trigger_count = 0; + return; + } + + for (subset = game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + trigger->id = achievement->public_.id; + memcpy(trigger->md5, achievement->md5, 16); + trigger->trigger = achievement->trigger; + ++trigger; + } + } + } + } + + game->runtime.trigger_count = active_count; +} + +static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + ++active_count; + } + + return active_count; +} + +void rc_client_update_active_achievements(rc_client_game_info_t* game) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_count_active_achievements(subset); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if ((achievement->public_.unlocked & active_bit) == 0) { + switch (achievement->public_.state) { + case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: + rc_reset_trigger(achievement->trigger); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + ++active_count; + break; + + case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: + ++active_count; + break; + } + } + else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + + /* if it's active despite being unlocked, and we're in encore mode, leave it active */ + if (client->state.encore_mode) { + ++active_count; + continue; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? + achievement->unlock_time_hardcore : achievement->unlock_time_softcore; + + if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client_event.achievement = &achievement->public_; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state)) + achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; + } + } + + return active_count; +} + +static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client) +{ + const uint8_t active_bit = (client->state.encore_mode) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_client_toggle_hardcore_achievements(game, client, active_bit); +} + +static void rc_client_update_legacy_runtime_leaderboards(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_subset_info_t* subset; + rc_runtime_lboard_t* lboard; + + if (active_count <= game->runtime.lboard_capacity) { + if (active_count != 0) + memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t)); + } else { + if (game->runtime.lboards) + free(game->runtime.lboards); + + game->runtime.lboard_capacity = active_count; + game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t)); + } + + lboard = game->runtime.lboards; + if (!lboard) { + /* malloc failed. no way to report error, just bail */ + game->runtime.lboard_count = 0; + return; + } + + for (subset = game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + lboard->id = leaderboard->public_.id; + memcpy(lboard->md5, leaderboard->md5, 16); + lboard->lboard = leaderboard->lboard; + ++lboard; + } + } + } + } + + game->runtime.lboard_count = active_count; +} + +void rc_client_update_active_leaderboards(rc_client_game_info_t* game) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) + { + switch (leaderboard->public_.state) + { + case RC_CLIENT_LEADERBOARD_STATE_ACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + ++active_count; + break; + } + } + } + + rc_client_update_legacy_runtime_leaderboards(game, active_count); +} + +static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + if (client->state.hardcore) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (client->state.hardcore) + ++active_count; + else + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + rc_client_update_legacy_runtime_leaderboards(game, active_count); +} + +static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(client->game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + game->runtime.lboard_count = 0; +} + +static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, rc_api_unlock_entry_t* unlocks, uint32_t num_unlocks, uint8_t mode) +{ + rc_client_achievement_info_t* start = subset->achievements; + rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; + rc_client_achievement_info_t* scan; + rc_api_unlock_entry_t* unlock = unlocks; + rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks; + + for (; unlock < unlock_stop; ++unlock) { + for (scan = start; scan < stop; ++scan) { + if (scan->public_.id == unlock->achievement_id) { + scan->public_.unlocked |= mode; + + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) + scan->unlock_time_hardcore = unlock->when; + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE) + scan->unlock_time_softcore = unlock->when; + + if (scan == start) + ++start; + else if (scan + 1 == stop) + --stop; + break; + } + } + } +} + +static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response) +{ + rc_client_t* client = load_state->client; + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_STATE_DONE : RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + /* unlocks not available - assume malloc failed */ + if (load_state->callback) + load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); + } + else { + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks, + start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks, + start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load == NULL) + client->game = load_state->game; + rc_mutex_unlock(&client->state.mutex); + + if (client->game != load_state->game) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* if a change media request is pending, kick it off */ + rc_client_pending_media_t* pending_media; + + rc_mutex_lock(&load_state->client->state.mutex); + pending_media = load_state->pending_media; + load_state->pending_media = NULL; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (pending_media) { + rc_client_begin_change_media(client, pending_media->file_path, + pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + /* client->game must be set before calling this function so it can query the console_id */ + rc_client_validate_addresses(load_state->game, client); + + rc_client_activate_achievements(load_state->game, client); + rc_client_activate_leaderboards(load_state->game, client); + + if (load_state->hash->hash[0] != '[') { + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { + /* schedule the periodic ping */ + rc_client_scheduled_callback_data_t* callback_data = rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(callback_data, 0, sizeof(*callback_data)); + callback_data->callback = rc_client_ping; + callback_data->related_id = load_state->game->public_.id; + callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000; + rc_client_schedule_callback(client, callback_data); + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcore %s%s", load_state->game->public_.id, + client->state.hardcore ? "enabled" : "disabled", + (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id); + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + + /* detach the game object so it doesn't get freed by free_load_state */ + load_state->game = NULL; + } + } + + rc_client_free_load_state(load_state); +} + +static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_start_session_response_t start_session_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(load_state->client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_start_session_server_response(&start_session_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else if (outstanding_requests == 0) { + rc_client_activate_game(load_state, &start_session_response); + } + else { + load_state->start_session_response = + (rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t)); + + if (!load_state->start_session_response) { + rc_client_load_error(callback_data, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + } + else { + /* safer to parse the response again than to try to copy it */ + rc_api_process_start_session_response(load_state->start_session_response, server_response->body); + } + } + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void rc_client_begin_start_session(rc_client_load_state_t* load_state) +{ + rc_api_start_session_request_t start_session_params; + rc_client_t* client = load_state->client; + rc_api_request_t start_session_request; + int result; + + memset(&start_session_params, 0, sizeof(start_session_params)); + start_session_params.username = client->user.username; + start_session_params.api_token = client->user.token; + start_session_params.game_id = load_state->hash->game_id; + + result = rc_api_init_start_session_request(&start_session_request, &start_session_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); + rc_api_destroy_request(&start_session_request); + } +} + +static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements) +{ + const rc_api_achievement_definition_t* read; + const rc_api_achievement_definition_t* stop; + rc_client_achievement_info_t* achievements; + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* scan; + rc_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + size_t size; + int trigger_size; + + subset->achievements = NULL; + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + + stop = achievement_definitions + num_achievements; + + /* if not testing unofficial, filter them out */ + if (!load_state->client->state.unofficial_enabled) { + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) + --num_achievements; + } + + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + } + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */ + + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */ + + sizeof(rc_client_achievement_info_t); + rc_buffer_reserve(&load_state->game->buffer, size * num_achievements); + + /* allocate the achievement array */ + size = sizeof(rc_client_achievement_info_t) * num_achievements; + buffer = &load_state->game->buffer; + achievement = achievements = rc_buffer_alloc(buffer, size); + memset(achievements, 0, size); + + /* copy the achievement data */ + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled) + continue; + + achievement->public_.title = rc_buffer_strcpy(buffer, read->title); + achievement->public_.description = rc_buffer_strcpy(buffer, read->description); + snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name); + achievement->public_.id = read->id; + achievement->public_.points = read->points; + achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ? + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, achievement->md5); + + trigger_size = rc_trigger_size(memaddr); + if (trigger_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buffer_reserve(buffer, trigger_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + achievement->trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset); + achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + achievement->created_time = read->created; + achievement->updated_time = read->updated; + + scan = achievement; + while (scan > achievements) { + --scan; + if (strcmp(scan->author, read->author) == 0) { + achievement->author = scan->author; + break; + } + } + if (!achievement->author) + achievement->author = rc_buffer_strcpy(buffer, read->author); + + ++achievement; + } + + subset->achievements = achievements; +} + +uint8_t rc_client_map_leaderboard_format(int format) +{ + switch (format) { + case RC_FORMAT_SECONDS: + case RC_FORMAT_CENTISECS: + case RC_FORMAT_MINUTES: + case RC_FORMAT_SECONDS_AS_MINUTES: + case RC_FORMAT_FRAMES: + return RC_CLIENT_LEADERBOARD_FORMAT_TIME; + + case RC_FORMAT_SCORE: + return RC_CLIENT_LEADERBOARD_FORMAT_SCORE; + + case RC_FORMAT_VALUE: + case RC_FORMAT_FLOAT1: + case RC_FORMAT_FLOAT2: + case RC_FORMAT_FLOAT3: + case RC_FORMAT_FLOAT4: + case RC_FORMAT_FLOAT5: + case RC_FORMAT_FLOAT6: + default: + return RC_CLIENT_LEADERBOARD_FORMAT_VALUE; + } +} + +static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards) +{ + const rc_api_leaderboard_definition_t* read; + const rc_api_leaderboard_definition_t* stop; + rc_client_leaderboard_info_t* leaderboards; + rc_client_leaderboard_info_t* leaderboard; + rc_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + const char* ptr; + size_t size; + int lboard_size; + + subset->leaderboards = NULL; + subset->public_.num_leaderboards = num_leaderboards; + + if (num_leaderboards == 0) + return; + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_lboard_t) /* lboard container */ + + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */ + + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */ + + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */ + + sizeof(rc_client_leaderboard_info_t); + rc_buffer_reserve(&load_state->game->buffer, size * num_leaderboards); + + /* allocate the achievement array */ + size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; + buffer = &load_state->game->buffer; + leaderboard = leaderboards = rc_buffer_alloc(buffer, size); + memset(leaderboards, 0, size); + + /* copy the achievement data */ + read = leaderboard_definitions; + stop = read + num_leaderboards; + do { + leaderboard->public_.title = rc_buffer_strcpy(buffer, read->title); + leaderboard->public_.description = rc_buffer_strcpy(buffer, read->description); + leaderboard->public_.id = read->id; + leaderboard->public_.format = rc_client_map_leaderboard_format(read->format); + leaderboard->public_.lower_is_better = read->lower_is_better; + leaderboard->format = (uint8_t)read->format; + leaderboard->hidden = (uint8_t)read->hidden; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, leaderboard->md5); + + ptr = strstr(memaddr, "VAL:"); + if (ptr != NULL) { + /* calculate the DJB2 hash of the VAL portion of the string*/ + uint32_t hash = 5381; + ptr += 4; /* skip 'VAL:' */ + while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) + hash = (hash << 5) + hash + *ptr++; + leaderboard->value_djb2 = hash; + } + + lboard_size = rc_lboard_size(memaddr); + if (lboard_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buffer_reserve(buffer, lboard_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset); + leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + ++leaderboard; + ++read; + } while (read < stop); + + subset->leaderboards = leaderboards; +} + +static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title) +{ + const char* subset_prefix = strstr(title, "[Subset - "); + if (subset_prefix) { + const char* start = subset_prefix + 10; + const char* stop = strstr(start, "]"); + const size_t len = stop - start; + char* result = (char*)rc_buffer_alloc(&game->buffer, len + 1); + + memcpy(result, start, len); + result[len] = '\0'; + return result; + } + + return NULL; +} + +static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_game_data_response_t fetch_game_data_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(load_state->client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(load_state, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = fetch_game_data_response.id; + subset->active = 1; + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", fetch_game_data_response.image_name); + load_state->subset = subset; + + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && + fetch_game_data_response.console_id != load_state->game->public_.console_id) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", + fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + } + + /* kick off the start session request while we process the game data */ + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + /* we can't unlock achievements without a session, lock spectator mode for the game */ + load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED; + } + else { + rc_client_begin_start_session(load_state); + } + + /* process the game data */ + rc_client_copy_achievements(load_state, subset, + fetch_game_data_response.achievements, fetch_game_data_response.num_achievements); + rc_client_copy_leaderboards(load_state, subset, + fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards); + + if (!load_state->game->subsets) { + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + load_state->game->subsets = subset; + load_state->game->public_.badge_name = subset->public_.badge_name; + load_state->game->public_.console_id = fetch_game_data_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); + + subset->public_.title = load_state->game->public_.title; + + if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); + } + } + } + else { + rc_client_subset_info_t* scan; + + /* subset - extract subset title */ + subset->public_.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title); + if (!subset->public_.title) { + const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public_.title); + if (core_subset_title) { + rc_client_subset_info_t* scan = load_state->game->subsets; + for (; scan; scan = scan->next) { + if (scan->public_.title == load_state->game->public_.title) { + scan->public_.title = core_subset_title; + break; + } + } + } + + subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + } + + /* append to subset list */ + scan = load_state->game->subsets; + while (scan->next) + scan = scan->next; + scan->next = subset; + } + + if (load_state->client->callbacks.post_process_game_data_response) { + load_state->client->callbacks.post_process_game_data_response(server_response, + &fetch_game_data_response, load_state->client, load_state->callback_userdata); + } + + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (outstanding_requests == 0) + rc_client_activate_game(load_state, load_state->start_session_response); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + + if (load_state->hash->game_id == 0) { + char hash[33]; + + if (rc_hash_iterate(hash, &load_state->hash_iterator)) { + /* found another hash to try */ + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + rc_client_load_game(load_state, hash, NULL); + return; + } + + if (load_state->game->media_hash && + load_state->game->media_hash->game_hash && + load_state->game->media_hash->game_hash->next) { + /* multiple hashes were tried, create a CSV */ + struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash; + int count = 1; + char* ptr; + size_t size; + + size = strlen(game_hash->hash) + 1; + while (game_hash->next) { + game_hash = game_hash->next; + size += strlen(game_hash->hash) + 1; + count++; + } + + ptr = (char*)rc_buffer_alloc(&load_state->game->buffer, size); + ptr += size - 1; + *ptr = '\0'; + game_hash = load_state->game->media_hash->game_hash; + do { + const size_t hash_len = strlen(game_hash->hash); + ptr -= hash_len; + memcpy(ptr, game_hash->hash, hash_len); + + game_hash = game_hash->next; + if (!game_hash) + break; + + ptr--; + *ptr = ','; + } while (1); + + load_state->game->public_.hash = ptr; + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + } else { + /* only a single hash was tried, capture it */ + load_state->game->public_.console_id = load_state->hash_console_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + load_state->game->public_.title = "Unknown Game"; + load_state->game->public_.badge_name = ""; + client->game = load_state->game; + load_state->game = NULL; + + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + + if (load_state->hash->hash[0] != '[') { + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + /* done with the hashing code, release the global pointer */ + g_hash_client = NULL; + + rc_mutex_lock(&client->state.mutex); + result = client->state.user; + if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + load_state->progress = RC_CLIENT_LOAD_STATE_AWAIT_LOGIN; + rc_mutex_unlock(&client->state.mutex); + + switch (result) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + /* do nothing, this function will be called again after login completes */ + return; + + default: + rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED)); + return; + } + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = client->user.username; + fetch_game_data_request.api_token = client->user.token; + fetch_game_data_request.game_id = load_state->hash->game_id; + + result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, 1); + + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id); + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + + rc_api_destroy_request(&request); +} + +static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (error_message) { + rc_client_end_load_state(load_state); + rc_client_load_error(load_state, result, error_message); + } + else { + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = resolve_hash_response.game_id; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + /* have to call end_load_state after updating hash in case the load_state gets free'd */ + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_begin_fetch_game_data(load_state); + } + } + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) +{ + rc_client_game_hash_t* game_hash; + + rc_mutex_lock(&client->state.mutex); + game_hash = client->hashes; + while (game_hash) { + if (strcasecmp(game_hash->hash, hash) == 0) + break; + + game_hash = game_hash->next; + } + + if (!game_hash) { + game_hash = rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); + memset(game_hash, 0, sizeof(*game_hash)); + snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash); + game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID; + game_hash->next = client->hashes; + client->hashes = game_hash; + } + rc_mutex_unlock(&client->state.mutex); + + return game_hash; +} + +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, + const char* hash, const char* file_path) +{ + rc_client_t* client = load_state->client; + rc_client_game_hash_t* old_hash; + + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + rc_buffer_init(&load_state->game->buffer); + rc_runtime_init(&load_state->game->runtime); + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + old_hash = load_state->hash; + load_state->hash = rc_client_find_game_hash(client, hash); + + if (file_path) { + rc_client_media_hash_t* media_hash = + (rc_client_media_hash_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(*media_hash)); + media_hash->game_hash = load_state->hash; + media_hash->path_djb2 = rc_djb2(file_path); + media_hash->next = load_state->game->media_hash; + load_state->game->media_hash = media_hash; + } + else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) { + load_state->game->media_hash->game_hash = load_state->hash; + } + + if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return NULL; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, 1); + + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client); + + rc_api_destroy_request(&request); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + rc_client_begin_fetch_game_data(load_state); + } + + return (client->state.load == load_state) ? &load_state->async_handle : NULL; +} + +rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) +{ + if (client && client->state.load) + return &client->state.load->hash_iterator; + + return NULL; +} + +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + return rc_client_load_game(load_state, hash, NULL); +} + +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + char hash[33]; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (data) { + if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data); + } + } + else if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path); + } + else { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (!file_path) + file_path = "?"; + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + if (console_id == RC_CONSOLE_UNKNOWN) { + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + + if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + } + else { + /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ + load_state->hash_console_id = console_id; + + if (data != NULL) { + if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + else { + if (!rc_hash_generate_from_file(hash, console_id, file_path)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + rc_client_subset_info_t* subset; + + for (subset = game->subsets; subset; subset = subset->next) { + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE && + achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } + + rc_client_hide_progress_tracker(client, game); +} + +void rc_client_unload_game(rc_client_t* client) +{ + rc_client_game_info_t* game; + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + game = client->game; + client->game = NULL; + + if (client->state.load) { + /* this mimics rc_client_abort_async without nesting the lock */ + client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED; + client->state.load = NULL; + } + + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) + client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; + + if (game != NULL) + rc_client_game_mark_ui_to_be_hidden(client, game); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; + + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) { + *last = next->next; + continue; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); + + if (game != NULL) { + rc_client_raise_pending_events(client, game); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id); + rc_client_free_game(game); + } +} + +static void rc_client_change_media(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + if (game_hash->game_id == client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash); + } + else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + RC_CLIENT_LOG_INFO(client, "Switching to unknown media"); + } + else if (game_hash->game_id == 0) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash); + } + + client->game->public_.hash = game_hash->hash; + callback(RC_OK, NULL, client, callback_userdata); +} + +static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + + int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + const int async_aborted = rc_client_end_async(client, &load_state->async_handle); + if (async_aborted) { + if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Media change aborted"); + /* if lookup succeeded, still capture the new hash */ + if (result == RC_OK) + load_state->hash->game_id = resolve_hash_response.game_id; + } + } + else if (client->game != load_state->game) { + /* loaded game changed. return success regardless of result */ + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (error_message) { + load_state->callback(result, error_message, client, load_state->callback_userdata); + } + else { + load_state->hash->game_id = resolve_hash_response.game_id; + + if (resolve_hash_response.game_id == 0 && client->state.hardcore) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", load_state->hash->hash); + rc_client_set_hardcore_enabled(client, 0); + client->game->public_.hash = load_state->hash->hash; /* do still update the loaded hash */ + load_state->callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, load_state->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + rc_client_change_media(client, load_state->hash, load_state->callback, load_state->callback_userdata); + } + } + + free(load_state); + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_game_hash_t* game_hash = NULL; + rc_client_media_hash_t* media_hash; + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + uint32_t path_djb2; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!data && !file_path) { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) { + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + pending_media->file_path = strdup(file_path); + pending_media->callback = callback; + pending_media->callback_userdata = callback_userdata; + if (data && data_size) { + pending_media->data_size = data_size; + pending_media->data = (uint8_t*)malloc(data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + memcpy(pending_media->data, data, data_size); + } + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return NULL; + } + + /* still waiting for game data */ + if (pending_media) + return NULL; + + /* check to see if we've already hashed this file */ + path_djb2 = rc_djb2(file_path); + rc_mutex_lock(&client->state.mutex); + for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) { + if (media_hash->path_djb2 == path_djb2) { + game_hash = media_hash->game_hash; + break; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!game_hash) { + char hash[33]; + int result; + + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + g_hash_client = client; + rc_hash_init_error_message_callback(rc_client_log_hash_message); + rc_hash_init_verbose_message_callback(rc_client_log_hash_message); + } + + if (data != NULL) + result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); + else + result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); + + g_hash_client = NULL; + + if (!result) { + /* when changing discs, if the disc is not supported by the system, allow it. this is + * primarily for games that support user-provided audio CDs, but does allow using discs + * from other systems for games that leverage user-provided discs. */ + strcpy_s(hash, sizeof(hash), "[NO HASH]"); + } + + game_hash = rc_client_find_game_hash(client, hash); + + media_hash = (rc_client_media_hash_t*)rc_buffer_alloc(&game->buffer, sizeof(*media_hash)); + media_hash->game_hash = game_hash; + media_hash->path_djb2 = path_djb2; + + rc_mutex_lock(&client->state.mutex); + media_hash->next = game->media_hash; + game->media_hash = media_hash; + rc_mutex_unlock(&client->state.mutex); + + if (!result) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + } + + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + else { + /* call the server to make sure the hash is valid for the loaded game */ + rc_client_load_state_t* callback_data; + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = game_hash->hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + callback(result, rc_error_str(result), client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client = client; + callback_data->hash = game_hash; + callback_data->game = game; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; + } +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + return (client && client->game) ? &client->game->public_ : NULL; +} + +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size) +{ + if (!game) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); +} + +/* ===== Subsets ===== */ + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata) +{ + char buffer[32]; + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return; + } + + if (!client->game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return; + } + + snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)subset_id); + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + load_state->game = client->game; + load_state->hash = rc_client_find_game_hash(client, buffer); + load_state->hash->game_id = subset_id; + client->state.load = load_state; + + rc_client_begin_fetch_game_data(load_state); +} + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->public_.id == subset_id) + return &subset->public_; + } + + return NULL; +} + +/* ===== Achievements ===== */ + +static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) +{ + uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN; + uint32_t new_measured_value = 0; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + return; + + achievement->public_.measured_progress[0] = '\0'; + + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { + /* achievement unlocked */ + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + } + else { + /* active achievement */ + new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + if (achievement->trigger) { + if (achievement->trigger->measured_target) { + if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) { + /* value hasn't been initialized yet, leave progress string empty */ + } + else if (achievement->trigger->measured_value == 0) { + /* value is 0, leave progress string empty. update progress to 0.0 */ + achievement->public_.measured_percent = 0.0; + } + else { + /* clamp measured value at target (can't get more than 100%) */ + new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ? + achievement->trigger->measured_target : achievement->trigger->measured_value; + + achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target; + + if (!achievement->trigger->measured_as_percent) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target); + } + else if (achievement->public_.measured_percent >= 1.0) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu%%", (unsigned long)achievement->public_.measured_percent); + } + } + } + + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE; + else if (achievement->public_.measured_percent >= 80.0) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE; + } + } + + if (new_bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED && achievement->public_.unlock_time >= recent_unlock_time) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + + achievement->public_.bucket = new_bucket; +} + +static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_achievement_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static int rc_client_compare_achievement_unlock_times(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + if (unlock_b->unlock_time == unlock_a->unlock_time) + return 0; + return (unlock_b->unlock_time < unlock_a->unlock_time) ? -1 : 1; +} + +static int rc_client_compare_achievement_progress(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + if (unlock_b->measured_percent == unlock_a->measured_percent) { + if (unlock_a->id == unlock_b->id) + return 0; + return (unlock_a->id < unlock_b->id) ? -1 : 1; + } + return (unlock_b->measured_percent < unlock_a->measured_percent) ? -1 : 1; +} + +static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) +{ + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { + switch (bucket) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + default: + return bucket; + } + } + + return bucket; +} + +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_client_achievement_t** bucket_achievements; + rc_client_achievement_t** achievement_ptr; + rc_client_achievement_bucket_t* bucket_ptr; + rc_client_achievement_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[16]; + uint32_t num_buckets; + uint32_t num_achievements; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED + }; + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_achievement_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + } + } + } + + num_buckets = 0; + num_achievements = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_achievements += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) { + ++num_buckets; + break; + } + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); + + list = (rc_client_achievement_list_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + bucket_ptr = list->buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + + if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) + qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE) + qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); + + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_has_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + result = 0; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + if (subset->public_.num_achievements > 0) { + result = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static const rc_client_achievement_t* rc_client_subset_get_achievement_info( + rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.id == id) { + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_mutex_lock((rc_mutex_t*)(&client->state.mutex)); + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex)); + return &achievement->public_; + } + } + + return NULL; +} + +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id); + if (achievement != NULL) + return achievement; + } + + return NULL; +} + +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) +{ + const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? + RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + if (!achievement || !achievement->badge_name[0]) + return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); +} + +typedef struct rc_client_award_achievement_callback_data_t +{ + uint32_t id; + uint32_t retry_count; + uint8_t hardcore; + const char* game_hash; + time_t unlock_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_award_achievement_callback_data_t; + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); + +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data->data; + + rc_client_award_achievement_server_call(ach_data); +} + +static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data; + rc_api_award_achievement_response_t award_achievement_response; + + int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response); + + if (error_message) { + if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, result, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message); + rc_client_award_achievement_server_call(ach_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (ach_data->retry_count > 8) ? 120 : (1 << (ach_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay); + + if (!ach_data->scheduled_callback_data) { + ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data)); + if (!ach_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry; + ach_data->scheduled_callback_data->data = ach_data; + ach_data->scheduled_callback_data->related_id = ach_data->id; + } + + ach_data->scheduled_callback_data->when = + ach_data->client->callbacks.get_time_millisecs(ach_data->client) + delay * 1000; + + rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data); + + rc_client_update_disconnect_state(ach_data->client); + return; + } + } + else { + ach_data->client->user.score = award_achievement_response.new_player_score; + ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore; + + if (award_achievement_response.awarded_achievement_id != ach_data->id) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message); + } + else { + if (award_achievement_response.response.error_message) { + /* previously unlocked achievements are returned as a success with an error message */ + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u", + ach_data->id, ach_data->retry_count + 1, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u", + ach_data->id, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + + if (award_achievement_response.achievements_remaining == 0) { + rc_client_subset_info_t* subset; + for (subset = ach_data->client->game->subsets; subset; subset = subset->next) { + if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE && + rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) { + if (subset->public_.id == ach_data->client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + + /* TODO: subset mastery notification */ + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + } + } + } + } + } + } + + if (ach_data->retry_count) + rc_client_update_disconnect_state(ach_data->client); + + if (ach_data->scheduled_callback_data) + free(ach_data->scheduled_callback_data); + free(ach_data); +} + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data) +{ + rc_api_award_achievement_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = ach_data->client->user.username; + api_params.api_token = ach_data->client->user.token; + api_params.achievement_id = ach_data->id; + api_params.hardcore = ach_data->hardcore; + api_params.game_hash = ach_data->game_hash; + + result = rc_api_init_award_achievement_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); + free(ach_data); + return; + } + + ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement) +{ + rc_client_award_achievement_callback_data_t* callback_data; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.hardcore) { + achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL); + if (achievement->unlock_time_softcore == 0) + achievement->unlock_time_softcore = achievement->unlock_time_hardcore; + + /* adjust score now - will get accurate score back from server */ + client->user.score += achievement->public_.points; + } + else { + achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL); + + /* adjust score now - will get accurate score back from server */ + client->user.score_softcore += achievement->public_.points; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlocked |= (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_unlock(&client->state.mutex); + + if (client->callbacks.can_submit_achievement_unlock && + !client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id); + return; + } + + /* can't unlock unofficial achievements on the server */ + if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + /* don't actually unlock achievements when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id); + rc_client_raise_server_error_event(client, "award_achievement", achievement->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = achievement->public_.id; + callback_data->hardcore = client->state.hardcore; + callback_data->game_hash = client->game->public_.hash; + callback_data->unlock_time = achievement->public_.unlock_time; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); + rc_client_award_achievement_server_call(callback_data); +} + +static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + + rc_reset_trigger(trigger); + } +} + +static void rc_client_reset_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_achievements(subset); +} + +/* ===== Leaderboards ===== */ + +static rc_client_leaderboard_info_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.id == id) + return leaderboard; + } + + return NULL; +} + +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_leaderboard_info_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id); + if (leaderboard != NULL) + return &leaderboard->public_; + } + + return NULL; +} + +static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive"; + case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active"; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_leaderboard_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping) +{ + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED; + + default: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + } +} + +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_leaderboard_t** bucket_leaderboards; + rc_client_leaderboard_t** leaderboard_ptr; + rc_client_leaderboard_bucket_t* bucket_ptr; + rc_client_leaderboard_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[8]; + uint32_t num_buckets; + uint32_t num_leaderboards; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ALL, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED + }; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_leaderboard_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->hidden) + continue; + + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + bucket_counts[leaderboard->bucket]++; + } + } + + num_buckets = 0; + num_leaderboards = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_leaderboards += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); + + list = (rc_client_leaderboard_list_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + bucket_ptr = list->buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_has_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + result = 0; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + if (subset->public_.num_leaderboards > 0) { + result = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker; + rc_client_leaderboard_tracker_info_t* available_tracker = NULL; + + for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) { + if (tracker->reference_count == 0) { + if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + available_tracker = tracker; + + continue; + } + + if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format) + continue; + + if (tracker->raw_value != leaderboard->value) { + /* if the value comes from tracking hits, we can't assume the trackers started in the + * same frame, so we can't share the tracker */ + if (tracker->value_from_hits) + continue; + + /* value has changed. prepare an update event */ + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } + + /* attach to the existing tracker */ + ++tracker->reference_count; + tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + leaderboard->tracker = tracker; + leaderboard->public_.tracker_value = tracker->public_.display; + return; + } + + if (!available_tracker) { + rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers; + + available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buffer_alloc(&game->buffer, sizeof(*available_tracker)); + memset(available_tracker, 0, sizeof(*available_tracker)); + available_tracker->public_.id = 1; + + for (tracker = *next; tracker; next = &tracker->next, tracker = *next) + available_tracker->public_.id++; + + *next = available_tracker; + } + + /* update the claimed tracker */ + available_tracker->reference_count = 1; + available_tracker->value_djb2 = leaderboard->value_djb2; + available_tracker->format = leaderboard->format; + available_tracker->raw_value = leaderboard->value; + available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW; + available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value); + leaderboard->tracker = available_tracker; + leaderboard->public_.tracker_value = available_tracker->public_.display; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; +} + +void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + leaderboard->tracker = NULL; + + if (tracker && --tracker->reference_count == 0) { + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + if (tracker && tracker->raw_value != leaderboard->value) { + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +typedef struct rc_client_submit_leaderboard_entry_callback_data_t +{ + uint32_t id; + int32_t score; + uint32_t retry_count; + const char* game_hash; + time_t submit_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_submit_leaderboard_entry_callback_data_t; + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data); + +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; + + rc_client_submit_leaderboard_entry_server_call(lboard_data); +} + +static void rc_client_raise_scoreboard_event(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data, + const rc_api_submit_lboard_entry_response_t* response) +{ + rc_client_leaderboard_scoreboard_t sboard; + rc_client_event_t client_event; + rc_client_subset_info_t* subset; + rc_client_t* client = lboard_data->client; + rc_client_leaderboard_info_t* leaderboard = NULL; + + if (!client || !client->game) + return; + + for (subset = client->game->subsets; subset; subset = subset->next) { + leaderboard = rc_client_subset_get_leaderboard_info(subset, lboard_data->id); + if (leaderboard != NULL) + break; + } + if (leaderboard == NULL) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Trying to raise scoreboard for unknown leaderboard %u", lboard_data->id); + return; + } + + memset(&sboard, 0, sizeof(sboard)); + sboard.leaderboard_id = lboard_data->id; + rc_format_value(sboard.submitted_score, sizeof(sboard.submitted_score), response->submitted_score, leaderboard->format); + rc_format_value(sboard.best_score, sizeof(sboard.best_score), response->best_score, leaderboard->format); + sboard.new_rank = response->new_rank; + sboard.num_entries = response->num_entries; + sboard.num_top_entries = response->num_top_entries; + if (sboard.num_top_entries > 0) { + sboard.top_entries = (rc_client_leaderboard_scoreboard_entry_t*)calloc( + response->num_top_entries, sizeof(rc_client_leaderboard_scoreboard_entry_t)); + if (sboard.top_entries != NULL) { + uint32_t i; + for (i = 0; i < response->num_top_entries; i++) { + sboard.top_entries[i].username = response->top_entries[i].username; + sboard.top_entries[i].rank = response->top_entries[i].rank; + rc_format_value(sboard.top_entries[i].score, sizeof(sboard.top_entries[i].score), response->top_entries[i].score, + leaderboard->format); + } + } + } + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD; + client_event.leaderboard = &leaderboard->public_; + client_event.leaderboard_scoreboard = &sboard; + + lboard_data->client->callbacks.event_handler(&client_event, lboard_data->client); + + if (sboard.top_entries != NULL) { + free(sboard.top_entries); + } +} + +static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data; + rc_api_submit_lboard_entry_response_t submit_lboard_entry_response; + + int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response); + + if (error_message) { + if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, result, submit_lboard_entry_response.response.error_message); + } + else if (lboard_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message); + rc_client_submit_leaderboard_entry_server_call(lboard_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (lboard_data->retry_count > 8) ? 120 : (1 << (lboard_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay); + + if (!lboard_data->scheduled_callback_data) { + lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data)); + if (!lboard_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry; + lboard_data->scheduled_callback_data->data = lboard_data; + lboard_data->scheduled_callback_data->related_id = lboard_data->id; + } + + lboard_data->scheduled_callback_data->when = + lboard_data->client->callbacks.get_time_millisecs(lboard_data->client) + delay * 1000; + + rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data); + + rc_client_update_disconnect_state(lboard_data->client); + return; + } + } + else { + /* raise event for scoreboard */ + if (lboard_data->retry_count < 2) { + rc_client_raise_scoreboard_event(lboard_data, &submit_lboard_entry_response); + } + + /* not currently doing anything with the response */ + if (lboard_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts", + lboard_data->id, lboard_data->score, lboard_data->retry_count); + } + } + + if (lboard_data->retry_count) + rc_client_update_disconnect_state(lboard_data->client); + + if (lboard_data->scheduled_callback_data) + free(lboard_data->scheduled_callback_data); + free(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data) +{ + rc_api_submit_lboard_entry_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = lboard_data->client->user.username; + api_params.api_token = lboard_data->client->user.token; + api_params.leaderboard_id = lboard_data->id; + api_params.score = lboard_data->score; + api_params.game_hash = lboard_data->game_hash; + + result = rc_api_init_submit_lboard_entry_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); + return; + } + + lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_submit_leaderboard_entry_callback_data_t* callback_data; + + if (client->callbacks.can_submit_leaderboard_entry && + !client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id); + return; + } + + /* don't actually submit leaderboard entries when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + return; + } + + callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id); + rc_client_raise_server_error_event(client, "submit_lboard_entry", leaderboard->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = leaderboard->public_.id; + callback_data->score = leaderboard->value; + callback_data->game_hash = client->game->public_.hash; + callback_data->submit_time = time(NULL); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + rc_client_submit_leaderboard_entry_server_call(callback_data); +} + +static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard) + continue; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_reset_lboard(lboard); + break; + } + } +} + +static void rc_client_reset_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_leaderboards(client->game, subset); +} + +typedef struct rc_client_fetch_leaderboard_entries_callback_data_t { + rc_client_t* client; + rc_client_fetch_leaderboard_entries_callback_t callback; + void* callback_userdata; + uint32_t leaderboard_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_leaderboard_entries_callback_data_t; + +static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data; + rc_client_t* client = lbinfo_callback_data->client; + rc_api_fetch_leaderboard_info_response_t lbinfo_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &lbinfo_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted"); + } + free(lbinfo_callback_data); + return; + } + + result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message); + lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries; + size_t needed_size = list_size; + uint32_t i; + + for (i = 0; i < lbinfo_response.num_entries; i++) + needed_size += strlen(lbinfo_response.entries[i].username) + 1; + + list = (rc_client_leaderboard_entry_list_t*)malloc(needed_size); + if (!list) { + lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)list + sizeof(*list)); + char* user = (char*)((uint8_t*)list + list_size); + const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries; + const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries; + const size_t logged_in_user_len = strlen(client->user.display_name) + 1; + list->user_index = -1; + + for (; lbentry < stop; ++lbentry, ++entry) { + const size_t len = strlen(lbentry->username) + 1; + entry->user = user; + memcpy(user, lbentry->username, len); + user += len; + + if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0) + list->user_index = (int)(entry - list->entries); + + entry->index = lbentry->index; + entry->rank = lbentry->rank; + entry->submitted = lbentry->submitted; + + rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format); + } + + list->num_entries = lbinfo_response.num_entries; + + lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response); + free(lbinfo_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client, + const rc_api_fetch_leaderboard_info_request_t* lbinfo_request, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_api_request_t request; + int result; + const char* error_message; + + result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->leaderboard_id = lbinfo_request->leaderboard_id; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.first_entry = first_entry; + lbinfo_request.count = count; + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.username = client->user.username; + lbinfo_request.count = count; + + if (!lbinfo_request.username) { + callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata); + return NULL; + } + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size) +{ + if (!entry) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user); +} + +/* ===== Rich Presence ===== */ + +static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_t* client = (rc_client_t*)callback_data; + rc_api_ping_response_t response; + + int result = rc_api_process_ping_server_response(&response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response); + if (error_message) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message); + } + + rc_api_destroy_ping_response(&response); +} + +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_api_ping_request_t api_params; + rc_api_request_t request; + char buffer[256]; + int result; + + if (!client->callbacks.rich_presence_override || + !client->callbacks.rich_presence_override(client, buffer, sizeof(buffer))) { + rc_mutex_lock(&client->state.mutex); + + rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer), + client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + } + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public_.id; + api_params.rich_presence = buffer; + + result = rc_api_init_ping_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); + } + else { + client->callbacks.server_call(&request, rc_client_ping_callback, client, client); + } + + callback_data->when = now + 120 * 1000; + rc_client_schedule_callback(client, callback_data); +} + +int rc_client_has_rich_presence(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (!client->game->runtime.richpresence || !client->game->runtime.richpresence->richpresence) + return 0; + + return 1; +} + +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size) +{ + int result; + + if (!client || !client->game || !buffer) + return 0; + + rc_mutex_lock(&client->state.mutex); + + result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size, + client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + if (result == 0) + result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title); + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (client) + client->callbacks.event_handler = handler; +} + +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + if (client) + client->callbacks.read_memory = handler; +} + +static void rc_client_invalidate_processing_memref(rc_client_t* client) +{ + rc_memref_t** next_memref = &client->game->runtime.memrefs; + rc_memref_t* memref; + + /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ + if (!client->state.processing_memref) + return; + + /* invalid memref. remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch the current value. */ + while ((memref = *next_memref) != NULL) { + if (memref == client->state.processing_memref) { + *next_memref = memref->next; + break; + } + next_memref = &memref->next; + } + + rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); + rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); + + client->state.processing_memref = NULL; +} + +static uint32_t rc_client_peek_le(uint32_t address, uint32_t num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint32_t value = 0; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + if (num_bytes <= sizeof(value)) { + num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client); + if (num_read == num_bytes) + return value; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +static uint32_t rc_client_peek(uint32_t address, uint32_t num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint8_t buffer[4]; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + switch (num_bytes) { + case 1: + num_read = client->callbacks.read_memory(address, buffer, 1, client); + if (num_read == 1) + return buffer[0]; + break; + case 2: + num_read = client->callbacks.read_memory(address, buffer, 2, client); + if (num_read == 2) + return buffer[0] | (buffer[1] << 8); + break; + case 3: + num_read = client->callbacks.read_memory(address, buffer, 3, client); + if (num_read == 3) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16); + break; + case 4: + num_read = client->callbacks.read_memory(address, buffer, 4, client); + if (num_read == 4) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + break; + default: + break; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +void rc_client_set_legacy_peek(rc_client_t* client, int method) +{ + if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { + union { + uint32_t whole; + uint8_t parts[4]; + } u; + u.whole = 1; + method = (u.parts[0] == 1) ? + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED; + } + + client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ? + rc_client_peek_le : rc_client_peek; +} + +int rc_client_is_processing_required(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (client->game->runtime.trigger_count || client->game->runtime.lboard_count) + return 1; + + return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence); +} + +static void rc_client_update_memref_values(rc_client_t* client) +{ + rc_memref_t* memref = client->game->runtime.memrefs; + uint32_t value; + int invalidated_memref = 0; + + for (; memref; memref = memref->next) { + if (memref->value.is_indirect) + continue; + + client->state.processing_memref = memref; + + value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + + if (client->state.processing_memref) { + rc_update_memref_value(&memref->value, value); + } + else { + /* if the peek function cleared the processing_memref, the memref was invalidated */ + invalidated_memref = 1; + } + } + + client->state.processing_memref = NULL; + + if (invalidated_memref) + rc_client_update_active_achievements(client->game); +} + +static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + int old_state, new_state; + uint32_t old_measured_value; + + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL); + + /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_value <= trigger->measured_target && + rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { + + /* only show a popup for the achievement closest to triggering */ + float progress = (float)trigger->measured_value / (float)trigger->measured_target; + + if (trigger->measured_as_percent) { + /* if reporting the measured value as a percentage, only show the popup if the percentage changes */ + const uint32_t old_percent = (uint32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const uint32_t new_percent = (uint32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent == new_percent) + progress = -1.0; + } + + if (progress > client->game->progress_tracker.progress) { + client->game->progress_tracker.progress = progress; + client->game->progress_tracker.achievement = achievement; + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE; + } + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + + /* raise events for each of the possible new states */ + if (new_state == RC_TRIGGER_STATE_TRIGGERED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED; + else if (new_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } +} + +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (game->progress_tracker.hide_callback && + game->progress_tracker.hide_callback->when && + game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0); + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + } +} + +static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + + rc_mutex_lock(&client->state.mutex); + if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + client->game->progress_tracker.hide_callback->when = 0; + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + } + rc_mutex_unlock(&client->state.mutex); + + if (client_event.type) + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + if (!game->progress_tracker.hide_callback) { + game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) + rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t)); + game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed; + } + + if (game->progress_tracker.hide_callback->when == 0) + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW; + else + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; + + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, + client->callbacks.get_time_millisecs(client) + 2 * 1000); +} + +static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + switch (game->progress_tracker.action) { + case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + break; + case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + break; + default: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + break; + } + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE; + + client_event.achievement = &game->progress_tracker.achievement->public_; + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + rc_client_event_t client_event; + time_t recent_unlock_time = 0; + + memset(&client_event, 0, sizeof(client_event)); + + for (; achievement < stop; ++achievement) { + if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE) + continue; + + /* kick off award achievement request first */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + rc_client_award_achievement(client, achievement); + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS; + } + + /* update display state */ + if (recent_unlock_time == 0) + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + + /* raise events */ + client_event.achievement = &achievement->public_; + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + client->callbacks.event_handler(&client_event, client); + } + + /* clear pending flags */ + achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + int old_state, new_state; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + default: + if (!lboard) + continue; + + break; + } + + old_state = lboard->state; + new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL); + + switch (new_state) { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (old_state != RC_LBOARD_STATE_STARTED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED; + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + } + else { + rc_client_update_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (old_state != RC_LBOARD_STATE_CANCELED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED; + + if (old_state != RC_LBOARD_STATE_STARTED) + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + else + rc_client_update_leaderboard_tracker(client->game, leaderboard); + + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + } + + if (leaderboard->pending_events) + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } +} + +static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = game->leaderboard_trackers; + for (; tracker; tracker = tracker->next) { + if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard_tracker = &tracker->public_; + + /* update display text for new trackers or updated trackers */ + if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE)) + rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format); + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) { + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + /* request to show and hide in the same frame - ignore the event */ + } + else { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + client->callbacks.event_handler(&client_event, client); + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + client->callbacks.event_handler(&client_event, client); + } + + tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard = &leaderboard->public_; + + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) { + /* kick off submission request before raising event */ + rc_client_submit_leaderboard_entry(client, leaderboard); + + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + client->callbacks.event_handler(&client_event, client); + } + + leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE; + } +} + +static void rc_client_reset_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; + + for (subset = client->game->subsets; subset; subset = subset->next) + subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE; +} + +static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + /* raise any pending achievement events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT) + rc_client_raise_achievement_events(client, subset); + + /* raise any pending leaderboard events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD) + rc_client_raise_leaderboard_events(client, subset); + + /* raise mastery event if pending */ + if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING) + rc_client_raise_mastery_event(client, subset); +} + +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client, game); + + for (subset = game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* raise progress tracker events after achievement events so formatted values are updated for tracker event */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_raise_progress_tracker_events(client, game); + + /* if any achievements were unlocked, resync the active achievements list */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(game); + rc_mutex_unlock(&client->state.mutex); + } + + game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; +} + +void rc_client_do_frame(rc_client_t* client) +{ + if (!client) + return; + + if (client->game && !client->game->waiting_for_reset) { + rc_runtime_richpresence_t* richpresence; + rc_client_subset_info_t* subset; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + rc_client_update_memref_values(client); + rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL); + + client->game->progress_tracker.progress = 0.0; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_do_frame_update_progress_tracker(client, client->game); + + if (client->state.hardcore) { + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_leaderboards(client, subset); + } + } + + richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + const rc_clock_t now = client->callbacks.get_time_millisecs(client); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (scheduled_callback->when > now) { + /* not time for next callback yet, ignore it */ + scheduled_callback = NULL; + } + else { + /* remove the callback from the queue while we process it. callback can requeue if desired */ + client->state.scheduled_callbacks = scheduled_callback->next; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!scheduled_callback) + break; + + scheduled_callback->callback(scheduled_callback, client, now); + } while (1); + } + + if (client->state.disconnect & ~RC_CLIENT_DISCONNECT_VISIBLE) + rc_client_raise_disconnect_events(client); +} + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_mutex_lock(&client->state.mutex); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next || scheduled_callback->when < next->when) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reschedule_callback(rc_client_t* client, + rc_client_scheduled_callback_data_t* callback, rc_clock_t when) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + /* ASSERT: this should only be called if the mutex is held */ + + callback->when = when; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + + if (next == callback) { + if (when == 0) { + /* request to unschedule the callback */ + *last = next->next; + next->next = NULL; + break; + } + + if (!next->next) { + /* end of list, just append it */ + break; + } + + if (when < next->next->when) { + /* already in the correct place */ + break; + } + + /* remove from current position - will insert later */ + *last = next->next; + next->next = NULL; + continue; + } + + if (!next || when < next->when) { + /* insert here */ + callback->next = next; + *last = callback; + break; + } + + last = &next->next; + } while (1); +} + +static void rc_client_reset_richpresence(rc_client_t* client) +{ + rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_reset_richpresence(richpresence->richpresence); +} + +static void rc_client_reset_variables(rc_client_t* client) +{ + rc_value_t* variable = client->game->runtime.variables; + for (; variable; variable = variable->next) + rc_reset_value(variable); +} + +static void rc_client_reset_all(rc_client_t* client) +{ + rc_client_reset_achievements(client); + rc_client_reset_leaderboards(client); + rc_client_reset_richpresence(client); + rc_client_reset_variables(client); +} + +void rc_client_reset(rc_client_t* client) +{ + rc_client_game_hash_t* game_hash; + if (!client || !client->game) + return; + + game_hash = rc_client_find_game_hash(client, client->game->public_.hash); + if (game_hash && game_hash->game_id != client->game->public_.id) { + /* current media is not for loaded game. unload game */ + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)", + (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash); + rc_client_unload_game(client); + return; + } + + RC_CLIENT_LOG_INFO(client, "Resetting runtime"); + + rc_mutex_lock(&client->state.mutex); + + client->game->waiting_for_reset = 0; + rc_client_reset_pending_events(client); + + rc_client_hide_progress_tracker(client, client->game); + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_progress_size(&client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer) +{ + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + if (!buffer) + return RC_INVALID_STATE; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_serialize_progress(buffer, &client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any visible challenge indicators to be hidden */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED && + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + /* flag any visible trackers to be hidden */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (lboard && lboard->state == RC_LBOARD_STATE_STARTED && + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } + } +} + +static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any challenge indicators that should be shown */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + /* if it's already shown, just keep it. otherwise flag it to be shown */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + } + else { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + /* ASSERT: only active achievements are serialized, so we don't have to worry about + * deserialization deactiving them. */ + } + + /* flag any trackers that need to be shown */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (lboard->state == RC_LBOARD_STATE_STARTED) { + leaderboard->value = (int)lboard->value.value.value; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + + /* if it's already being tracked, just update tracker. otherwise, allocate one */ + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_update_leaderboard_tracker(game, leaderboard); + } + else { + rc_client_allocate_leaderboard_tracker(game, leaderboard); + } + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + /* deallocate the tracker (don't actually raise the failed event) */ + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } +} + +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_before_deserialize_progress(subset); + + rc_client_hide_progress_tracker(client, client->game); + + if (!serialized) { + rc_client_reset_all(client); + result = RC_OK; + } + else { + result = rc_runtime_deserialize_progress(&client->game->runtime, serialized, NULL); + } + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_after_deserialize_progress(client->game, subset); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + + return result; +} + +/* ===== Toggles ===== */ + +static void rc_client_enable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 1; + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); + rc_client_activate_leaderboards(client->game, client); + + /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */ + RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset"); + client->game->waiting_for_reset = 1; + } + else { + RC_CLIENT_LOG_INFO(client, "Hardcore enabled"); + } +} + +static void rc_client_disable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 0; + RC_CLIENT_LOG_INFO(client, "Hardcore disabled"); + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_deactivate_leaderboards(client->game, client); + } +} + +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled) +{ + int changed = 0; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + enabled = enabled ? 1 : 0; + if (client->state.hardcore != enabled) { + if (enabled) + rc_client_enable_hardcore(client); + else + rc_client_disable_hardcore(client); + + changed = 1; + } + + rc_mutex_unlock(&client->state.mutex); + + /* events must be raised outside of lock */ + if (changed && client->game) { + if (enabled) { + /* if enabling hardcore, notify client that a reset is requested */ + if (client->game->waiting_for_reset) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + client->callbacks.event_handler(&client_event, client); + } + } + else { + /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */ + rc_client_raise_pending_events(client, client->game); + } + } +} + +int rc_client_get_hardcore_enabled(const rc_client_t* client) +{ + return client && client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled"); + client->state.unofficial_enabled = enabled ? 1 : 0; + } +} + +int rc_client_get_unofficial_enabled(const rc_client_t* client) +{ + return client && client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled"); + client->state.encore_mode = enabled ? 1 : 0; + } +} + +int rc_client_get_encore_mode_enabled(const rc_client_t* client) +{ + return client && client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) { + RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game."); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled"); + client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF; + } +} + +int rc_client_get_spectator_mode_enabled(const rc_client_t* client) +{ + return client && (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1; +} + +void rc_client_set_userdata(rc_client_t* client, void* userdata) +{ + if (client) + client->callbacks.client_data = userdata; +} + +void* rc_client_get_userdata(const rc_client_t* client) +{ + return client ? client->callbacks.client_data : NULL; +} + +void rc_client_set_host(const rc_client_t* client, const char* hostname) +{ + /* if empty, just pass NULL */ + if (hostname && !hostname[0]) + hostname = NULL; + + /* clear the image host so it'll use the custom host for images too */ + rc_api_set_image_host(NULL); + + /* set the custom host */ + if (hostname && client) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + } + rc_api_set_host(hostname); +} diff --git a/deps/rcheevos/src/rc_client_internal.h b/deps/rcheevos/src/rc_client_internal.h new file mode 100644 index 000000000000..c3ff588215f8 --- /dev/null +++ b/deps/rcheevos/src/rc_client_internal.h @@ -0,0 +1,350 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_client.h" + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +struct rc_api_fetch_game_data_response_t; +typedef void (*rc_client_post_process_game_data_response_t)(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata); +typedef int (*rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); +typedef int (*rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); +typedef int (*rc_client_rich_presence_override_t)(rc_client_t* client, char buffer[], size_t buffersize); + +typedef struct rc_client_callbacks_t { + rc_client_read_memory_func_t read_memory; + rc_client_event_handler_t event_handler; + rc_client_server_call_t server_call; + rc_client_message_callback_t log_call; + rc_get_time_millisecs_func_t get_time_millisecs; + rc_client_post_process_game_data_response_t post_process_game_data_response; + rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock; + rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry; + rc_client_rich_presence_override_t rich_presence_override; + + void* client_data; +} rc_client_callbacks_t; + +struct rc_client_scheduled_callback_data_t; +typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + rc_clock_t when; + uint32_t related_id; + rc_client_scheduled_callback_t callback; + void* data; + struct rc_client_scheduled_callback_data_t* next; +} rc_client_scheduled_callback_data_t; + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE = 0, + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED = (1 << 1), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW = (1 << 2), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE = (1 << 3), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE = (1 << 4) /* not a real event, just triggers update */ +}; + +typedef struct rc_client_achievement_info_t { + rc_client_achievement_t public_; + + rc_trigger_t* trigger; + uint8_t md5[16]; + + time_t unlock_time_hardcore; + time_t unlock_time_softcore; + + uint8_t pending_events; + + const char* author; + time_t created_time; + time_t updated_time; +} rc_client_achievement_info_t; + +enum { + RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW, + RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE +}; + +typedef struct rc_client_progress_tracker_t { + rc_client_achievement_info_t* achievement; + float progress; + + rc_client_scheduled_callback_data_t* hide_callback; + uint8_t action; +} rc_client_progress_tracker_t; + +/*****************************************************************************\ +| Leaderboard Trackers | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE = (1 << 1), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW = (1 << 2), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE = (1 << 3) +}; + +typedef struct rc_client_leaderboard_tracker_info_t { + rc_client_leaderboard_tracker_t public_; + struct rc_client_leaderboard_tracker_info_t* next; + int32_t raw_value; + + uint32_t value_djb2; + + uint8_t format; + uint8_t pending_events; + uint8_t reference_count; + uint8_t value_from_hits; +} rc_client_leaderboard_tracker_info_t; + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED = (1 << 1), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED = (1 << 2), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED = (1 << 3) +}; + +typedef struct rc_client_leaderboard_info_t { + rc_client_leaderboard_t public_; + + rc_lboard_t* lboard; + uint8_t md5[16]; + + rc_client_leaderboard_tracker_info_t* tracker; + + uint32_t value_djb2; + int32_t value; + + uint8_t format; + uint8_t pending_events; + uint8_t bucket; + uint8_t hidden; +} rc_client_leaderboard_info_t; + +uint8_t rc_client_map_leaderboard_format(int format); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +enum { + RC_CLIENT_SUBSET_PENDING_EVENT_NONE = 0, + RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT = (1 << 1), + RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD = (1 << 2) +}; + +typedef struct rc_client_subset_info_t { + rc_client_subset_t public_; + + rc_client_achievement_info_t* achievements; + rc_client_leaderboard_info_t* leaderboards; + + struct rc_client_subset_info_t* next; + + const char* all_label; + const char* inactive_label; + const char* locked_label; + const char* unlocked_label; + const char* unofficial_label; + const char* unsupported_label; + + uint8_t active; + uint8_t mastery; + uint8_t pending_events; +} rc_client_subset_info_t; + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +typedef struct rc_client_game_hash_t { + char hash[33]; + uint32_t game_id; + struct rc_client_game_hash_t* next; +} rc_client_game_hash_t; + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash); + +typedef struct rc_client_media_hash_t { + rc_client_game_hash_t* game_hash; + struct rc_client_media_hash_t* next; + uint32_t path_djb2; +} rc_client_media_hash_t; + +enum { + RC_CLIENT_GAME_PENDING_EVENT_NONE = 0, + RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER = (1 << 1), + RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS = (1 << 2), + RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER = (1 << 3) +}; + +typedef struct rc_client_game_info_t { + rc_client_game_t public_; + rc_client_leaderboard_tracker_info_t* leaderboard_trackers; + rc_client_progress_tracker_t progress_tracker; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + + uint32_t max_valid_address; + + uint8_t waiting_for_reset; + uint8_t pending_events; + + rc_buffer_t buffer; +} rc_client_game_info_t; + +void rc_client_update_active_achievements(rc_client_game_info_t* game); +void rc_client_update_active_leaderboards(rc_client_game_info_t* game); + +/*****************************************************************************\ +| Client | +\*****************************************************************************/ + +enum { + RC_CLIENT_LOAD_STATE_NONE, + RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_STATE_AWAIT_LOGIN, + RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_STATE_STARTING_SESSION, + RC_CLIENT_LOAD_STATE_DONE, + RC_CLIENT_LOAD_STATE_UNKNOWN_GAME +}; + +enum { + RC_CLIENT_USER_STATE_NONE, + RC_CLIENT_USER_STATE_LOGIN_REQUESTED, + RC_CLIENT_USER_STATE_LOGGED_IN +}; + +enum { + RC_CLIENT_MASTERY_STATE_NONE, + RC_CLIENT_MASTERY_STATE_PENDING, + RC_CLIENT_MASTERY_STATE_SHOWN +}; + +enum { + RC_CLIENT_SPECTATOR_MODE_OFF, + RC_CLIENT_SPECTATOR_MODE_ON, + RC_CLIENT_SPECTATOR_MODE_LOCKED +}; + +enum { + RC_CLIENT_DISCONNECT_HIDDEN = 0, + RC_CLIENT_DISCONNECT_VISIBLE = (1 << 0), + RC_CLIENT_DISCONNECT_SHOW_PENDING = (1 << 1), + RC_CLIENT_DISCONNECT_HIDE_PENDING = (1 << 2) +}; + +struct rc_client_load_state_t; + +typedef struct rc_client_state_t { + rc_mutex_t mutex; + rc_buffer_t buffer; + + rc_client_scheduled_callback_data_t* scheduled_callbacks; + + uint8_t hardcore; + uint8_t encore_mode; + uint8_t spectator_mode; + uint8_t unofficial_enabled; + uint8_t log_level; + uint8_t user; + uint8_t disconnect; + + struct rc_client_load_state_t* load; + struct rc_client_async_handle_t* async_handles[4]; + rc_memref_t* processing_memref; + + rc_peek_t legacy_peek; +} rc_client_state_t; + +struct rc_client_t { + rc_client_game_info_t* game; + rc_client_game_hash_t* hashes; + + rc_client_user_t user; + + rc_client_callbacks_t callbacks; + + rc_client_state_t state; +}; + +/*****************************************************************************\ +| Helpers | +\*****************************************************************************/ + +#ifdef RC_NO_VARIADIC_MACROS + void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...); +#else + void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...); + #define RC_CLIENT_LOG_ERR_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_WARN_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_INFO_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_VERBOSE_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message_formatted(client, format, __VA_ARGS__); } +#endif + +void rc_client_log_message(const rc_client_t* client, const char* message); +#define RC_CLIENT_LOG_ERR(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_WARN(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_INFO(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_VERBOSE(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message(client, message); } + +/* internals pulled from runtime.c */ +void rc_runtime_checksum(const char* memaddr, uint8_t* md5); +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref); +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref); +/* end runtime.c internals */ + +/* helper functions for unit tests */ +struct rc_hash_iterator; +struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +/* end helper functions for unit tests */ + +enum { + RC_CLIENT_LEGACY_PEEK_AUTO, + RC_CLIENT_LEGACY_PEEK_CONSTRUCTED, + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS +}; + +void rc_client_set_legacy_peek(rc_client_t* client, int method); + +void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_CLIENT_INTERNAL_H */ diff --git a/deps/rcheevos/src/rcheevos/compat.c b/deps/rcheevos/src/rc_compat.c similarity index 61% rename from deps/rcheevos/src/rcheevos/compat.c rename to deps/rcheevos/src/rc_compat.c index ce635fc1eef7..0ba7601ad218 100644 --- a/deps/rcheevos/src/rcheevos/compat.c +++ b/deps/rcheevos/src/rc_compat.c @@ -83,3 +83,57 @@ struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) } #endif + +#ifndef RC_NO_THREADS +#ifdef _WIN32 + +/* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */ + +#define WIN32_LEAN_AND_MEAN +#include + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* default security, not owned by calling thread, unnamed */ + mutex->handle = CreateMutex(NULL, FALSE, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + CloseHandle(mutex->handle); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + WaitForSingleObject(mutex->handle, 0xFFFFFFFF); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + ReleaseMutex(mutex->handle); +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + pthread_mutex_init(mutex, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + pthread_mutex_destroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + pthread_mutex_lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + pthread_mutex_unlock(mutex); +} + +#endif +#endif /* RC_NO_THREADS */ diff --git a/deps/rcheevos/src/rcheevos/rc_compat.h b/deps/rcheevos/src/rc_compat.h similarity index 71% rename from deps/rcheevos/src/rcheevos/rc_compat.h rename to deps/rcheevos/src/rc_compat.h index f396726db5f5..e22f9b84e50a 100644 --- a/deps/rcheevos/src/rcheevos/rc_compat.h +++ b/deps/rcheevos/src/rc_compat.h @@ -13,6 +13,8 @@ extern "C" { /* MinGW redefinitions */ +#define RC_NO_VARIADIC_MACROS 1 + #elif defined(_MSC_VER) /* Visual Studio redefinitions */ @@ -32,6 +34,8 @@ extern "C" { /* C89 redefinitions */ #define RC_C89_HELPERS 1 +#define RC_NO_VARIADIC_MACROS 1 + #ifndef snprintf extern int rc_snprintf(char* buffer, size_t size, const char* format, ...); #define snprintf rc_snprintf @@ -65,6 +69,29 @@ extern "C" { #define gmtime_s rc_gmtime_s #endif +#ifdef RC_NO_THREADS + typedef int rc_mutex_t; + + #define rc_mutex_init(mutex) + #define rc_mutex_destroy(mutex) + #define rc_mutex_lock(mutex) + #define rc_mutex_unlock(mutex) +#else + #ifdef _WIN32 + typedef struct rc_mutex_t { + void* handle; /* HANDLE is defined as "void*" */ + } rc_mutex_t; + #else + #include + typedef pthread_mutex_t rc_mutex_t; + #endif + + void rc_mutex_init(rc_mutex_t* mutex); + void rc_mutex_destroy(rc_mutex_t* mutex); + void rc_mutex_lock(rc_mutex_t* mutex); + void rc_mutex_unlock(rc_mutex_t* mutex); +#endif + #ifdef __cplusplus } #endif diff --git a/deps/rcheevos/src/rcheevos/rc_libretro.c b/deps/rcheevos/src/rc_libretro.c similarity index 87% rename from deps/rcheevos/src/rcheevos/rc_libretro.c rename to deps/rcheevos/src/rc_libretro.c index 541f62a4baa7..4facc14af461 100644 --- a/deps/rcheevos/src/rcheevos/rc_libretro.c +++ b/deps/rcheevos/src/rc_libretro.c @@ -255,7 +255,7 @@ const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* l typedef struct rc_disallowed_core_systems_t { const char* library_name; - const int disallowed_consoles[4]; + const uint32_t disallowed_consoles[4]; } rc_disallowed_core_systems_t; static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { @@ -264,7 +264,7 @@ static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { { NULL, { 0 } } }; -int rc_libretro_is_system_allowed(const char* library_name, int console_id) { +int rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id) { const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; size_t library_name_length; size_t i; @@ -288,8 +288,8 @@ int rc_libretro_is_system_allowed(const char* library_name, int console_id) { return 1; } -unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail) { - unsigned i; +uint8_t* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail) { + uint32_t i; for (i = 0; i < regions->count; ++i) { const size_t size = regions->size[i]; @@ -298,12 +298,12 @@ unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* break; if (avail) - *avail = (unsigned)(size - address); + *avail = (uint32_t)(size - address); return ®ions->data[i][address]; } - address -= (unsigned)size; + address -= (uint32_t)size; } if (avail) @@ -312,10 +312,35 @@ unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* return NULL; } -unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address) { +uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address) { return rc_libretro_memory_find_avail(regions, address, NULL); } +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, + uint8_t* buffer, uint32_t num_bytes) { + uint32_t i; + uint32_t avail; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + avail = (unsigned)(size - address); + if (avail < num_bytes) + return avail; + + memcpy(buffer, ®ions->data[i][address], num_bytes); + return num_bytes; + } + + address -= (unsigned)size; + } + + return 0; +} + void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { rc_libretro_verbose_message_callback = callback; } @@ -342,7 +367,7 @@ static const char* rc_memory_type_str(int type) { } static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, - unsigned char* data, size_t size, const char* description) { + uint8_t* data, size_t size, const char* description) { if (size == 0) return; @@ -394,7 +419,7 @@ static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); } -static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, unsigned real_address, size_t* offset) +static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, uint32_t real_address, size_t* offset) { const struct retro_memory_descriptor* desc = mmap->descriptors; const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; @@ -412,14 +437,14 @@ static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(c /* address is in the block if (addr & select) == (start & select) */ if (((desc->start ^ real_address) & desc->select) == 0) { /* get the relative offset of the address from the start of the memory block */ - unsigned reduced_address = real_address - (unsigned)desc->start; + uint32_t reduced_address = real_address - (unsigned)desc->start; /* remove any bits from the reduced_address that correspond to the bits in the disconnect * mask and collapse the remaining bits. this code was copied from the mmap_reduce function * in RetroArch. i'm not exactly sure how it works, but it does. */ - unsigned disconnect_mask = (unsigned)desc->disconnect; + uint32_t disconnect_mask = (unsigned)desc->disconnect; while (disconnect_mask) { - const unsigned tmp = (disconnect_mask - 1) & ~disconnect_mask; + const uint32_t tmp = (disconnect_mask - 1) & ~disconnect_mask; reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; } @@ -441,24 +466,24 @@ static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(c static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, const rc_memory_regions_t* console_regions) { char description[64]; - unsigned i; - unsigned char* region_start; - unsigned char* desc_start; + uint32_t i; + uint8_t* region_start; + uint8_t* desc_start; size_t desc_size; size_t offset; for (i = 0; i < console_regions->num_regions; ++i) { const rc_memory_region_t* console_region = &console_regions->region[i]; size_t console_region_size = console_region->end_address - console_region->start_address + 1; - unsigned real_address = console_region->real_address; - unsigned disconnect_size = 0; + uint32_t real_address = console_region->real_address; + uint32_t disconnect_size = 0; while (console_region_size > 0) { const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); if (!desc) { if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { snprintf(description, sizeof(description), "Could not map region starting at $%06X", - real_address - console_region->real_address + console_region->start_address); + (unsigned)(real_address - console_region->real_address + console_region->start_address)); rc_libretro_verbose(description); } @@ -498,7 +523,7 @@ static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t if (desc_size == 0) { if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { snprintf(description, sizeof(description), "Could not map region starting at $%06X", - real_address - console_region->real_address + console_region->start_address); + (unsigned)(real_address - console_region->real_address + console_region->start_address)); rc_libretro_verbose(description); } @@ -519,7 +544,7 @@ static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t } } -static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { +static uint32_t rc_libretro_memory_console_region_to_ram_type(uint8_t region_type) { switch (region_type) { case RC_MEMORY_TYPE_SAVE_RAM: @@ -536,15 +561,15 @@ static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { char description[64]; - unsigned i, j; + uint32_t i, j; rc_libretro_core_memory_info_t info; size_t offset; for (i = 0; i < console_regions->num_regions; ++i) { const rc_memory_region_t* console_region = &console_regions->region[i]; const size_t console_region_size = console_region->end_address - console_region->start_address + 1; - const unsigned type = rc_libretro_memory_console_region_to_ram_type(console_region->type); - unsigned base_address = 0; + const uint32_t type = rc_libretro_memory_console_region_to_ram_type(console_region->type); + uint32_t base_address = 0; for (j = 0; j <= i; ++j) { const rc_memory_region_t* console_region2 = &console_regions->region[j]; @@ -570,7 +595,7 @@ static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regi } else { if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { - snprintf(description, sizeof(description), "Could not map region starting at $%06X", console_region->start_address); + snprintf(description, sizeof(description), "Could not map region starting at $%06X", (unsigned)console_region->start_address); rc_libretro_verbose(description); } @@ -595,7 +620,7 @@ int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); rc_libretro_memory_regions_t new_regions; int has_valid_region = 0; - unsigned i; + uint32_t i; if (!regions) return 0; @@ -650,47 +675,50 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, file_len = rc_file_tell(file_handle); rc_file_seek(file_handle, 0, SEEK_SET); - m3u_contents = (char*)malloc(file_len + 1); - rc_file_read(file_handle, m3u_contents, (int)file_len); - m3u_contents[file_len] = '\0'; - - rc_file_close(file_handle); - - ptr = m3u_contents; - do + m3u_contents = (char*)malloc((size_t)file_len + 1); + if (m3u_contents) { - /* ignore whitespace */ - while (isspace((int)*ptr)) - ++ptr; + rc_file_read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + rc_file_close(file_handle); - if (*ptr == '#') + ptr = m3u_contents; + do { - /* ignore comment unless it's the special SAVEDISK extension */ - if (memcmp(ptr, "#SAVEDISK:", 10) == 0) + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') { - /* get the path to the save disk from the frontend, assign it a bogus hash so - * it doesn't get hashed later */ - if (get_image_path(index, image_path, sizeof(image_path))) + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) { - const char save_disk_hash[33] = "[SAVE DISK]"; - rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); - ++index; + /* get the path to the save disk from the frontend, assign it a bogus hash so + * it doesn't get hashed later */ + if (get_image_path(index, image_path, sizeof(image_path))) + { + const char save_disk_hash[33] = "[SAVE DISK]"; + rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); + ++index; + } } } - } - else - { - /* non-empty line, tally a file */ - ++index; - } + else + { + /* non-empty line, tally a file */ + ++index; + } - /* find the end of the line */ - while (*ptr && *ptr != '\n') - ++ptr; + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; - } while (*ptr); + } while (*ptr); - free(m3u_contents); + free(m3u_contents); + } if (hash_set->entries_count > 0) { @@ -707,9 +735,9 @@ void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { memset(hash_set, 0, sizeof(*hash_set)); } -static unsigned rc_libretro_djb2(const char* input) +static uint32_t rc_libretro_djb2(const char* input) { - unsigned result = 5381; + uint32_t result = 5381; char c; while ((c = *input++) != '\0') @@ -719,8 +747,8 @@ static unsigned rc_libretro_djb2(const char* input) } void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, - const char* path, int game_id, const char hash[33]) { - const unsigned path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; + const char* path, uint32_t game_id, const char hash[33]) { + const uint32_t path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; struct rc_libretro_hash_entry_t* entry = NULL; struct rc_libretro_hash_entry_t* scan; struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; @@ -765,7 +793,7 @@ void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) { - const unsigned path_djb2 = rc_libretro_djb2(path); + const uint32_t path_djb2 = rc_libretro_djb2(path); struct rc_libretro_hash_entry_t* scan = hash_set->entries; struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; for (; scan < stop; ++scan) diff --git a/deps/rcheevos/src/rcheevos/rc_libretro.h b/deps/rcheevos/src/rc_libretro.h similarity index 79% rename from deps/rcheevos/src/rcheevos/rc_libretro.h rename to deps/rcheevos/src/rc_libretro.h index 642e3cbdc0f1..ceb92983f8a0 100644 --- a/deps/rcheevos/src/rcheevos/rc_libretro.h +++ b/deps/rcheevos/src/rc_libretro.h @@ -1,13 +1,16 @@ #ifndef RC_LIBRETRO_H #define RC_LIBRETRO_H +#ifdef __cplusplus +extern "C" { +#endif + /* this file comes from the libretro repository, which is not an explicit submodule. * the integration must set up paths appropriately to find it. */ #include -#ifdef __cplusplus -extern "C" { -#endif +#include +#include /*****************************************************************************\ | Disallowed Settings | @@ -21,7 +24,7 @@ typedef struct rc_disallowed_setting_t const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name); int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); -int rc_libretro_is_system_allowed(const char* library_name, int console_id); +int rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id); /*****************************************************************************\ | Memory Mapping | @@ -34,26 +37,27 @@ void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback call #define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 typedef struct rc_libretro_memory_regions_t { - unsigned char* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + uint8_t* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; size_t total_size; - unsigned count; + uint32_t count; } rc_libretro_memory_regions_t; typedef struct rc_libretro_core_memory_info_t { - unsigned char* data; + uint8_t* data; size_t size; } rc_libretro_core_memory_info_t; -typedef void (*rc_libretro_get_core_memory_info_func)(unsigned id, rc_libretro_core_memory_info_t* info); +typedef void (*rc_libretro_get_core_memory_info_func)(uint32_t id, rc_libretro_core_memory_info_t* info); int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id); void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); -unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address); -unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail); +uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address); +uint8_t* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail); +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, uint8_t* buffer, uint32_t num_bytes); /*****************************************************************************\ | Disk Identification | @@ -62,7 +66,7 @@ unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* typedef struct rc_libretro_hash_entry_t { uint32_t path_djb2; - int game_id; + uint32_t game_id; char hash[33]; } rc_libretro_hash_entry_t; @@ -73,14 +77,14 @@ typedef struct rc_libretro_hash_set_t uint16_t entries_size; } rc_libretro_hash_set_t; -typedef int (*rc_libretro_get_image_path_func)(unsigned index, char* buffer, size_t buffer_size); +typedef int (*rc_libretro_get_image_path_func)(uint32_t index, char* buffer, size_t buffer_size); void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, const char* m3u_path, rc_libretro_get_image_path_func get_image_path); void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, - const char* path, int game_id, const char hash[33]); + const char* path, uint32_t game_id, const char hash[33]); const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); diff --git a/deps/rcheevos/src/rc_util.c b/deps/rcheevos/src/rc_util.c new file mode 100644 index 000000000000..9deee91b4562 --- /dev/null +++ b/deps/rcheevos/src/rc_util.c @@ -0,0 +1,188 @@ +#include "rc_util.h" + +#include "rc_compat.h" +#include "rc_error.h" + +#include +#include + +#undef DEBUG_BUFFERS + +/* --- rc_buffer --- */ + +void rc_buffer_init(rc_buffer_t* buffer) +{ + buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; + buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; + buffer->chunk.next = NULL; + /* leave buffer->data uninitialized */ +} + +void rc_buffer_destroy(rc_buffer_t* buffer) +{ + rc_buffer_chunk_t* chunk; +#ifdef DEBUG_BUFFERS + int count = 0; + int wasted = 0; + int total = 0; +#endif + + /* first chunk is not allocated. skip it. */ + chunk = buffer->chunk.next; + + /* deallocate any additional buffers */ + while (chunk) + { + rc_buffer_chunk_t* next = chunk->next; +#ifdef DEBUG_BUFFERS + total += (int)(chunk->end - chunk->data); + wasted += (int)(chunk->end - chunk->write); + ++count; +#endif + free(chunk); + chunk = next; + } + +#ifdef DEBUG_BUFFERS + printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, + total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); +#endif +} + +uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount) +{ + rc_buffer_chunk_t* chunk = &buffer->chunk; + size_t remaining; + while (chunk) + { + remaining = chunk->end - chunk->write; + if (remaining >= amount) + return chunk->write; + + if (!chunk->next) + { + /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated + * to the chunk header, and the remaining will be used for data. + */ + const size_t chunk_header_size = sizeof(rc_buffer_chunk_t); + const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; + chunk->next = (rc_buffer_chunk_t*)malloc(alloc_size); + if (!chunk->next) + break; + + chunk->next->start = (uint8_t*)chunk->next + chunk_header_size; + chunk->next->write = chunk->next->start; + chunk->next->end = (uint8_t*)chunk->next + alloc_size; + chunk->next->next = NULL; + } + + chunk = chunk->next; + } + + return NULL; +} + +void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end) +{ + rc_buffer_chunk_t* chunk = &buffer->chunk; + do + { + if (chunk->write == start) + { + size_t offset = (end - chunk->start); + offset = (offset + 7) & ~7; + chunk->write = &chunk->start[offset]; + + if (chunk->write > chunk->end) + chunk->write = chunk->end; + break; + } + + chunk = chunk->next; + } while (chunk); +} + +void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount) +{ + uint8_t* ptr = rc_buffer_reserve(buffer, amount); + rc_buffer_consume(buffer, ptr, ptr + amount); + return (void*)ptr; +} + +char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len) +{ + uint8_t* dst = rc_buffer_reserve(buffer, len + 1); + memcpy(dst, src, len); + dst[len] = '\0'; + rc_buffer_consume(buffer, dst, dst + len + 2); + return (char*)dst; +} + +char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src) +{ + return rc_buffer_strncpy(buffer, src, strlen(src)); +} + +/* --- other --- */ + +void rc_format_md5(char checksum[33], const uint8_t digest[16]) +{ + snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); +} + +uint32_t rc_djb2(const char* input) +{ + uint32_t result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +const char* rc_error_str(int ret) +{ + switch (ret) { + case RC_OK: return "OK"; + case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; + case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; + case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; + case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; + case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; + case RC_INVALID_OPERATOR: return "Invalid operator"; + case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; + case RC_DUPLICATED_START: return "Duplicated start condition"; + case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; + case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; + case RC_DUPLICATED_VALUE: return "Duplicated value expression"; + case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; + case RC_MISSING_START: return "Missing start condition"; + case RC_MISSING_CANCEL: return "Missing cancel condition"; + case RC_MISSING_SUBMIT: return "Missing submit condition"; + case RC_MISSING_VALUE: return "Missing value expression"; + case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; + case RC_MISSING_DISPLAY_STRING: return "Missing display string"; + case RC_OUT_OF_MEMORY: return "Out of memory"; + case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; + case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; + case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; + case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; + case RC_INVALID_COMPARISON: return "Invalid comparison"; + case RC_INVALID_STATE: return "Invalid state"; + case RC_INVALID_JSON: return "Invalid JSON"; + case RC_API_FAILURE: return "API call failed"; + case RC_LOGIN_REQUIRED: return "Login required"; + case RC_NO_GAME_LOADED: return "No game loaded"; + case RC_HARDCORE_DISABLED: return "Hardcore disabled"; + case RC_ABORTED: return "Aborted"; + case RC_NO_RESPONSE: return "No response"; + case RC_ACCESS_DENIED: return "Access denied"; + case RC_INVALID_CREDENTIALS: return "Invalid credentials"; + case RC_EXPIRED_TOKEN: return "Expired token"; + default: return "Unknown error"; + } +} diff --git a/deps/rcheevos/src/rc_util.h b/deps/rcheevos/src/rc_util.h new file mode 100644 index 000000000000..34f75da912a7 --- /dev/null +++ b/deps/rcheevos/src/rc_util.h @@ -0,0 +1,53 @@ +#ifndef RC_UTIL_H +#define RC_UTIL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_buffer_chunk_t { + /* The current location where data is being written */ + uint8_t* write; + /* The first byte past the end of data where writing cannot occur */ + uint8_t* end; + /* The first byte of the data */ + uint8_t* start; + /* The next block in the allocated memory chain */ + struct rc_buffer_chunk_t* next; +} +rc_buffer_chunk_t; + +/** + * A preallocated block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_buffer_t { + /* The chunk data (will point at the local data member) */ + struct rc_buffer_chunk_t chunk; + /* Small chunk of memory pre-allocated for the chunk */ + uint8_t data[256]; +} +rc_buffer_t; + +void rc_buffer_init(rc_buffer_t* buffer); +void rc_buffer_destroy(rc_buffer_t* buffer); +uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount); +void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end); +void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount); +char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src); +char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len); + +uint32_t rc_djb2(const char* input); + +void rc_format_md5(char checksum[33], const uint8_t digest[16]); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_UTIL_H */ diff --git a/deps/rcheevos/src/rc_version.h b/deps/rcheevos/src/rc_version.h new file mode 100644 index 000000000000..19be5671aaef --- /dev/null +++ b/deps/rcheevos/src/rc_version.h @@ -0,0 +1,29 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define RCHEEVOS_VERSION_MAJOR 11 +#define RCHEEVOS_VERSION_MINOR 0 +#define RCHEEVOS_VERSION_PATCH 0 + +#define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) +#define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) + +#define RCHEEVOS_MAKE_STRING(num) #num +#define RCHEEVOS_MAKE_VERSION_STRING(major, minor, patch) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) "." RCHEEVOS_MAKE_STRING(patch) +#define RCHEEVOS_MAKE_VERSION_STRING_SHORT(major, minor) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) + +#if RCHEEVOS_VERSION_PATCH > 0 + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) +#else + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING_SHORT(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VERSION_H */ diff --git a/deps/rcheevos/src/rcheevos/alloc.c b/deps/rcheevos/src/rcheevos/alloc.c index c361eecee6bd..7c2af1f38af3 100644 --- a/deps/rcheevos/src/rcheevos/alloc.c +++ b/deps/rcheevos/src/rcheevos/alloc.c @@ -3,7 +3,7 @@ #include #include -void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset) +void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset) { rc_scratch_buffer_t* buffer; @@ -13,7 +13,7 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s /* update how much space will be required in the real buffer */ { - const int aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); + const int32_t aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); *offset += (aligned_offset - *offset); *offset += size; } @@ -21,8 +21,8 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s /* find a scratch buffer to hold the temporary data */ buffer = &scratch->buffer; do { - const int aligned_buffer_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); - const int remaining = sizeof(buffer->buffer) - aligned_buffer_offset; + const uint32_t aligned_buffer_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const uint32_t remaining = sizeof(buffer->buffer) - aligned_buffer_offset; if (remaining >= size) { /* claim the required space from an existing buffer */ @@ -36,13 +36,13 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s } while (1); /* not enough space in any existing buffer, allocate more */ - if (size > (int)sizeof(buffer->buffer)) { + if (size > (uint32_t)sizeof(buffer->buffer)) { /* caller is asking for more than we can fit in a standard rc_scratch_buffer_t. * leverage the fact that the buffer is the last field and extend its size. * this chunk will be exactly large enough to hold the needed data, and since offset * will exceed sizeof(buffer->buffer), it will never be eligible to hold anything else. */ - const int needed = sizeof(rc_scratch_buffer_t) - sizeof(buffer->buffer) + size; + const size_t needed = sizeof(rc_scratch_buffer_t) - sizeof(buffer->buffer) + size; buffer->next = (rc_scratch_buffer_t*)malloc(needed); } else { @@ -62,7 +62,7 @@ void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_s return rc_alloc(buffer->buffer, &buffer->offset, size, alignment, NULL, -1); } -void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset) { +void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset) { void* ptr; *offset = (*offset + alignment - 1) & ~(alignment - 1); @@ -89,8 +89,8 @@ void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t return ptr; } -char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length) { - int used = 0; +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length) { + int32_t used = 0; char* ptr; rc_scratch_string_t** next = &parse->scratch.strings; @@ -109,7 +109,7 @@ char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length) { } *next = (rc_scratch_string_t*)rc_alloc_scratch(NULL, &used, sizeof(rc_scratch_string_t), RC_ALIGNOF(rc_scratch_string_t), &parse->scratch, RC_OFFSETOF(parse->scratch.objs, __rc_scratch_string_t)); - ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, length + 1, RC_ALIGNOF(char), &parse->scratch, -1); + ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, (uint32_t)length + 1, RC_ALIGNOF(char), &parse->scratch, -1); if (!ptr || !*next) { if (parse->offset >= 0) @@ -158,38 +158,3 @@ void rc_destroy_parse_state(rc_parse_state_t* parse) buffer = next; } } - -const char* rc_error_str(int ret) -{ - switch (ret) { - case RC_OK: return "OK"; - case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; - case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; - case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; - case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; - case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; - case RC_INVALID_OPERATOR: return "Invalid operator"; - case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; - case RC_DUPLICATED_START: return "Duplicated start condition"; - case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; - case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; - case RC_DUPLICATED_VALUE: return "Duplicated value expression"; - case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; - case RC_MISSING_START: return "Missing start condition"; - case RC_MISSING_CANCEL: return "Missing cancel condition"; - case RC_MISSING_SUBMIT: return "Missing submit condition"; - case RC_MISSING_VALUE: return "Missing value expression"; - case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; - case RC_MISSING_DISPLAY_STRING: return "Missing display string"; - case RC_OUT_OF_MEMORY: return "Out of memory"; - case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; - case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; - case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; - case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; - case RC_INVALID_COMPARISON: return "Invalid comparison"; - case RC_INVALID_STATE: return "Invalid state"; - case RC_INVALID_JSON: return "Invalid JSON"; - - default: return "Unknown error"; - } -} diff --git a/deps/rcheevos/src/rcheevos/condition.c b/deps/rcheevos/src/rcheevos/condition.c index 55a8084e7199..236dff373478 100644 --- a/deps/rcheevos/src/rcheevos/condition.c +++ b/deps/rcheevos/src/rcheevos/condition.c @@ -3,7 +3,7 @@ #include #include -static int rc_test_condition_compare(unsigned value1, unsigned value2, char oper) { +static int rc_test_condition_compare(uint32_t value1, uint32_t value2, uint8_t oper) { switch (oper) { case RC_OPERATOR_EQ: return value1 == value2; case RC_OPERATOR_NE: return value1 != value2; @@ -149,7 +149,7 @@ static int rc_parse_operator(const char** memaddr) { } } -rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect) { +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect) { rc_condition_t* self; const char* aux; int result; @@ -323,23 +323,23 @@ int rc_condition_is_combining(const rc_condition_t* self) { } static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { - const unsigned value1 = self->operand1.value.memref->value.value; - const unsigned value2 = self->operand2.value.num; + const uint32_t value1 = self->operand1.value.memref->value.value; + const uint32_t value2 = self->operand2.value.num; assert(self->operand1.size == self->operand1.value.memref->value.size); return rc_test_condition_compare(value1, value2, self->oper); } static int rc_test_condition_compare_delta_to_const(rc_condition_t* self) { const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; - const unsigned value1 = (memref1->changed) ? memref1->prior : memref1->value; - const unsigned value2 = self->operand2.value.num; + const uint32_t value1 = (memref1->changed) ? memref1->prior : memref1->value; + const uint32_t value2 = self->operand2.value.num; assert(self->operand1.size == self->operand1.value.memref->value.size); return rc_test_condition_compare(value1, value2, self->oper); } static int rc_test_condition_compare_memref_to_memref(rc_condition_t* self) { - const unsigned value1 = self->operand1.value.memref->value.value; - const unsigned value2 = self->operand2.value.memref->value.value; + const uint32_t value1 = self->operand1.value.memref->value.value; + const uint32_t value2 = self->operand2.value.memref->value.value; assert(self->operand1.size == self->operand1.value.memref->value.size); assert(self->operand2.size == self->operand2.value.memref->value.size); return rc_test_condition_compare(value1, value2, self->oper); @@ -387,7 +387,7 @@ static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { rc_typed_value_t value1; - const unsigned value2 = self->operand2.value.num; + const uint32_t value2 = self->operand2.value.num; value1.type = RC_VALUE_TYPE_UNSIGNED; value1.value.u32 = self->operand1.value.memref->value.value; @@ -399,7 +399,7 @@ static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* static int rc_test_condition_compare_delta_to_const_transformed(rc_condition_t* self) { rc_typed_value_t value1; const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; - const unsigned value2 = self->operand2.value.num; + const uint32_t value2 = self->operand2.value.num; value1.type = RC_VALUE_TYPE_UNSIGNED; value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; diff --git a/deps/rcheevos/src/rcheevos/condset.c b/deps/rcheevos/src/rcheevos/condset.c index e77a2358152b..4c2e7ea37e24 100644 --- a/deps/rcheevos/src/rcheevos/condset.c +++ b/deps/rcheevos/src/rcheevos/condset.c @@ -28,7 +28,7 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in rc_condset_t* self; rc_condition_t** next; int in_add_address; - unsigned measured_target = 0; + uint32_t measured_target = 0; self = RC_ALLOC(rc_condset_t, parse); self->has_pause = self->is_paused = self->has_indirect_memrefs = 0; @@ -181,7 +181,7 @@ static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc rc_typed_value_t value; int set_valid, cond_valid, and_next, or_next, reset_next, measured_from_hits, can_measure; rc_typed_value_t measured_value; - unsigned total_hits; + uint32_t total_hits; measured_value.type = RC_VALUE_TYPE_NONE; measured_from_hits = 0; diff --git a/deps/rcheevos/src/rcheevos/consoleinfo.c b/deps/rcheevos/src/rcheevos/consoleinfo.c index 10905ad9024d..985c42922085 100644 --- a/deps/rcheevos/src/rcheevos/consoleinfo.c +++ b/deps/rcheevos/src/rcheevos/consoleinfo.c @@ -444,11 +444,13 @@ static const rc_memory_regions_t rc_memory_regions_gameboy = { _rc_memory_region static const rc_memory_regions_t rc_memory_regions_gameboy_color = { _rc_memory_regions_gameboy, 17 }; /* ===== GameBoy Advance ===== */ +/* http://problemkaputt.de/gbatek-gba-memory-map.htm */ static const rc_memory_region_t _rc_memory_regions_gameboy_advance[] = { - { 0x000000U, 0x007FFFU, 0x03000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, - { 0x008000U, 0x047FFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + { 0x000000U, 0x007FFFU, 0x03000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* 32KB Internal Work RAM */ + { 0x008000U, 0x047FFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* 256KB External Work RAM */ + { 0x048000U, 0x057FFFU, 0x0E000000U, RC_MEMORY_TYPE_SAVE_RAM, "Save RAM" } /* 64KB Game Pak SRAM */ }; -static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 2 }; +static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 3 }; /* ===== GameCube ===== */ /* https://wiibrew.org/wiki/Memory_map */ @@ -541,11 +543,11 @@ static const rc_memory_region_t _rc_memory_regions_megadrive[] = { static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; /* ===== MegaDrive 32X (Genesis 32X) ===== */ -/* https://en.wikibooks.org/wiki/Genesis_Programming/68K_Memory_map/ */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { - { 0x000000U, 0x00FFFFU, 0xFF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, - { 0x010000U, 0x04FFFFU, 0x200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, - { 0x050000U, 0x05FFFFU, 0x000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Main MegaDrive RAM */ + { 0x010000U, 0x04FFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, /* Additional 32X RAM */ + { 0x050000U, 0x05FFFFU, 0x00000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } }; static const rc_memory_regions_t rc_memory_regions_megadrive_32x = { _rc_memory_regions_megadrive_32x, 3 }; @@ -759,16 +761,19 @@ static const rc_memory_region_t _rc_memory_regions_sg1000[] = { static const rc_memory_regions_t rc_memory_regions_sg1000 = { _rc_memory_regions_sg1000, 4 }; /* ===== Super Cassette Vision ===== */ +/* https://github.com/mamedev/mame/blob/f32bb79e8541ba96d3a8144b220c48fb7536ba4b/src/mame/epoch/scv.cpp#L78-L86 */ +/* SCV only has 128 bytes of system RAM, any additional memory is provided on the individual carts and is + * not backed up by battery. */ +/* http://www.videogameconsolelibrary.com/pg80-super_cass_vis.htm#page=specs */ static const rc_memory_region_t _rc_memory_regions_scv[] = { - { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_READONLY, "System ROM" }, + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_READONLY, "System ROM" }, /* BIOS */ { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, - { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, /* only really goes to $33FF? */ { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_UNUSED, "" }, - { 0x008000U, 0x00DFFFU, 0x008000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM" }, - { 0x00E000U, 0x00FF7FU, 0x00E000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x008000U, 0x00FF7FU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, { 0x00FF80U, 0x00FFFFU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } }; -static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_scv, 7 }; +static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_scv, 6 }; /* ===== Super Nintendo ===== */ /* https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM */ @@ -877,7 +882,7 @@ static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_reg /* ===== default ===== */ static const rc_memory_regions_t rc_memory_regions_none = { 0, 0 }; -const rc_memory_regions_t* rc_console_memory_regions(int console_id) +const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) { switch (console_id) { diff --git a/deps/rcheevos/src/rcheevos/format.c b/deps/rcheevos/src/rcheevos/format.c index 1129f950ba9f..c797faeb422f 100644 --- a/deps/rcheevos/src/rcheevos/format.c +++ b/deps/rcheevos/src/rcheevos/format.c @@ -1,6 +1,6 @@ #include "rc_internal.h" -#include "rc_compat.h" +#include "../rc_compat.h" #include #include @@ -75,16 +75,16 @@ int rc_parse_format(const char* format_str) { return RC_FORMAT_VALUE; } -static int rc_format_value_minutes(char* buffer, int size, unsigned minutes) { - unsigned hours; +static int rc_format_value_minutes(char* buffer, size_t size, uint32_t minutes) { + uint32_t hours; hours = minutes / 60; minutes -= hours * 60; return snprintf(buffer, size, "%uh%02u", hours, minutes); } -static int rc_format_value_seconds(char* buffer, int size, unsigned seconds) { - unsigned hours, minutes; +static int rc_format_value_seconds(char* buffer, size_t size, uint32_t seconds) { + uint32_t hours, minutes; /* apply modulus math to split the seconds into hours/minutes/seconds */ minutes = seconds / 60; @@ -98,8 +98,8 @@ static int rc_format_value_seconds(char* buffer, int size, unsigned seconds) { return snprintf(buffer, size, "%uh%02u:%02u", hours, minutes, seconds); } -static int rc_format_value_centiseconds(char* buffer, int size, unsigned centiseconds) { - unsigned seconds; +static int rc_format_value_centiseconds(char* buffer, size_t size, uint32_t centiseconds) { + uint32_t seconds; int chars, chars2; /* modulus off the centiseconds */ @@ -119,7 +119,7 @@ static int rc_format_value_centiseconds(char* buffer, int size, unsigned centise return chars; } -int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, int format) { +int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format) { int chars; rc_typed_value_t converted_value; @@ -197,7 +197,7 @@ int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, return chars; } -int rc_format_value(char* buffer, int size, int value, int format) { +int rc_format_value(char* buffer, int size, int32_t value, int format) { rc_typed_value_t typed_value; typed_value.value.i32 = value; diff --git a/deps/rcheevos/src/rcheevos/lboard.c b/deps/rcheevos/src/rcheevos/lboard.c index f7b1ec5a76c1..0191336a2115 100644 --- a/deps/rcheevos/src/rcheevos/lboard.c +++ b/deps/rcheevos/src/rcheevos/lboard.c @@ -164,7 +164,7 @@ rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, in return (parse.offset >= 0) ? self : 0; } -int rc_evaluate_lboard(rc_lboard_t* self, int* value, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L) { int start_ok, cancel_ok, submit_ok; rc_update_memref_values(self->memrefs, peek, peek_ud); @@ -262,6 +262,9 @@ int rc_lboard_state_active(int state) { } void rc_reset_lboard(rc_lboard_t* self) { + if (!self) + return; + self->state = RC_LBOARD_STATE_WAITING; rc_reset_trigger(&self->start); diff --git a/deps/rcheevos/src/rcheevos/memref.c b/deps/rcheevos/src/rcheevos/memref.c index 964f68490d69..87f6ec0bf1bd 100644 --- a/deps/rcheevos/src/rcheevos/memref.c +++ b/deps/rcheevos/src/rcheevos/memref.c @@ -6,7 +6,7 @@ #define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF -rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect) { +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size, uint8_t is_indirect) { rc_memref_t** next_memref; rc_memref_t* memref; @@ -41,7 +41,7 @@ rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char siz return memref; } -int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address) { const char* aux = *memaddr; char* end; unsigned long value; @@ -94,6 +94,7 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { ++aux; switch (*aux++) { case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; + case 'b': case 'B': *size = RC_MEMSIZE_FLOAT_BE; break; case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; @@ -113,15 +114,15 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { if (value > 0xffffffffU) value = 0xffffffffU; - *address = (unsigned)value; + *address = (uint32_t)value; *memaddr = end; return RC_OK; } -static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { +static float rc_build_float(uint32_t mantissa_bits, int32_t exponent, int sign) { /* 32-bit float has a 23-bit mantissa and 8-bit exponent */ - const unsigned implied_bit = 1 << 23; - const unsigned mantissa = mantissa_bits | implied_bit; + const uint32_t implied_bit = 1 << 23; + const uint32_t mantissa = mantissa_bits | implied_bit; double dbl = ((double)mantissa) / ((double)implied_bit); if (exponent > 127) { @@ -178,20 +179,32 @@ static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { static void rc_transform_memref_float(rc_typed_value_t* value) { /* decodes an IEEE 754 float */ - const unsigned mantissa = (value->value.u32 & 0x7FFFFF); - const int exponent = (int)((value->value.u32 >> 23) & 0xFF) - 127; + const uint32_t mantissa = (value->value.u32 & 0x7FFFFF); + const int32_t exponent = (int32_t)((value->value.u32 >> 23) & 0xFF) - 127; const int sign = (value->value.u32 & 0x80000000); value->value.f32 = rc_build_float(mantissa, exponent, sign); value->type = RC_VALUE_TYPE_FLOAT; } +static void rc_transform_memref_float_be(rc_typed_value_t* value) { + /* decodes an IEEE 754 float in big endian format */ + const uint32_t mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00007F00) << 8); + const int32_t exponent = (int32_t)(((value->value.u32 & 0x0000007F) << 1) | + ((value->value.u32 & 0x00008000) >> 15)) - 127; + const int sign = (value->value.u32 & 0x00000080); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + static void rc_transform_memref_mbf32(rc_typed_value_t* value) { /* decodes a Microsoft Binary Format float */ /* NOTE: 32-bit MBF is stored in memory as big endian (at least for Apple II) */ - const unsigned mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + const uint32_t mantissa = ((value->value.u32 & 0xFF000000) >> 24) | ((value->value.u32 & 0x00FF0000) >> 8) | ((value->value.u32 & 0x00007F00) << 8); - const int exponent = (int)(value->value.u32 & 0xFF) - 129; + const int32_t exponent = (int32_t)(value->value.u32 & 0xFF) - 129; const int sign = (value->value.u32 & 0x00008000); if (mantissa == 0 && exponent == -129) @@ -205,8 +218,8 @@ static void rc_transform_memref_mbf32(rc_typed_value_t* value) { static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { /* decodes a Microsoft Binary Format float */ /* Locomotive BASIC (CPC) uses MBF40, but in little endian format */ - const unsigned mantissa = value->value.u32 & 0x007FFFFF; - const int exponent = (int)(value->value.u32 >> 24) - 129; + const uint32_t mantissa = value->value.u32 & 0x007FFFFF; + const int32_t exponent = (int32_t)(value->value.u32 >> 24) - 129; const int sign = (value->value.u32 & 0x00800000); if (mantissa == 0 && exponent == -129) @@ -217,9 +230,9 @@ static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { value->type = RC_VALUE_TYPE_FLOAT; } -static const unsigned char rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; +static const uint8_t rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; -void rc_transform_memref_value(rc_typed_value_t* value, char size) { +void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size) { /* ASSERT: value->type == RC_VALUE_TYPE_UNSIGNED */ switch (size) { @@ -305,6 +318,10 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) { rc_transform_memref_float(value); break; + case RC_MEMSIZE_FLOAT_BE: + rc_transform_memref_float_be(value); + break; + case RC_MEMSIZE_MBF32: rc_transform_memref_mbf32(value); break; @@ -318,7 +335,7 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) { } } -static const unsigned rc_memref_masks[] = { +static const uint32_t rc_memref_masks[] = { 0x000000ff, /* RC_MEMSIZE_8_BITS */ 0x0000ffff, /* RC_MEMSIZE_16_BITS */ 0x00ffffff, /* RC_MEMSIZE_24_BITS */ @@ -340,10 +357,11 @@ static const unsigned rc_memref_masks[] = { 0xffffffff, /* RC_MEMSIZE_FLOAT */ 0xffffffff, /* RC_MEMSIZE_MBF32 */ 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT_BE */ 0xffffffff /* RC_MEMSIZE_VARIABLE */ }; -unsigned rc_memref_mask(char size) { +uint32_t rc_memref_mask(uint8_t size) { const size_t index = (size_t)size; if (index >= sizeof(rc_memref_masks) / sizeof(rc_memref_masks[0])) return 0xffffffff; @@ -354,7 +372,7 @@ unsigned rc_memref_mask(char size) { /* all sizes less than 8-bits (1 byte) are mapped to 8-bits. 24-bit is mapped to 32-bit * as we don't expect the client to understand a request for 3 bytes. all other reads are * mapped to the little-endian read of the same size. */ -static const char rc_memref_shared_sizes[] = { +static const uint8_t rc_memref_shared_sizes[] = { RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_8_BITS */ RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS */ @@ -376,10 +394,11 @@ static const char rc_memref_shared_sizes[] = { RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT_BE */ RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ }; -char rc_memref_shared_size(char size) { +uint8_t rc_memref_shared_size(uint8_t size) { const size_t index = (size_t)size; if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) return size; @@ -387,7 +406,7 @@ char rc_memref_shared_size(char size) { return rc_memref_shared_sizes[index]; } -static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud) { if (!peek) return 0; @@ -404,7 +423,7 @@ static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* default: { - unsigned value; + uint32_t value; const size_t index = (size_t)size; if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) return 0; @@ -418,7 +437,7 @@ static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* } } -void rc_update_memref_value(rc_memref_value_t* memref, unsigned new_value) { +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t new_value) { if (memref->value == new_value) { memref->changed = 0; } @@ -444,7 +463,7 @@ void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) *memrefs = 0; } -static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { +static uint32_t rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { switch (operand_type) { /* most common case explicitly first, even though it could be handled by default case. @@ -464,10 +483,10 @@ static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int o } } -unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state) { +uint32_t rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state) { /* if this is an indirect reference, handle the indirection. */ if (memref->value.is_indirect) { - const unsigned new_address = memref->address + eval_state->add_address; + const uint32_t new_address = memref->address + eval_state->add_address; rc_update_memref_value(&memref->value, rc_peek_value(new_address, memref->value.size, eval_state->peek, eval_state->peek_userdata)); } diff --git a/deps/rcheevos/src/rcheevos/operand.c b/deps/rcheevos/src/rcheevos/operand.c index f8abbbd7ba0f..d7e20908f8b6 100644 --- a/deps/rcheevos/src/rcheevos/operand.c +++ b/deps/rcheevos/src/rcheevos/operand.c @@ -68,10 +68,10 @@ static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_par return RC_OK; } -static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse, int is_indirect) { +static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect) { const char* aux = *memaddr; - unsigned address; - char size; + uint32_t address; + uint8_t size; int ret; switch (*aux) { @@ -115,7 +115,7 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ size = self->size; } - self->value.memref = rc_alloc_memref(parse, address, size, (char)is_indirect); + self->value.memref = rc_alloc_memref(parse, address, size, is_indirect); if (parse->offset < 0) return parse->offset; @@ -123,7 +123,7 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ return RC_OK; } -int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse) { +int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indirect, rc_parse_state_t* parse) { const char* aux = *memaddr; char* end; int ret; @@ -286,11 +286,11 @@ typedef struct { rc_luapeek_t; static int rc_luapeek(lua_State* L) { - unsigned address = (unsigned)luaL_checkinteger(L, 1); - unsigned num_bytes = (unsigned)luaL_checkinteger(L, 2); + uint32_t address = (uint32_t)luaL_checkinteger(L, 1); + uint32_t num_bytes = (uint32_t)luaL_checkinteger(L, 2); rc_luapeek_t* luapeek = (rc_luapeek_t*)lua_touserdata(L, 3); - unsigned value = luapeek->peek(address, num_bytes, luapeek->ud); + uint32_t value = luapeek->peek(address, num_bytes, luapeek->ud); lua_pushinteger(L, value); return 1; @@ -301,6 +301,7 @@ static int rc_luapeek(lua_State* L) { int rc_operand_is_float_memref(const rc_operand_t* self) { switch (self->size) { case RC_MEMSIZE_FLOAT: + case RC_MEMSIZE_FLOAT_BE: case RC_MEMSIZE_MBF32: case RC_MEMSIZE_MBF32_LE: return 1; @@ -329,7 +330,7 @@ int rc_operand_is_float(const rc_operand_t* self) { return rc_operand_is_float_memref(self); } -unsigned rc_transform_operand_value(unsigned value, const rc_operand_t* self) { +uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { switch (self->type) { case RC_OPERAND_BCD: @@ -450,10 +451,10 @@ void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_s if (lua_pcall(eval_state->L, 2, 1, 0) == LUA_OK) { if (lua_isboolean(eval_state->L, -1)) { - result->value.u32 = (unsigned)lua_toboolean(eval_state->L, -1); + result->value.u32 = (uint32_t)lua_toboolean(eval_state->L, -1); } else { - result->value.u32 = (unsigned)lua_tonumber(eval_state->L, -1); + result->value.u32 = (uint32_t)lua_tonumber(eval_state->L, -1); } } diff --git a/deps/rcheevos/src/rcheevos/rc_internal.h b/deps/rcheevos/src/rcheevos/rc_internal.h index 7f80d179ab8a..4a2f306a8977 100644 --- a/deps/rcheevos/src/rcheevos/rc_internal.h +++ b/deps/rcheevos/src/rcheevos/rc_internal.h @@ -1,5 +1,5 @@ -#ifndef INTERNAL_H -#define INTERNAL_H +#ifndef RC_INTERNAL_H +#define RC_INTERNAL_H #include "rc_runtime_types.h" @@ -36,10 +36,13 @@ RC_ALLOW_ALIGN(char) #define RC_ALLOC(t, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) #define RC_ALLOC_SCRATCH(t, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +/* force alignment to 4 bytes on 32-bit systems, or 8 bytes on 64-bit systems */ +#define RC_ALIGN(n) (((n) + (sizeof(void*)-1)) & ~(sizeof(void*)-1)) + typedef struct rc_scratch_buffer { struct rc_scratch_buffer* next; - int offset; - unsigned char buffer[512 - 16]; + int32_t offset; + uint8_t buffer[512 - 16]; } rc_scratch_buffer_t; @@ -74,8 +77,8 @@ enum { typedef struct { union { - unsigned u32; - int i32; + uint32_t u32; + int32_t i32; float f32; } value; @@ -87,24 +90,24 @@ rc_typed_value_t; typedef struct { rc_typed_value_t add_value;/* AddSource/SubSource */ - int add_hits; /* AddHits */ - unsigned add_address; /* AddAddress */ + int32_t add_hits; /* AddHits */ + uint32_t add_address; /* AddAddress */ rc_peek_t peek; void* peek_userdata; lua_State* L; rc_typed_value_t measured_value; /* Measured */ - char was_reset; /* ResetIf triggered */ - char has_hits; /* one of more hit counts is non-zero */ - char primed; /* true if all non-Trigger conditions are true */ - char measured_from_hits; /* true if the measured_value came from a condition's hit count */ - char was_cond_reset; /* ResetNextIf triggered */ + uint8_t was_reset; /* ResetIf triggered */ + uint8_t has_hits; /* one of more hit counts is non-zero */ + uint8_t primed; /* true if all non-Trigger conditions are true */ + uint8_t measured_from_hits; /* true if the measured_value came from a condition's hit count */ + uint8_t was_cond_reset; /* ResetNextIf triggered */ } rc_eval_state_t; typedef struct { - int offset; + int32_t offset; lua_State* L; int funcs_ndx; @@ -115,11 +118,11 @@ typedef struct { rc_memref_t** first_memref; rc_value_t** variables; - unsigned measured_target; + uint32_t measured_target; int lines_read; - char has_required_hits; - char measured_as_percent; + uint8_t has_required_hits; + uint8_t measured_as_percent; } rc_parse_state_t; @@ -128,18 +131,19 @@ void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables); void rc_destroy_parse_state(rc_parse_state_t* parse); -void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); -void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); -char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length); +void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); +void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length); -rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect); -int rc_parse_memref(const char** memaddr, char* size, unsigned* address); +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size, uint8_t is_indirect); +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address); void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud); -void rc_update_memref_value(rc_memref_value_t* memref, unsigned value); -unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state); -char rc_memref_shared_size(char size); -unsigned rc_memref_mask(char size); -void rc_transform_memref_value(rc_typed_value_t* value, char size); +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t value); +uint32_t rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state); +uint8_t rc_memref_shared_size(uint8_t size); +uint32_t rc_memref_mask(uint8_t size); +void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size); +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud); void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_trigger_state_active(int state); @@ -164,12 +168,12 @@ enum { RC_PROCESSING_COMPARE_ALWAYS_FALSE }; -rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect); +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect); int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state); int rc_condition_is_combining(const rc_condition_t* self); -int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse); +int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indirect, rc_parse_state_t* parse); void rc_evaluate_operand(rc_typed_value_t* value, rc_operand_t* self, rc_eval_state_t* eval_state); int rc_operand_is_float_memref(const rc_operand_t* self); int rc_operand_is_float(const rc_operand_t* self); @@ -177,7 +181,8 @@ int rc_operand_is_float(const rc_operand_t* self); void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L); void rc_reset_value(rc_value_t* self); -rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse); +int rc_value_from_hits(rc_value_t* self); +rc_value_t* rc_alloc_helper_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse); void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L); void rc_typed_value_convert(rc_typed_value_t* value, char new_type); @@ -188,7 +193,7 @@ void rc_typed_value_negate(rc_typed_value_t* value); int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper); void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); -int rc_format_typed_value(char* buffer, int size, const rc_typed_value_t* value, int format); +int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format); void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse); int rc_lboard_state_active(int state); @@ -199,4 +204,4 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, } #endif -#endif /* INTERNAL_H */ +#endif /* RC_INTERNAL_H */ diff --git a/deps/rcheevos/src/rcheevos/richpresence.c b/deps/rcheevos/src/rcheevos/richpresence.c index b7d883e81c3f..db58c76c0003 100644 --- a/deps/rcheevos/src/rcheevos/richpresence.c +++ b/deps/rcheevos/src/rcheevos/richpresence.c @@ -1,6 +1,6 @@ #include "rc_internal.h" -#include "rc_compat.h" +#include "../rc_compat.h" #include @@ -16,8 +16,8 @@ enum { static rc_memref_value_t* rc_alloc_helper_variable_memref_value(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { const char* end; rc_value_t* variable; - unsigned address; - char size; + uint32_t address; + uint8_t size; /* single memory reference lookups without a modifier flag can be handled without a variable */ end = memaddr; @@ -78,7 +78,7 @@ static const char* rc_parse_line(const char* line, const char** end, rc_parse_st typedef struct rc_richpresence_builtin_macro_t { const char* name; size_t name_len; - unsigned short display_type; + uint8_t display_type; } rc_richpresence_builtin_macro_t; static rc_richpresence_display_t* rc_parse_richpresence_display_internal(const char* line, const char* endline, rc_parse_state_t* parse, rc_richpresence_lookup_t* first_lookup) { @@ -280,7 +280,6 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo { rc_richpresence_lookup_item_t** items; rc_scratch_buffer_t* buffer; - const int alignment = sizeof(rc_richpresence_lookup_item_t*); int index; int size; @@ -293,7 +292,7 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo size = count * sizeof(rc_richpresence_lookup_item_t*); buffer = &parse->scratch.buffer; do { - const int aligned_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const int aligned_offset = RC_ALIGN(buffer->offset); const int remaining = sizeof(buffer->buffer) - aligned_offset; if (remaining >= size) { @@ -325,7 +324,7 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo } static void rc_insert_richpresence_lookup_item(rc_richpresence_lookup_t* lookup, - unsigned first, unsigned last, const char* label, int label_len, rc_parse_state_t* parse) + uint32_t first, uint32_t last, const char* label, size_t label_len, rc_parse_state_t* parse) { rc_richpresence_lookup_item_t** next; rc_richpresence_lookup_item_t* item; @@ -371,7 +370,7 @@ static const char* rc_parse_richpresence_lookup(rc_richpresence_lookup_t* lookup const char* endline; const char* label; char* endptr = 0; - unsigned first, last; + uint32_t first, last; int base; do @@ -526,7 +525,7 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, memcpy(format, line, chars); format[chars] = '\0'; - lookup->format = (unsigned short)rc_parse_format(format); + lookup->format = (uint8_t)rc_parse_format(format); } else { lookup->format = RC_FORMAT_VALUE; } @@ -664,7 +663,7 @@ void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, voi } } -static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part, char* buffer, unsigned buffersize) +static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part, char* buffer, size_t buffersize) { rc_richpresence_lookup_item_t* item; rc_typed_value_t value; @@ -808,7 +807,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part return (int)(ptr - buffer); } -int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { rc_richpresence_display_t* display; for (display = richpresence->first_display; display; display = display->next) { @@ -829,7 +828,7 @@ int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* bu return 0; } -int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, unsigned buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { rc_update_richpresence(richpresence, peek, peek_ud, L); return rc_get_richpresence_display_string(richpresence, buffer, buffersize, peek, peek_ud, L); } diff --git a/deps/rcheevos/src/rcheevos/runtime.c b/deps/rcheevos/src/rcheevos/runtime.c index b5b55692706b..d663b7a9596d 100644 --- a/deps/rcheevos/src/rcheevos/runtime.c +++ b/deps/rcheevos/src/rcheevos/runtime.c @@ -1,7 +1,7 @@ #include "rc_runtime.h" #include "rc_internal.h" -#include "rc_compat.h" +#include "../rc_compat.h" #include "../rhash/md5.h" #include @@ -9,6 +9,17 @@ #define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 +rc_runtime_t* rc_runtime_alloc(void) { + rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); + + if (self) { + rc_runtime_init(self); + self->owns_self = 1; + } + + return self; +} + void rc_runtime_init(rc_runtime_t* self) { memset(self, 0, sizeof(rc_runtime_t)); self->next_memref = &self->memrefs; @@ -16,7 +27,7 @@ void rc_runtime_init(rc_runtime_t* self) { } void rc_runtime_destroy(rc_runtime_t* self) { - unsigned i; + uint32_t i; if (self->triggers) { for (i = 0; i < self->trigger_count; ++i) @@ -48,9 +59,13 @@ void rc_runtime_destroy(rc_runtime_t* self) { self->next_memref = 0; self->memrefs = 0; + + if (self->owns_self) { + free(self); + } } -static void rc_runtime_checksum(const char* memaddr, unsigned char* md5) { +void rc_runtime_checksum(const char* memaddr, uint8_t* md5) { md5_state_t state; md5_init(&state); md5_append(&state, (unsigned char*)memaddr, (int)strlen(memaddr)); @@ -81,7 +96,7 @@ static char rc_runtime_allocated_memrefs(rc_runtime_t* self) { return owns_memref; } -static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, unsigned index) { +static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, uint32_t index) { if (self->triggers[index].owns_memrefs) { /* if the trigger has one or more memrefs in its buffer, we can't free the buffer. * just null out the trigger so the runtime processor will skip it @@ -98,8 +113,8 @@ static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, unsigned } } -void rc_runtime_deactivate_achievement(rc_runtime_t* self, unsigned id) { - unsigned i; +void rc_runtime_deactivate_achievement(rc_runtime_t* self, uint32_t id) { + uint32_t i; for (i = 0; i < self->trigger_count; ++i) { if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) @@ -107,14 +122,14 @@ void rc_runtime_deactivate_achievement(rc_runtime_t* self, unsigned id) { } } -int rc_runtime_activate_achievement(rc_runtime_t* self, unsigned id, const char* memaddr, lua_State* L, int funcs_idx) { +int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { void* trigger_buffer; rc_trigger_t* trigger; rc_runtime_trigger_t* runtime_trigger; rc_parse_state_t parse; - unsigned char md5[16]; + uint8_t md5[16]; int size; - unsigned i; + uint32_t i; if (memaddr == NULL) return RC_INVALID_MEMORY_OPERAND; @@ -207,9 +222,9 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, unsigned id, const char* return RC_OK; } -rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, unsigned id) +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, uint32_t id) { - unsigned i; + uint32_t i; for (i = 0; i < self->trigger_count; ++i) { if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) @@ -219,7 +234,7 @@ rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, unsigned id) return NULL; } -int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id, unsigned* measured_value, unsigned* measured_target) +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target) { const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); if (!measured_value || !measured_target) @@ -242,10 +257,10 @@ int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id return 1; } -int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned id, char* buffer, size_t buffer_size) +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char* buffer, size_t buffer_size) { const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); - unsigned value; + uint32_t value; if (!buffer || !buffer_size) return 0; @@ -262,14 +277,14 @@ int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned value = trigger->measured_target; if (trigger->measured_as_percent) { - unsigned percent = (unsigned)(((unsigned long long)value * 100) / trigger->measured_target); + const uint32_t percent = (uint32_t)(((unsigned long long)value * 100) / trigger->measured_target); return snprintf(buffer, buffer_size, "%u%%", percent); } return snprintf(buffer, buffer_size, "%u/%u", value, trigger->measured_target); } -static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, unsigned index) { +static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, uint32_t index) { if (self->lboards[index].owns_memrefs) { /* if the lboard has one or more memrefs in its buffer, we can't free the buffer. * just null out the lboard so the runtime processor will skip it @@ -286,8 +301,8 @@ static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, unsigned i } } -void rc_runtime_deactivate_lboard(rc_runtime_t* self, unsigned id) { - unsigned i; +void rc_runtime_deactivate_lboard(rc_runtime_t* self, uint32_t id) { + uint32_t i; for (i = 0; i < self->lboard_count; ++i) { if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) @@ -295,14 +310,14 @@ void rc_runtime_deactivate_lboard(rc_runtime_t* self, unsigned id) { } } -int rc_runtime_activate_lboard(rc_runtime_t* self, unsigned id, const char* memaddr, lua_State* L, int funcs_idx) { +int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { void* lboard_buffer; - unsigned char md5[16]; + uint8_t md5[16]; rc_lboard_t* lboard; rc_parse_state_t parse; rc_runtime_lboard_t* runtime_lboard; int size; - unsigned i; + uint32_t i; if (memaddr == 0) return RC_INVALID_MEMORY_OPERAND; @@ -395,9 +410,9 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, unsigned id, const char* mema return RC_OK; } -rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, unsigned id) +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, uint32_t id) { - unsigned i; + uint32_t i; for (i = 0; i < self->lboard_count; ++i) { if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) @@ -407,7 +422,7 @@ rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, unsigned id) return NULL; } -int rc_runtime_format_lboard_value(char* buffer, int size, int value, int format) +int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format) { return rc_format_value(buffer, size, value, format); } @@ -417,7 +432,7 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua rc_runtime_richpresence_t* previous; rc_runtime_richpresence_t** previous_ptr; rc_parse_state_t parse; - unsigned char md5[16]; + uint8_t md5[16]; int size; if (script == NULL) @@ -514,7 +529,7 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua return RC_OK; } -int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, unsigned buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L) { +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L) { if (self->richpresence && self->richpresence->richpresence) return rc_get_richpresence_display_string(self->richpresence->richpresence, buffer, buffersize, peek, peek_ud, L); @@ -534,7 +549,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha for (i = self->trigger_count - 1; i >= 0; --i) { rc_trigger_t* trigger = self->triggers[i].trigger; int old_state, new_state; - unsigned old_measured_value; + uint32_t old_measured_value; if (!trigger) continue; @@ -578,8 +593,8 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha if (trigger->measured_as_percent) { /* if reporting measured value as a percentage, only send the notification if the percentage changes */ - unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); - unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + const int32_t old_percent = (int32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const int32_t new_percent = (int32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); if (old_percent != new_percent) { runtime_event.value = new_percent; event_handler(&runtime_event); @@ -700,7 +715,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha void rc_runtime_reset(rc_runtime_t* self) { rc_value_t* variable; - unsigned i; + uint32_t i; for (i = 0; i < self->trigger_count; ++i) { if (self->triggers[i].trigger) @@ -712,13 +727,8 @@ void rc_runtime_reset(rc_runtime_t* self) { rc_reset_lboard(self->lboards[i].lboard); } - if (self->richpresence && self->richpresence->richpresence) { - rc_richpresence_display_t* display = self->richpresence->richpresence->first_display; - while (display != 0) { - rc_reset_trigger(&display->trigger); - display = display->next; - } - } + if (self->richpresence && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); for (variable = self->variables; variable; variable = variable->next) rc_reset_value(variable); @@ -739,7 +749,7 @@ static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memr return 0; } -static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { rc_condset_t* condset; if (!value) return 0; @@ -752,7 +762,7 @@ static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* return 0; } -static int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { rc_condset_t* condset; if (!trigger) return 0; @@ -769,7 +779,7 @@ static int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memr } static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref) { - unsigned i; + uint32_t i; /* disable any achievements dependent on the address */ for (i = 0; i < self->trigger_count; ++i) { @@ -804,7 +814,7 @@ static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref } } -void rc_runtime_invalidate_address(rc_runtime_t* self, unsigned address) { +void rc_runtime_invalidate_address(rc_runtime_t* self, uint32_t address) { rc_memref_t** last_memref = &self->memrefs; rc_memref_t* memref = self->memrefs; diff --git a/deps/rcheevos/src/rcheevos/runtime_progress.c b/deps/rcheevos/src/rcheevos/runtime_progress.c index cebe981c326d..d301377db8a7 100644 --- a/deps/rcheevos/src/rcheevos/runtime_progress.c +++ b/deps/rcheevos/src/rcheevos/runtime_progress.c @@ -1,6 +1,7 @@ #include "rc_runtime.h" #include "rc_internal.h" +#include "../rc_util.h" #include "../rhash/md5.h" #include @@ -17,12 +18,12 @@ #define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ typedef struct rc_runtime_progress_t { - rc_runtime_t* runtime; + const rc_runtime_t* runtime; - int offset; - unsigned char* buffer; + uint32_t offset; + uint8_t* buffer; - int chunk_size_offset; + uint32_t chunk_size_offset; lua_State* L; } rc_runtime_progress_t; @@ -39,7 +40,7 @@ typedef struct rc_runtime_progress_t { #define RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF 0x00100000 #define RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME 0x00200000 -static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, unsigned value) +static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, uint32_t value) { if (progress->buffer) { progress->buffer[progress->offset + 0] = value & 0xFF; value >>= 8; @@ -51,9 +52,9 @@ static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, unsi progress->offset += 4; } -static unsigned rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) +static uint32_t rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) { - unsigned value = progress->buffer[progress->offset + 0] | + uint32_t value = progress->buffer[progress->offset + 0] | (progress->buffer[progress->offset + 1] << 8) | (progress->buffer[progress->offset + 2] << 16) | (progress->buffer[progress->offset + 3] << 24); @@ -62,7 +63,7 @@ static unsigned rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) return value; } -static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, unsigned char* md5) +static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, uint8_t* md5) { if (progress->buffer) memcpy(&progress->buffer[progress->offset], md5, 16); @@ -70,7 +71,7 @@ static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, unsig progress->offset += 16; } -static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, unsigned char* md5) +static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, uint8_t* md5) { int result = 0; if (progress->buffer) @@ -81,18 +82,7 @@ static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, unsign return result; } -static unsigned rc_runtime_progress_djb2(const char* input) -{ - unsigned result = 5381; - char c; - - while ((c = *input++) != '\0') - result = ((result << 5) + result) + c; /* result = result * 33 + c */ - - return result; -} - -static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, unsigned chunk_id) +static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, uint32_t chunk_id) { rc_runtime_progress_write_uint(progress, chunk_id); @@ -103,14 +93,14 @@ static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, uns static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) { - unsigned length; - int offset; + uint32_t length; + uint32_t offset; progress->offset = (progress->offset + 3) & ~0x03; /* align to 4 byte boundary */ if (progress->buffer) { /* ignore chunk size field when calculating chunk size */ - length = (unsigned)(progress->offset - progress->chunk_size_offset - 4); + length = (uint32_t)(progress->offset - progress->chunk_size_offset - 4); /* temporarily update the write pointer to write the chunk size field */ offset = progress->offset; @@ -120,7 +110,7 @@ static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) } } -static void rc_runtime_progress_init(rc_runtime_progress_t* progress, rc_runtime_t* runtime, lua_State* L) +static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime, lua_State* L) { memset(progress, 0, sizeof(rc_runtime_progress_t)); progress->runtime = runtime; @@ -130,7 +120,7 @@ static void rc_runtime_progress_init(rc_runtime_progress_t* progress, rc_runtime static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) { rc_memref_t* memref = progress->runtime->memrefs; - unsigned int flags = 0; + uint32_t flags = 0; rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_MEMREFS); @@ -161,9 +151,9 @@ static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) static int rc_runtime_progress_read_memrefs(rc_runtime_progress_t* progress) { - unsigned entries; - unsigned address, flags, value, prior; - char size; + uint32_t entries; + uint32_t address, flags, value, prior; + uint8_t size; rc_memref_t* memref; rc_memref_t* first_unmatched_memref = progress->runtime->memrefs; @@ -218,7 +208,7 @@ static int rc_runtime_progress_is_indirect_memref(rc_operand_t* oper) static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) { rc_condition_t* cond; - unsigned flags; + uint32_t flags; rc_runtime_progress_write_uint(progress, condset->is_paused); @@ -262,7 +252,7 @@ static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) { rc_condition_t* cond; - unsigned flags; + uint32_t flags; condset->is_paused = (char)rc_runtime_progress_read_uint(progress); @@ -297,7 +287,7 @@ static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_ return RC_OK; } -static unsigned rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) +static uint32_t rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) { const rc_condition_t* condition; @@ -318,7 +308,7 @@ static unsigned rc_runtime_progress_should_serialize_variable_condset(const rc_c static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, const rc_value_t* variable) { - unsigned flags; + uint32_t flags; flags = rc_runtime_progress_should_serialize_variable_condset(variable->conditions); if (variable->value.changed) @@ -339,7 +329,7 @@ static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, c static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) { - unsigned count = 0; + uint32_t count = 0; const rc_value_t* variable; for (variable = progress->runtime->variables; variable; variable = variable->next) @@ -352,7 +342,7 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) for (variable = progress->runtime->variables; variable; variable = variable->next) { - unsigned djb2 = rc_runtime_progress_djb2(variable->name); + uint32_t djb2 = rc_djb2(variable->name); rc_runtime_progress_write_uint(progress, djb2); rc_runtime_progress_write_variable(progress, variable); @@ -364,7 +354,7 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) static int rc_runtime_progress_read_variable(rc_runtime_progress_t* progress, rc_value_t* variable) { - unsigned flags = rc_runtime_progress_read_uint(progress); + uint32_t flags = rc_runtime_progress_read_uint(progress); variable->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; variable->value.value = rc_runtime_progress_read_uint(progress); variable->value.prior = rc_runtime_progress_read_uint(progress); @@ -386,14 +376,14 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) struct rc_pending_value_t { rc_value_t* variable; - unsigned djb2; + uint32_t djb2; }; struct rc_pending_value_t local_pending_variables[32]; struct rc_pending_value_t* pending_variables; rc_value_t* variable; - unsigned count, serialized_count; + uint32_t count, serialized_count; int result; - unsigned i; + uint32_t i; serialized_count = rc_runtime_progress_read_uint(progress); if (serialized_count == 0) @@ -418,13 +408,13 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) count = 0; for (variable = progress->runtime->variables; variable; variable = variable->next) { pending_variables[count].variable = variable; - pending_variables[count].djb2 = rc_runtime_progress_djb2(variable->name); + pending_variables[count].djb2 = rc_djb2(variable->name); ++count; } result = RC_OK; for (; serialized_count > 0 && result == RC_OK; --serialized_count) { - unsigned djb2 = rc_runtime_progress_read_uint(progress); + uint32_t djb2 = rc_runtime_progress_read_uint(progress); for (i = 0; i < count; ++i) { if (pending_variables[i].djb2 == djb2) { variable = pending_variables[i].variable; @@ -502,7 +492,7 @@ static int rc_runtime_progress_read_trigger(rc_runtime_progress_t* progress, rc_ static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progress) { - unsigned i; + uint32_t i; int offset = 0; int result; @@ -543,8 +533,8 @@ static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progres static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) { - unsigned id = rc_runtime_progress_read_uint(progress); - unsigned i; + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; for (i = 0; i < progress->runtime->trigger_count; ++i) { rc_runtime_trigger_t* runtime_trigger = &progress->runtime->triggers[i]; @@ -564,8 +554,8 @@ static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) static int rc_runtime_progress_write_leaderboards(rc_runtime_progress_t* progress) { - unsigned i; - unsigned flags; + uint32_t i; + uint32_t flags; int offset = 0; int result; @@ -621,8 +611,8 @@ static int rc_runtime_progress_write_leaderboards(rc_runtime_progress_t* progres static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) { - unsigned id = rc_runtime_progress_read_uint(progress); - unsigned i; + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; int result; for (i = 0; i < progress->runtime->lboard_count; ++i) { @@ -632,7 +622,7 @@ static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) if (runtime_lboard->lboard->state == RC_TRIGGER_STATE_UNUPDATED) { /* only update state if definition hasn't changed (md5 matches) */ if (rc_runtime_progress_match_md5(progress, runtime_lboard->md5)) { - unsigned flags = rc_runtime_progress_read_uint(progress); + uint32_t flags = rc_runtime_progress_read_uint(progress); result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->start); if (result != RC_OK) @@ -712,7 +702,7 @@ static int rc_runtime_progress_read_rich_presence(rc_runtime_progress_t* progres static int rc_runtime_progress_serialize_internal(rc_runtime_progress_t* progress) { md5_state_t state; - unsigned char md5[16]; + uint8_t md5[16]; int result; rc_runtime_progress_write_uint(progress, RC_RUNTIME_MARKER); @@ -751,7 +741,7 @@ int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) rc_runtime_progress_t progress; int result; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + rc_runtime_progress_init(&progress, runtime, L); result = rc_runtime_progress_serialize_internal(&progress); if (result != RC_OK) @@ -764,26 +754,34 @@ int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua { rc_runtime_progress_t progress; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); - progress.buffer = (unsigned char*)buffer; + if (!buffer) + return RC_INVALID_STATE; + + rc_runtime_progress_init(&progress, runtime, L); + progress.buffer = (uint8_t*)buffer; return rc_runtime_progress_serialize_internal(&progress); } -int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* serialized, lua_State* L) +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L) { rc_runtime_progress_t progress; md5_state_t state; - unsigned char md5[16]; - unsigned chunk_id; - unsigned chunk_size; - unsigned next_chunk_offset; - unsigned i; + uint8_t md5[16]; + uint32_t chunk_id; + uint32_t chunk_size; + uint32_t next_chunk_offset; + uint32_t i; int seen_rich_presence = 0; int result = RC_OK; + if (!serialized) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + rc_runtime_progress_init(&progress, runtime, L); - progress.buffer = (unsigned char*)serialized; + progress.buffer = (uint8_t*)serialized; if (rc_runtime_progress_read_uint(&progress) != RC_RUNTIME_MARKER) { rc_runtime_reset(runtime); diff --git a/deps/rcheevos/src/rcheevos/trigger.c b/deps/rcheevos/src/rcheevos/trigger.c index 6061ab8ee194..71e186c01487 100644 --- a/deps/rcheevos/src/rcheevos/trigger.c +++ b/deps/rcheevos/src/rcheevos/trigger.c @@ -96,7 +96,7 @@ int rc_trigger_state_active(int state) } } -static int rc_condset_is_measured_from_hitcount(const rc_condset_t* condset, unsigned measured_value) +static int rc_condset_is_measured_from_hitcount(const rc_condset_t* condset, uint32_t measured_value) { const rc_condition_t* condition; for (condition = condset->conditions; condition; condition = condition->next) { @@ -280,6 +280,9 @@ int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) } void rc_reset_trigger(rc_trigger_t* self) { + if (!self) + return; + rc_reset_trigger_hitcounts(self); self->state = RC_TRIGGER_STATE_WAITING; diff --git a/deps/rcheevos/src/rcheevos/value.c b/deps/rcheevos/src/rcheevos/value.c index 9922231dc346..e7df82df908d 100644 --- a/deps/rcheevos/src/rcheevos/value.c +++ b/deps/rcheevos/src/rcheevos/value.c @@ -255,7 +255,7 @@ int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t return valid; } -int rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { +int32_t rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { rc_typed_value_t result; int valid = rc_evaluate_value_typed(self, &result, peek, ud, L); @@ -285,17 +285,31 @@ void rc_reset_value(rc_value_t* self) { self->value.changed = 0; } +int rc_value_from_hits(rc_value_t* self) +{ + rc_condset_t* condset = self->conditions; + for (; condset != NULL; condset = condset->next) { + rc_condition_t* condition = condset->conditions; + for (; condition != NULL; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) + return (condition->required_hits != 0); + } + } + + return 0; +} + void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { parse->variables = variables; *variables = 0; } -rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) +rc_value_t* rc_alloc_helper_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse) { rc_value_t** variables = parse->variables; rc_value_t* value; const char* name; - unsigned measured_target; + uint32_t measured_target; while ((value = *variables) != NULL) { if (strncmp(value->name, memaddr, memaddr_len) == 0 && value->name[memaddr_len] == 0) diff --git a/deps/rcheevos/src/rhash/cdreader.c b/deps/rcheevos/src/rhash/cdreader.c index c0f5c88a2bac..9ff9cb7396b3 100644 --- a/deps/rcheevos/src/rhash/cdreader.c +++ b/deps/rcheevos/src/rhash/cdreader.c @@ -1,6 +1,6 @@ #include "rc_hash.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include #include @@ -31,7 +31,7 @@ struct cdrom_t #endif }; -static int cdreader_get_sector(unsigned char header[16]) +static int cdreader_get_sector(uint8_t header[16]) { int minutes = (header[12] >> 4) * 10 + (header[12] & 0x0F); int seconds = (header[13] >> 4) * 10 + (header[13] & 0x0F); @@ -50,11 +50,11 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) * Then check for the presence of "CD001", which is gauranteed to be in either the * boot record or primary volume descriptor, one of which is always at sector 16. */ - const unsigned char sync_pattern[] = { + const uint8_t sync_pattern[] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 }; - unsigned char header[32]; + uint8_t header[32]; const int64_t toc_sector = 16 + cdrom->track_pregap_sectors; cdrom->sector_size = 0; diff --git a/deps/rcheevos/src/rhash/hash.c b/deps/rcheevos/src/rhash/hash.c index 72d7692b05d8..a953d525cb00 100644 --- a/deps/rcheevos/src/rhash/hash.c +++ b/deps/rcheevos/src/rhash/hash.c @@ -1,6 +1,6 @@ #include "rc_hash.h" -#include "../rcheevos/rc_compat.h" +#include "../rc_compat.h" #include "md5.h" @@ -225,11 +225,11 @@ static void rc_cd_close_track(void* track_handle) rc_hash_error("no hook registered for cdreader_close_track"); } -static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, unsigned* size) +static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uint32_t* size) { uint8_t buffer[2048], *tmp; int sector; - unsigned num_sectors = 0; + uint32_t num_sectors = 0; size_t filename_length; const char* slash; @@ -258,7 +258,7 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns } else { - unsigned logical_block_size; + uint32_t logical_block_size; /* find the cd information */ if (!rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 16, buffer, 256)) @@ -452,7 +452,7 @@ static int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_si return rc_hash_finalize(&md5, hash); } -static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector, const char* name, unsigned size, const char* description) +static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector, const char* name, uint32_t size, const char* description) { uint8_t buffer[2048]; size_t num_read; @@ -760,11 +760,11 @@ static int rc_hash_jaguar_cd(char hash[33], const char* path) void* track_handle; md5_state_t md5; int byteswapped = 0; - unsigned size = 0; - unsigned offset = 0; - unsigned sector = 0; - unsigned remaining; - unsigned i; + uint32_t size = 0; + uint32_t offset = 0; + uint32_t sector = 0; + uint32_t remaining; + uint32_t i; /* Jaguar CD header is in the first sector of the first data track OF THE SECOND SESSION. * The first track must be an audio track, but may be a warning message or actual game audio */ @@ -905,7 +905,7 @@ static int rc_hash_neogeo_cd(char hash[33], const char* path) char buffer[1024], *ptr; void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; md5_state_t md5; track_handle = rc_cd_open_track(path, 1); @@ -1091,7 +1091,7 @@ static int rc_hash_nintendo_ds(char hash[33], const char* path) { uint8_t header[512]; uint8_t* hash_buffer; - unsigned int hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; + uint32_t hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; size_t num_read; int64_t offset = 0; md5_state_t md5; @@ -1215,6 +1215,8 @@ static int rc_hash_gamecube(char hash[33], const char* path) uint32_t ix; file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); /* Verify Gamecube */ rc_file_seek(file_handle, 0x1c, SEEK_SET); @@ -1330,8 +1332,8 @@ static int rc_hash_pce_track(char hash[33], void* track_handle) { uint8_t buffer[2048]; md5_state_t md5; - int sector, num_sectors; - unsigned size; + uint32_t sector, num_sectors; + uint32_t size; /* the PC-Engine uses the second sector to specify boot information and program name. * the string "PC Engine CD-ROM SYSTEM" should exist at 32 bytes into the sector @@ -1516,7 +1518,7 @@ static int rc_hash_dreamcast(char hash[33], const char* path) uint8_t buffer[256] = ""; void* track_handle; char exe_file[32] = ""; - unsigned size; + uint32_t size; uint32_t sector; int result = 0; md5_state_t md5; @@ -1610,10 +1612,10 @@ static int rc_hash_dreamcast(char hash[33], const char* path) } static int rc_hash_find_playstation_executable(void* track_handle, const char* boot_key, const char* cdrom_prefix, - char exe_name[], unsigned exe_name_size, unsigned* exe_size) + char exe_name[], uint32_t exe_name_size, unsigned* exe_size) { uint8_t buffer[2048]; - unsigned size; + uint32_t size; char* ptr; char* start; const size_t boot_key_len = strlen(boot_key); @@ -1683,7 +1685,7 @@ static int rc_hash_psx(char hash[33], const char* path) char exe_name[64] = ""; void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; int result = 0; md5_state_t md5; @@ -1747,7 +1749,7 @@ static int rc_hash_ps2(char hash[33], const char* path) char exe_name[64] = ""; void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; int result = 0; md5_state_t md5; @@ -1795,7 +1797,7 @@ static int rc_hash_psp(char hash[33], const char* path) { void* track_handle; uint32_t sector; - unsigned size; + uint32_t size; md5_state_t md5; track_handle = rc_cd_open_track(path, 1); @@ -1866,6 +1868,21 @@ static int rc_hash_sega_cd(char hash[33], const char* path) return rc_hash_buffer(hash, buffer, sizeof(buffer)); } +static int rc_hash_scv(char hash[33], const uint8_t* buffer, size_t buffer_size) +{ + /* if the file contains a header, ignore it */ + /* https://gitlab.com/MaaaX-EmuSCV/libretro-emuscv/-/blob/master/readme.txt#L211 */ + if (memcmp(buffer, "EmuSCV", 6) == 0) + { + rc_hash_verbose("Ignoring SCV header"); + + buffer += 32; + buffer_size -= 32; + } + + return rc_hash_buffer(hash, buffer, buffer_size); +} + static int rc_hash_snes(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it */ @@ -2026,6 +2043,9 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_PC_ENGINE: /* NOTE: does not support PCEngine CD */ return rc_hash_pce(hash, buffer, buffer_size); + case RC_CONSOLE_SUPER_CASSETTEVISION: + return rc_hash_scv(hash, buffer, buffer_size); + case RC_CONSOLE_SUPER_NINTENDO: return rc_hash_snes(hash, buffer, buffer_size); @@ -2322,6 +2342,7 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ATARI_LYNX: case RC_CONSOLE_NINTENDO: case RC_CONSOLE_PC_ENGINE: + case RC_CONSOLE_SUPER_CASSETTEVISION: case RC_CONSOLE_SUPER_NINTENDO: /* additional logic whole-file hash - buffer then call rc_hash_generate_from_buffer */ return rc_hash_buffered_file(hash, console_id, path); @@ -2463,7 +2484,7 @@ static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, c rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); } -void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size) +void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) { int need_path = !buffer; @@ -2540,7 +2561,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, - * Fairchild Channel F, Arcadia 2001, and Interton VC 4000. + * Fairchild Channel F, Arcadia 2001, Interton VC 4000, and Super Cassette Vision. * Since they all use the same hashing algorithm, only specify one of them */ iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; } @@ -2570,9 +2591,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; iterator->consoles[2] = RC_CONSOLE_DREAMCAST; iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; - iterator->consoles[5] = RC_CONSOLE_3DO; - iterator->consoles[6] = RC_CONSOLE_PCFX; + iterator->consoles[4] = RC_CONSOLE_PSP; + iterator->consoles[5] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[6] = RC_CONSOLE_3DO; + iterator->consoles[7] = RC_CONSOLE_PCFX; need_path = 1; } else if (rc_path_compare_extension(ext, "col")) @@ -2587,6 +2609,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { iterator->consoles[0] = RC_CONSOLE_FAIRCHILD_CHANNEL_F; } + else if (rc_path_compare_extension(ext, "cart")) + { + iterator->consoles[0] = RC_CONSOLE_SUPER_CASSETTEVISION; + } break; case 'd': @@ -2596,7 +2622,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "d64")) { - iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; } else if (rc_path_compare_extension(ext, "d88")) { @@ -2616,7 +2642,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "fd")) { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ } break; diff --git a/dist-scripts/dist-cores.sh b/dist-scripts/dist-cores.sh index 13f603c9b0af..5c4c675e4c91 100755 --- a/dist-scripts/dist-cores.sh +++ b/dist-scripts/dist-cores.sh @@ -247,8 +247,8 @@ for f in `ls -v *_${platform}.${EXT}`; do if [ $MAKEFILE_GRIFFIN = "yes" ]; then make -C ../ -f Makefile.griffin $OPTS platform=${platform} $whole_archive $big_stack -j3 || exit 1 elif [ $PLATFORM = "emscripten" ]; then - echo "BUILD COMMAND: make -C ../ -f Makefile.emscripten PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 TARGET=${name}_libretro.js" - make -C ../ -f Makefile.emscripten $OPTS PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 TARGET=${name}_libretro.js || exit 1 + echo "BUILD COMMAND: make -C ../ -f Makefile.emscripten PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 LIBRETRO=${name} TARGET=${name}_libretro.js" + make -C ../ -f Makefile.emscripten $OPTS PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 LIBRETRO=${name} TARGET=${name}_libretro.js || exit 1 elif [ $PLATFORM = "unix" ]; then make -C ../ -f Makefile LINK=g++ $whole_archive $big_stack -j3 || exit 1 elif [ $PLATFORM = "ctr" ]; then diff --git a/emscripten/pre.js b/emscripten/pre.js new file mode 100644 index 000000000000..ae4d12534ff1 --- /dev/null +++ b/emscripten/pre.js @@ -0,0 +1,2 @@ +// To work around a bug in emscripten's polyfills for setImmediate in strict mode +var setImmediate; diff --git a/file_path_special.h b/file_path_special.h index 844141320e5b..7a35d9692bee 100644 --- a/file_path_special.h +++ b/file_path_special.h @@ -75,7 +75,9 @@ RETRO_BEGIN_DECLS #define FILE_PATH_LOBBY_LIBRETRO_URL "http://lobby.libretro.com/" #define FILE_PATH_CORE_THUMBNAILS_URL "http://thumbnails.libretro.com" #define FILE_PATH_CORE_THUMBNAILPACKS_URL "http://thumbnailpacks.libretro.com" -#ifdef HAVE_LAKKA_NIGHTLY +#ifdef HAVE_LAKKA_CANARY +#define FILE_PATH_LAKKA_URL HAVE_LAKKA_CANARY +#elif HAVE_LAKKA_NIGHTLY #define FILE_PATH_LAKKA_URL "http://nightly.builds.lakka.tv/.updater" #else #define FILE_PATH_LAKKA_URL "http://le.builds.lakka.tv" diff --git a/frontend/drivers/platform_emscripten.c b/frontend/drivers/platform_emscripten.c index c842a49854a5..3856bdf08b4a 100644 --- a/frontend/drivers/platform_emscripten.c +++ b/frontend/drivers/platform_emscripten.c @@ -160,8 +160,12 @@ int main(int argc, char *argv[]) { dummyErrnoCodes(); - emscripten_set_canvas_element_size("#canvas", 800, 600); - emscripten_set_element_css_size("#canvas", 800.0, 600.0); + EM_ASM({ + specialHTMLTargets["!canvas"] = Module.canvas; + }); + + emscripten_set_canvas_element_size("!canvas", 800, 600); + emscripten_set_element_css_size("!canvas", 800.0, 600.0); emscripten_set_main_loop(emscripten_mainloop, 0, 0); rarch_main(argc, argv, NULL); diff --git a/frontend/drivers/platform_win32.c b/frontend/drivers/platform_win32.c index 3c50c8205d92..ffccd7c8c281 100644 --- a/frontend/drivers/platform_win32.c +++ b/frontend/drivers/platform_win32.c @@ -1064,9 +1064,12 @@ static bool accessibility_speak_windows(int speed, if (!wc || res != 0) { RARCH_ERR("Error communicating with NVDA\n"); + /* Fallback on powershell immediately and retry */ + g_plat_win32_flags &= ~PLAT_WIN32_FLAG_USE_NVDA; + g_plat_win32_flags |= PLAT_WIN32_FLAG_USE_POWERSHELL; if (wc) free(wc); - return false; + return accessibility_speak_windows(speed, speak_text, priority); } nvdaController_cancelSpeech_func(); diff --git a/gfx/common/wayland_common.c b/gfx/common/wayland_common.c index ebee1cb42d41..a0a2158d1ab0 100644 --- a/gfx/common/wayland_common.c +++ b/gfx/common/wayland_common.c @@ -28,6 +28,10 @@ #include "../../frontend/frontend_driver.h" #include "../../verbosity.h" +#ifdef HAVE_DBUS +#include "dbus_common.h" +#endif + #define SPLASH_SHM_NAME "retroarch-wayland-vk-splash" #define APP_ID "org.libretro.RetroArch" @@ -282,6 +286,13 @@ void gfx_ctx_wl_destroy_resources_common(gfx_ctx_wayland_data_t *wl) zxdg_decoration_manager_v1_destroy(wl->deco_manager); if (wl->idle_inhibit_manager) zwp_idle_inhibit_manager_v1_destroy(wl->idle_inhibit_manager); + else + { +#ifdef HAVE_DBUS + dbus_screensaver_uninhibit(); + dbus_close_connection(); +#endif + } if (wl->pointer_constraints) zwp_pointer_constraints_v1_destroy(wl->pointer_constraints); if (wl->relative_pointer_manager) @@ -659,6 +670,9 @@ bool gfx_ctx_wl_init_common( if (!wl->idle_inhibit_manager) { RARCH_LOG("[Wayland]: Compositor doesn't support zwp_idle_inhibit_manager_v1 protocol\n"); +#ifdef HAVE_DBUS + dbus_ensure_connection(); +#endif } if (!wl->deco_manager) @@ -878,7 +892,13 @@ bool gfx_ctx_wl_suppress_screensaver(void *data, bool state) gfx_ctx_wayland_data_t *wl = (gfx_ctx_wayland_data_t*)data; if (!wl->idle_inhibit_manager) +#ifdef HAVE_DBUS + /* Some Wayland compositors (e.g. Phoc) don't implement Wayland's Idle protocol. + * They instead rely on things like Gnome Screensaver. */ + return dbus_suspend_screensaver(state); +#else return false; +#endif if (state != (!!wl->idle_inhibitor)) { if (state) diff --git a/gfx/drivers/sdl2_gfx.c b/gfx/drivers/sdl2_gfx.c index 58c56ad26f71..9168a0e77d38 100644 --- a/gfx/drivers/sdl2_gfx.c +++ b/gfx/drivers/sdl2_gfx.c @@ -539,7 +539,10 @@ static bool sdl2_gfx_focus(void *data) return (SDL_GetWindowFlags(vid->window) & flags) == flags; } +#if !defined(HAVE_X11) static bool sdl2_gfx_suspend_screensaver(void *data, bool enable) { return false; } +#endif + /* TODO/FIXME - implement */ static bool sdl2_gfx_has_windowed(void *data) { return true; } diff --git a/gfx/drivers_context/emscriptenegl_ctx.c b/gfx/drivers_context/emscriptenegl_ctx.c index 448051308571..2082aef98e78 100644 --- a/gfx/drivers_context/emscriptenegl_ctx.c +++ b/gfx/drivers_context/emscriptenegl_ctx.c @@ -70,7 +70,7 @@ static void gfx_ctx_emscripten_get_canvas_size(int *width, int *height) if (!is_fullscreen) { - r = emscripten_get_canvas_element_size("#canvas", width, height); + r = emscripten_get_canvas_element_size("!canvas", width, height); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -105,14 +105,14 @@ static void gfx_ctx_emscripten_check_window(void *data, bool *quit, if ( (input_width != emscripten->fb_width) || (input_height != emscripten->fb_height)) { - r = emscripten_set_canvas_element_size("#canvas", + r = emscripten_set_canvas_element_size("!canvas", input_width, input_height); if (r != EMSCRIPTEN_RESULT_SUCCESS) RARCH_ERR("[EMSCRIPTEN/EGL]: error resizing canvas: %d\n", r); /* fix Module.requestFullscreen messing with the canvas size */ - r = emscripten_set_element_css_size("#canvas", + r = emscripten_set_element_css_size("!canvas", (double)input_width, (double)input_height); if (r != EMSCRIPTEN_RESULT_SUCCESS) @@ -194,7 +194,7 @@ static void *gfx_ctx_emscripten_init(void *video_driver) * be grabbed? */ if ( (emscripten->initial_width == 0) || (emscripten->initial_height == 0)) - emscripten_get_canvas_element_size("#canvas", + emscripten_get_canvas_element_size("!canvas", &emscripten->initial_width, &emscripten->initial_height); diff --git a/gfx/gfx_widgets.c b/gfx/gfx_widgets.c index 989decd85f5d..453dddc6636c 100644 --- a/gfx/gfx_widgets.c +++ b/gfx/gfx_widgets.c @@ -1471,6 +1471,67 @@ static void INLINE gfx_widgets_font_unbind(gfx_widget_font_data_t *font_data) font_driver_bind_block(font_data->font, NULL); } +#ifdef HAVE_TRANSLATE +static void gfx_widgets_ai_line( + video_frame_info_t *video, char *line, int line_idx, int line_total) +{ + settings_t *settings = config_get_ptr(); + gfx_display_t *p_disp = (gfx_display_t*)video->disp_userdata; + dispgfx_widget_t *p_widget = (dispgfx_widget_t*)video->widgets_userdata; + void *userdata = video->userdata; + unsigned video_width = video->width; + unsigned video_height = video->height; + + int line_width = font_driver_get_message_width( + p_widget->gfx_widget_fonts.regular.font, + line, strlen(line), 1.0f); + + int hpadding = p_widget->simple_widget_padding; + int vpadding = settings->uints.ai_service_text_padding; + int half_vw = video_width * 0.5f; + int block_width = line_width + hpadding * 2; + int block_height = p_widget->simple_widget_height; + int block_x = half_vw - block_width * 0.5f; + int block_y = 0; + int line_y = 0; + + int position = (settings->uints.ai_service_text_position > 0) + ? settings->uints.ai_service_text_position + : p_widget->ai_service_text_position; + + switch (position) + { + case 0: /* Undef. */ + case 1: /* Bottom */ + block_y = (video_height * (100 - vpadding) * 0.01f) + - ((line_total - line_idx) * block_height); + break; + case 2: /* Top */ + block_y = (video_height * (vpadding * 0.01f)) + + (line_idx * block_height); + break; + } + + line_y = block_y + block_height * 0.5f + + p_widget->gfx_widget_fonts.regular.line_centre_offset; + + gfx_display_set_alpha(p_widget->backdrop_orig, DEFAULT_BACKDROP); + + gfx_display_draw_quad( + p_disp, userdata, video_width, video_height, + block_x, block_y, block_width, block_height, + video_width, video_height, + p_widget->backdrop_orig, + NULL); + + gfx_widgets_draw_text( + &p_widget->gfx_widget_fonts.regular, + line, half_vw, line_y, + video_width, video_height, + 0xFFFFFFFF, TEXT_ALIGN_CENTER, true); +} +#endif + void gfx_widgets_frame(void *data) { size_t i; @@ -1520,12 +1581,8 @@ void gfx_widgets_frame(void *data) /* AI Service overlay */ if (p_dispwidget->ai_service_overlay_state > 0) { - float outline_color[16] = { - 0.00, 1.00, 0.00, 1.00, - 0.00, 1.00, 0.00, 1.00, - 0.00, 1.00, 0.00, 1.00, - 0.00, 1.00, 0.00, 1.00, - }; + int text_length = strlen(p_dispwidget->ai_service_text); + gfx_display_set_alpha(p_dispwidget->pure_white, 1.0f); if (p_dispwidget->ai_service_overlay_texture) @@ -1550,63 +1607,46 @@ void gfx_widgets_frame(void *data) if (dispctx->blend_end) dispctx->blend_end(userdata); } - - /* top line */ - gfx_display_draw_quad( - p_disp, - userdata, - video_width, video_height, - 0, 0, - video_width, - p_dispwidget->divider_width_1px, - video_width, - video_height, - outline_color, - NULL - ); - /* bottom line */ - gfx_display_draw_quad( - p_disp, - userdata, - video_width, video_height, - 0, - video_height - p_dispwidget->divider_width_1px, - video_width, - p_dispwidget->divider_width_1px, - video_width, - video_height, - outline_color, - NULL - ); - /* left line */ - gfx_display_draw_quad( - p_disp, - userdata, - video_width, - video_height, - 0, - 0, - p_dispwidget->divider_width_1px, - video_height, - video_width, - video_height, - outline_color, - NULL - ); - /* right line */ - gfx_display_draw_quad( - p_disp, - userdata, - video_width, video_height, - video_width - p_dispwidget->divider_width_1px, - 0, - p_dispwidget->divider_width_1px, - video_height, - video_width, - video_height, - outline_color, - NULL - ); + + /* AI Service subtitle overlay widget */ + if (text_length > 0) + { + int padding = p_dispwidget->simple_widget_padding; + int text_width = font_driver_get_message_width( + p_dispwidget->gfx_widget_fonts.regular.font, + p_dispwidget->ai_service_text, + text_length, 1.0f); + + if (text_width > (video_width * 0.9f - padding * 2)) + { + int text_half = text_length / 2; + char *extra_line = (char*)malloc(sizeof(char) * text_length); + for (; text_half > 0; text_half--) + { + if (p_dispwidget->ai_service_text[text_half] == ' ') + { + p_dispwidget->ai_service_text[text_half] = '\0'; + gfx_widgets_ai_line( + video_info, p_dispwidget->ai_service_text, 0, 2); + strlcpy( + extra_line, + p_dispwidget->ai_service_text + text_half + 1, + text_length - text_half); + gfx_widgets_ai_line( + video_info, extra_line, 1, 2); + + p_dispwidget->ai_service_text[text_half] = ' '; + free(extra_line); + break; + } + } + } + else + { + gfx_widgets_ai_line( + video_info, p_dispwidget->ai_service_text, 0, 1); + } + } if (p_dispwidget->ai_service_overlay_state == 2) p_dispwidget->ai_service_overlay_state = 3; @@ -2149,6 +2189,7 @@ void gfx_widgets_ai_service_overlay_unload(void) if (p_dispwidget->ai_service_overlay_state == 1) { video_driver_texture_unload(&p_dispwidget->ai_service_overlay_texture); + p_dispwidget->ai_service_text[0] = '\0'; p_dispwidget->ai_service_overlay_texture = 0; p_dispwidget->ai_service_overlay_state = 0; } diff --git a/gfx/gfx_widgets.h b/gfx/gfx_widgets.h index 3b8195dc70e7..65bb9e990953 100644 --- a/gfx/gfx_widgets.h +++ b/gfx/gfx_widgets.h @@ -236,6 +236,8 @@ typedef struct dispgfx_widget #ifdef HAVE_TRANSLATE unsigned ai_service_overlay_width; unsigned ai_service_overlay_height; + unsigned ai_service_text_position; + char ai_service_text[255]; #endif uint8_t flags; diff --git a/gfx/video_driver.c b/gfx/video_driver.c index 5f124955cd42..d977c1b1b415 100644 --- a/gfx/video_driver.c +++ b/gfx/video_driver.c @@ -20,6 +20,7 @@ #include #include #include +#include #ifdef HAVE_CONFIG_H #include "../config.h" @@ -67,6 +68,7 @@ #define TIME_TO_FPS(last_time, new_time, frames) ((1000000.0f * (frames)) / ((new_time) - (last_time))) #define FRAME_DELAY_AUTO_DEBUG 0 +#define FRAME_REST_DEBUG 0 typedef struct { @@ -527,27 +529,27 @@ video_driver_t *hw_render_context_driver( #endif case RETRO_HW_CONTEXT_D3D10: #if defined(HAVE_D3D10) - return &video_d3d10; + return &video_d3d10; #else break; #endif case RETRO_HW_CONTEXT_D3D11: #if defined(HAVE_D3D11) - return &video_d3d11; + return &video_d3d11; #else break; #endif case RETRO_HW_CONTEXT_D3D12: #if defined(HAVE_D3D12) - return &video_d3d12; + return &video_d3d12; #else break; #endif case RETRO_HW_CONTEXT_D3D9: #if defined(HAVE_D3D9) && defined(HAVE_HLSL) - return &video_d3d9_hlsl; + return &video_d3d9_hlsl; #else - break; + break; #endif case RETRO_HW_CONTEXT_VULKAN: #if defined(HAVE_VULKAN) @@ -2641,6 +2643,7 @@ void video_driver_build_info(video_frame_info_t *video_info) video_info->runloop_is_paused = (runloop_st->flags & RUNLOOP_FLAG_PAUSED) ? true : false; video_info->runloop_is_slowmotion = (runloop_st->flags & RUNLOOP_FLAG_SLOWMOTION) ? true : false; video_info->fastforward_frameskip = settings->bools.fastforward_frameskip; + video_info->frame_rest = settings->bools.video_frame_rest; #ifdef _WIN32 #ifdef HAVE_VULKAN @@ -3749,6 +3752,7 @@ void video_driver_frame(const void *data, unsigned width, if (render_frame && video_info.statistics_show) { audio_statistics_t audio_stats; + char throttle_stats[128]; char latency_stats[128]; char tmp[128]; size_t len; @@ -3796,9 +3800,27 @@ void video_driver_frame(const void *data, unsigned width, audio_compute_buffer_statistics(&audio_stats); - latency_stats[0] = '\0'; - tmp[0] = '\0'; - len = 0; + throttle_stats[0] = '\0'; + latency_stats[0] = '\0'; + tmp[0] = '\0'; + len = 0; + + if (video_info.frame_rest) + len = snprintf(tmp + len, sizeof(throttle_stats), + " Frame Rest: %2u.00 ms\n" + " - Rested: %5.2f %%\n", + video_st->frame_rest, + (float)video_st->frame_rest_time_count / runloop_st->core_runtime_usec * 100); + + if (len) + { + /* TODO/FIXME - localize */ + size_t _len = strlcpy(throttle_stats, "THROTTLE\n", sizeof(throttle_stats)); + strlcpy(throttle_stats + _len, tmp, sizeof(throttle_stats) - _len); + } + + tmp[0] = '\0'; + len = 0; /* TODO/FIXME - localize */ if (video_st->frame_delay_target > 0) @@ -3826,9 +3848,9 @@ void video_driver_frame(const void *data, unsigned width, if (len) { - /* TODO/FIXME - localize */ - size_t _len = strlcpy(latency_stats, "LATENCY\n", sizeof(latency_stats)); - strlcpy(latency_stats + _len, tmp, sizeof(latency_stats) - _len); + /* TODO/FIXME - localize */ + size_t _len = strlcpy(latency_stats, "LATENCY\n", sizeof(latency_stats)); + strlcpy(latency_stats + _len, tmp, sizeof(latency_stats) - _len); } /* TODO/FIXME - localize */ @@ -3853,6 +3875,7 @@ void video_driver_frame(const void *data, unsigned width, " Underrun: %5.2f %%\n" " Blocking: %5.2f %%\n" " Samples: %5d\n" + "%s" "%s", av_info->geometry.base_width, av_info->geometry.base_height, @@ -3875,6 +3898,7 @@ void video_driver_frame(const void *data, unsigned width, audio_stats.close_to_underrun, audio_stats.close_to_blocking, audio_stats.samples, + throttle_stats, latency_stats); /* TODO/FIXME - add OSD chat text here */ @@ -4173,3 +4197,128 @@ void video_frame_delay_auto(video_driver_state_t *video_st, video_frame_delay_au ); #endif } + +void video_frame_rest(video_driver_state_t *video_st, + settings_t *settings, + retro_time_t current_time) +{ +#ifdef HAVE_MENU + bool menu_is_pausing = settings->bools.menu_pause_libretro && (menu_state_get_ptr()->flags & MENU_ST_FLAG_ALIVE); +#else + bool menu_is_pausing = false; +#endif + runloop_state_t *runloop_st = runloop_state_get_ptr(); + retro_time_t latest_time = cpu_features_get_time_usec(); + retro_time_t frame_time_delta = latest_time - current_time; + retro_time_t frame_time_target = 1000000.0f / settings->floats.video_refresh_rate; + retro_time_t frame_time = 0; + static retro_time_t after_present = 0; + int sleep_max = frame_time_target / 1000 / 2; + int sleep = 0; + int frame_time_near_req_count = ceil(settings->floats.video_refresh_rate / 2); + static int frame_time_over_count = 0; + static int frame_time_near_count = 0; + static int frame_time_try_count = 0; + double video_stddev = 0; + audio_statistics_t audio_stats; + + /* Must require video and audio deviation standards */ + video_monitor_fps_statistics(NULL, &video_stddev, NULL); + audio_compute_buffer_statistics(&audio_stats); + + /* Don't care about deviations when core is not running */ + if ( (runloop_st->flags & RUNLOOP_FLAG_PAUSED) + || !(runloop_st->flags & RUNLOOP_FLAG_CORE_RUNNING) + || menu_is_pausing) + video_stddev = audio_stats.std_deviation_percentage = 0; + + /* Compare to previous timestamp */ + frame_time = latest_time - after_present; + + /* Count running timers */ + if (frame_time > frame_time_target) + frame_time_over_count++; + else if (frame_time < frame_time_target) + frame_time_over_count--; + + if (labs(frame_time - frame_time_target) < frame_time_target * 1.002f - frame_time_target) + frame_time_near_count++; + else + frame_time_near_count--; + + /* Take new timestamp */ + after_present = latest_time; + + /* Ignore unreasonable frame times */ + if ( frame_time < frame_time_target / 2 + || frame_time > frame_time_target * 2) + return; + + /* Carry the extra */ + frame_time_delta -= frame_time_target - frame_time; + sleep = (frame_time_delta > 0) ? frame_time_delta : 0; + + /* No rest with bogus values */ + if ( sleep < 0 + || ( frame_time_target < frame_time_delta + && frame_time_target < frame_time)) + sleep = 0; + + /* Reset over the target counter */ + if (!sleep) + frame_time_over_count = 0; + + frame_time_try_count++; + if ( frame_time_try_count > frame_time_near_req_count * 2 + || frame_time_try_count < frame_time_near_count) + frame_time_over_count = frame_time_near_count = frame_time_try_count = 0; + + /* Increase */ + if (sleep + && (frame_time_over_count < 2) + && (video_stddev * 100.0f < 25.00f) + && (audio_stats.std_deviation_percentage < 25.00f) + && (frame_time_near_count > frame_time_try_count / 2) + && (frame_time_near_count > frame_time_near_req_count) + ) + { +#if FRAME_REST_DEBUG + RARCH_LOG("+ frame=%5d delta=%5d sleep=%2d over=%3d near=%3d try=%3d\n", frame_time, frame_time_delta, video_st->frame_rest, frame_time_over_count, frame_time_near_count, frame_time_try_count); +#endif + video_st->frame_rest++; + frame_time_over_count = frame_time_near_count = frame_time_try_count = 0; + } + /* Decrease */ + else if ( sleep + && frame_time_over_count != 0 + && frame_time_try_count > 10 + && ( (frame_time_near_count < -2 && -frame_time_near_count > frame_time_try_count) + || (frame_time_over_count > frame_time_near_req_count / 2) + || (frame_time_over_count < -(frame_time_near_req_count / 2)) + ) + ) + { +#if FRAME_REST_DEBUG + RARCH_LOG("- frame=%5d delta=%5d sleep=%2d over=%3d near=%3d try=%3d\n", frame_time, frame_time_delta, video_st->frame_rest, frame_time_over_count, frame_time_near_count, frame_time_try_count); +#endif + if (video_st->frame_rest) + video_st->frame_rest--; + frame_time_over_count = frame_time_near_count = frame_time_try_count = 0; + } + + /* Limit to maximum sleep */ + if (video_st->frame_rest > sleep_max) + video_st->frame_rest = sleep_max; + +#if FRAME_REST_DEBUG + RARCH_LOG(" frame=%5d delta=%5d sleep=%2d over=%3d near=%3d try=%3d %f %f\n", frame_time, frame_time_delta, video_st->frame_rest, frame_time_over_count, frame_time_near_count, frame_time_try_count, video_stddev, audio_stats.std_deviation_percentage); +#endif + + /* Do what is promised and add to statistics */ + if (video_st->frame_rest > 0) + { + if (!menu_is_pausing) + video_st->frame_rest_time_count += video_st->frame_rest * 1000; + retro_sleep(video_st->frame_rest); + } +} diff --git a/gfx/video_driver.h b/gfx/video_driver.h index 7755ffc7b219..06b5d4db69e1 100644 --- a/gfx/video_driver.h +++ b/gfx/video_driver.h @@ -482,6 +482,7 @@ typedef struct video_frame_info bool runloop_is_slowmotion; bool runloop_is_paused; bool fastforward_frameskip; + bool frame_rest; bool msg_bgcolor_enable; bool crt_switch_hires_menu; bool hdr_enable; @@ -774,6 +775,7 @@ typedef struct retro_time_t frame_time_samples[MEASURE_FRAME_TIME_SAMPLES_COUNT]; uint64_t frame_time_count; uint64_t frame_count; + uint64_t frame_rest_time_count; uint8_t *record_gpu_buffer; #ifdef HAVE_VIDEO_FILTER rarch_softfilter_t *state_filter; @@ -859,6 +861,7 @@ typedef struct char title_buf[64]; char cached_driver_id[32]; + uint8_t frame_rest; uint8_t frame_delay_target; uint8_t frame_delay_effective; bool frame_delay_pause; @@ -1087,7 +1090,12 @@ bool *video_driver_get_threaded(void); void video_driver_set_threaded(bool val); -void video_frame_delay_auto(video_driver_state_t *video_st, video_frame_delay_auto_t *vfda); +void video_frame_delay_auto(video_driver_state_t *video_st, + video_frame_delay_auto_t *vfda); + +void video_frame_rest(video_driver_state_t *video_st, + settings_t *settings, + retro_time_t current_time); /** * video_context_driver_init: diff --git a/griffin/griffin.c b/griffin/griffin.c index 9a3115abc9d8..1c40fb54bdcb 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -186,6 +186,15 @@ ACHIEVEMENTS #include "../libretro-common/net/net_http.c" #endif +/* rcheevos doesn't actually spawn and manage threads, RC_NO_THREADS + * simply disables the mutexes that provide thread safety. */ +#if !defined(HAVE_THREADS) +#define RC_NO_THREADS 1 +#elif defined(GEKKO) || defined(_3DS) + /* Gekko (Wii) and 3DS use custom pthread wrappers (see rthreads.c) */ +#define RC_NO_THREADS 1 +#endif + #include "../libretro-common/formats/cdfs/cdfs.c" #include "../network/net_http_special.c" @@ -193,11 +202,15 @@ ACHIEVEMENTS #include "../cheevos/cheevos_client.c" #include "../cheevos/cheevos_menu.c" +#include "../deps/rcheevos/src/rc_client.c" +#include "../deps/rcheevos/src/rc_compat.c" +#include "../deps/rcheevos/src/rc_libretro.c" +#include "../deps/rcheevos/src/rc_util.c" #include "../deps/rcheevos/src/rapi/rc_api_common.c" +#include "../deps/rcheevos/src/rapi/rc_api_info.c" #include "../deps/rcheevos/src/rapi/rc_api_runtime.c" #include "../deps/rcheevos/src/rapi/rc_api_user.c" #include "../deps/rcheevos/src/rcheevos/alloc.c" -#include "../deps/rcheevos/src/rcheevos/compat.c" #include "../deps/rcheevos/src/rcheevos/condition.c" #include "../deps/rcheevos/src/rcheevos/condset.c" #include "../deps/rcheevos/src/rcheevos/consoleinfo.c" @@ -205,7 +218,6 @@ ACHIEVEMENTS #include "../deps/rcheevos/src/rcheevos/lboard.c" #include "../deps/rcheevos/src/rcheevos/memref.c" #include "../deps/rcheevos/src/rcheevos/operand.c" -#include "../deps/rcheevos/src/rcheevos/rc_libretro.c" #include "../deps/rcheevos/src/rcheevos/richpresence.c" #include "../deps/rcheevos/src/rcheevos/runtime.c" #include "../deps/rcheevos/src/rcheevos/runtime_progress.c" diff --git a/input/drivers/cocoa_input.m b/input/drivers/cocoa_input.m index a7b3033326f6..d2704d12e651 100644 --- a/input/drivers/cocoa_input.m +++ b/input/drivers/cocoa_input.m @@ -383,30 +383,23 @@ static void cocoa_input_poll(void *data) cocoa_input_data_t *apple = (cocoa_input_data_t*)data; #ifndef IOS float backing_scale_factor = cocoa_screen_get_backing_scale_factor(); +#else + int backing_scale_factor = 1; #endif if (!apple) return; - for (i = 0; i < apple->touch_count; i++) + for (i = 0; i < apple->touch_count || i == 0; i++) { struct video_viewport vp; - vp.x = 0; - vp.y = 0; - vp.width = 0; - vp.height = 0; - vp.full_width = 0; - vp.full_height = 0; + memset(&vp, 0, sizeof(vp)); -#ifndef IOS - apple->touches[i].screen_x *= backing_scale_factor; - apple->touches[i].screen_y *= backing_scale_factor; -#endif video_driver_translate_coord_viewport_wrap( &vp, - apple->touches[i].screen_x, - apple->touches[i].screen_y, + apple->touches[i].screen_x * backing_scale_factor, + apple->touches[i].screen_y * backing_scale_factor, &apple->touches[i].fixed_x, &apple->touches[i].fixed_y, &apple->touches[i].full_x, @@ -568,8 +561,13 @@ static int16_t cocoa_input_state( case RETRO_DEVICE_POINTER: case RARCH_DEVICE_POINTER_SCREEN: { - - if (idx < apple->touch_count && (idx < MAX_TOUCHES)) +#ifdef IOS + if (!apple->touch_count) + return 0; +#endif + // with a physical mouse that is hovering, the touch_count will be 0 + // and apple->touches[0] will have the hover position + if ((idx == 0 || idx < apple->touch_count) && (idx < MAX_TOUCHES)) { const cocoa_touch_data_t *touch = (const cocoa_touch_data_t *) &apple->touches[idx]; @@ -579,6 +577,8 @@ static int16_t cocoa_input_state( switch (id) { case RETRO_DEVICE_ID_POINTER_PRESSED: + if (!apple->touch_count) + return 0; if (device == RARCH_DEVICE_POINTER_SCREEN) return (touch->full_x != -0x8000) && (touch->full_y != -0x8000); /* Inside? */ return (touch->fixed_x != -0x8000) && (touch->fixed_y != -0x8000); /* Inside? */ diff --git a/input/drivers/rwebinput_input.c b/input/drivers/rwebinput_input.c index 729793d62f4d..0cc23fc74761 100644 --- a/input/drivers/rwebinput_input.c +++ b/input/drivers/rwebinput_input.c @@ -291,7 +291,7 @@ static void *rwebinput_input_init(const char *joypad_driver) rwebinput_generate_lut(); r = emscripten_set_keydown_callback( - EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, + "!canvas", rwebinput, false, rwebinput_keyboard_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -300,7 +300,7 @@ static void *rwebinput_input_init(const char *joypad_driver) } r = emscripten_set_keyup_callback( - EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, + "!canvas", rwebinput, false, rwebinput_keyboard_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -309,7 +309,7 @@ static void *rwebinput_input_init(const char *joypad_driver) } r = emscripten_set_keypress_callback( - EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, + "!canvas", rwebinput, false, rwebinput_keyboard_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -317,7 +317,7 @@ static void *rwebinput_input_init(const char *joypad_driver) "[EMSCRIPTEN/INPUT] failed to create keypress callback: %d\n", r); } - r = emscripten_set_mousedown_callback("#canvas", rwebinput, false, + r = emscripten_set_mousedown_callback("!canvas", rwebinput, false, rwebinput_mouse_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -325,7 +325,7 @@ static void *rwebinput_input_init(const char *joypad_driver) "[EMSCRIPTEN/INPUT] failed to create mousedown callback: %d\n", r); } - r = emscripten_set_mouseup_callback("#canvas", rwebinput, false, + r = emscripten_set_mouseup_callback("!canvas", rwebinput, false, rwebinput_mouse_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -333,7 +333,7 @@ static void *rwebinput_input_init(const char *joypad_driver) "[EMSCRIPTEN/INPUT] failed to create mouseup callback: %d\n", r); } - r = emscripten_set_mousemove_callback("#canvas", rwebinput, false, + r = emscripten_set_mousemove_callback("!canvas", rwebinput, false, rwebinput_mouse_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -342,7 +342,7 @@ static void *rwebinput_input_init(const char *joypad_driver) } r = emscripten_set_wheel_callback( - EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, + "!canvas", rwebinput, false, rwebinput_wheel_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { @@ -651,7 +651,7 @@ static void rwebinput_input_poll(void *data) static void rwebinput_grab_mouse(void *data, bool state) { if (state) - emscripten_request_pointerlock("#canvas", EM_TRUE); + emscripten_request_pointerlock("!canvas", EM_TRUE); else emscripten_exit_pointerlock(); } diff --git a/input/drivers/udev_input.c b/input/drivers/udev_input.c index 865194e8b291..9b26076224eb 100644 --- a/input/drivers/udev_input.c +++ b/input/drivers/udev_input.c @@ -51,6 +51,7 @@ #include #include #include +#include #elif defined(__FreeBSD__) #include #endif @@ -119,12 +120,11 @@ /* UDEV_TOUCH_PRINTF_DEBUG */ #ifdef UDEV_TOUCH_DEEP_DEBUG -#define RARCH_DDBG(msg, ...) do{ \ - RARCH_DBG(msg, __VA_ARGS__); \ +#define RARCH_DDBG(...) do{ \ + RARCH_DBG(__VA_ARGS__); \ } while (0) #else -/* TODO - Since C89 doesn't allow variadic macros, we have an empty function instead... */ -void RARCH_DDBG(const char *fmt, ...) { } +#define RARCH_DDBG(msg, ...) #endif /* UDEV_TOUCH_DEEP_DEBUG */ @@ -956,8 +956,13 @@ static void udev_handle_mouse(void *data, */ static void udev_touch_event_ts_copy(const struct input_event *event, udev_touch_ts_t *ts) { - ts->s = event->input_event_sec; - ts->us = event->input_event_usec; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4,16,0) + ts->s = event->input_event_sec; + ts->us = event->input_event_usec; +#else + ts->s = event->time.tv_sec; + ts->us = event->time.tv_usec; +#endif } /** diff --git a/input/drivers_joypad/udev_joypad.c b/input/drivers_joypad/udev_joypad.c index f004d904821e..ff4b55dcf037 100644 --- a/input/drivers_joypad/udev_joypad.c +++ b/input/drivers_joypad/udev_joypad.c @@ -506,6 +506,12 @@ static void udev_joypad_poll(void) /* Hotplug removal */ else if (string_is_equal(action, "remove")) udev_joypad_remove_device(devnode); + /* Device change */ + else if (string_is_equal(action, "change")) + { + udev_joypad_remove_device(devnode); + udev_check_device(dev, devnode); + } } udev_device_unref(dev); diff --git a/input/drivers_joypad/xinput_hybrid_joypad.c b/input/drivers_joypad/xinput_hybrid_joypad.c index cb57417f6aa5..fa43b2970add 100644 --- a/input/drivers_joypad/xinput_hybrid_joypad.c +++ b/input/drivers_joypad/xinput_hybrid_joypad.c @@ -242,6 +242,11 @@ static BOOL CALLBACK enum_joypad_cb_hybrid( if (g_joypad_cnt == MAX_USERS) return DIENUM_STOP; + while (!g_xinput_states[g_last_xinput_pad_idx].connected && g_last_xinput_pad_idx < 3) + { + g_last_xinput_pad_idx++; + } + pad = &g_pads[g_joypad_cnt].joypad; #ifdef __cplusplus diff --git a/intl/googleplay_ru.json b/intl/googleplay_ru.json index 99f6046d74a9..3fc7e9d56879 100644 --- a/intl/googleplay_ru.json +++ b/intl/googleplay_ru.json @@ -1,4 +1,4 @@ { "main-desc": "RetroArch - это проект с открытым исходным кодом на основе мощного интерфейса разработки Libretro. Интерфейс Libretro позволяет создавать кросс-платформенные приложения с доступом к широкому выбору функций, включая OpenGL, поддержку камер, определение местоположения и многих других.\n\nВстроенная коллекция приложений предоставляет пользователям единое решение для развлекательных целей.\n\nLibretro и RetroArch - это идеальное решение для создания игр, эмуляторов и мультимедийных программ. Для получения дополнительной информации посетите наш сайт (см. ниже).\n\nОСОБЕННОСТИ:\n* Красивые меню на ваш выбор!\n* Сканируйте файлы/каталоги и добавляйте их в коллекции игровых систем!\n* Просматривайте информацию в базах данных по любой игре в коллекции!\n* Скачивайте программы ('ядра') онлайн\n* Обновляйте всё!\n* Скачивайте и запускайте игры Game & Watch на нашем эксклюзивном эмуляторе!\n* Встроенные профили устройств ввода\n* Возможность изменения схемы управления\n* Возможность создания и загрузки чит-кодов\n* Мультиязычный интерфейс!\n* Более 80 программ ('ядер') и это не предел!\n* Играйте по сети с помощью NetPlay!\n* Делайте скриншоты, быстрые сохранения и многое другое!\n\n* Без DRM\n* Без ограничений на использование\n* Открытый программный код\n* Без push-уведомлений\n* Без слежения\n* Без рекламы\n\nПрисоединяйтесь к Discord для получения поддержки и поиска сетевых оппонентов\nhttps://discord. g/C4amCeV\n\nПосетите наши каналы на Youtube для просмотра видеоинструкций, записей геймплея, новостей и отчётов о разработке!\nhttps://www.youtube.com/user/libretro\nhttps://www.youtube. om/RetroArchOfficial\n\nДля получения информации и помощи посетите сайт с документацией -\nhttps://docs.libretro.com/\n\nЗаходите на наш вебсайт!\nhttps://www.retroarch.com/\n\nwww.libretro.com", - "short-desc": "Ретро игры и эмуляторы на вашем устройстве!" + "short-desc": "Эмуляция ретроигр на вашем устройстве!" } diff --git a/intl/msg_hash_ar.h b/intl/msg_hash_ar.h index 7cdd8a3356df..ea95840d32a7 100644 --- a/intl/msg_hash_ar.h +++ b/intl/msg_hash_ar.h @@ -614,6 +614,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CPU_ARCHITECTURE, "بنية وحدة المعالجة المركزية" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CPU_CORES, + "نَوَيات المُعالِجْ" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_FRONTEND_IDENTIFIER, "واجهة Identifier" @@ -1152,6 +1156,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_CONFIG, "ملف الإعدادات." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_SHADER_PRESET, + "مِلف الإعدادات المسبقة للمظلل." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_SHADER, + "مِلف المظلل." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_CHEAT, "ملف الغش." @@ -1560,11 +1572,11 @@ MSG_HASH( #if defined(DINGUX) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_IPU_FILTER_TYPE, - "إستيفاء الصورة" + "استيفاء الصورة" ) MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_DINGUX_IPU_FILTER_TYPE, - "تحديد طريقة استخراج الصور عند قياس المحتوى عن طريق ال IPU. \"ثنائي تكعيبي\" او \"ثنائي خطي\" ينصح عند استخدام فلاتر الفيديو التي تعمل بالمعالج. هذا الخيار ليس له تأثير على الأداء." + "تحديد طريقة استخراج الصور عند قياس المحتوى عن طريق IPU. \"ثنائي تكعيبي\" أو \"ثنائي خطي\" ينصح عند استخدام فلاتر الفيديو التي تعمل بالمعالج. هذا الخِيار ليس له تأثير على الأداء." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_IPU_FILTER_BICUBIC, @@ -1581,7 +1593,11 @@ MSG_HASH( #if defined(RS90) || defined(MIYOO) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_RS90_SOFTFILTER_TYPE, - "إستيفاء الصورة" + "استيفاء الصورة" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_DINGUX_RS90_SOFTFILTER_TYPE, + "حدد طريقة استيفاء الصورة عندما يكونُ الخِيار 'تحجيم صحيح' معطل. أما 'أقرب جار' هو الأقل تأثيرًا على الأداء." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_RS90_SOFTFILTER_POINT, @@ -2523,6 +2539,14 @@ MSG_HASH( "القرص السابق" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_SHADER_TOGGLE, + "المظلل (تبديل)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_SHADER_TOGGLE, + "تفعيل/تعطيل المظلل الحالي." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_SHADER_NEXT, "المشهد التالي" @@ -3739,6 +3763,14 @@ MSG_HASH( /* FIXME Not RGUI specific */ /* Settings > User Interface > Menu Item Visibility > Quick Menu */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_SHADERS, + "إظهار 'الأظلال'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_SHADERS, + "إظهار خيارات 'الأظلال'." + ) /* Settings > User Interface > Views > Settings */ @@ -5516,6 +5548,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "وضع المشرف" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "أعلى" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "المحفوظات والمفضلة" diff --git a/intl/msg_hash_be.h b/intl/msg_hash_be.h index 668729d1c1dc..6d6b2d8cbae8 100644 --- a/intl/msg_hash_be.h +++ b/intl/msg_hash_be.h @@ -42,7 +42,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_EXPLORE_TAB, - "Агляд" + "Дослед" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENTLESS_CORES_TAB, + "Бяззмесціўныя ядры" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_ADD_TAB, @@ -61,7 +65,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_LIST, - "Загрузіць ядро" + "Загрузка ядра" ) MSG_HASH( MENU_ENUM_SUBLABEL_CORE_LIST, @@ -69,15 +73,19 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOAD_CONTENT_LIST, - "Загрузіць змесціва" + "Загрузка змесціва" ) MSG_HASH( MENU_ENUM_SUBLABEL_LOAD_CONTENT_LIST, "Выбраць змесціва для запуску." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_LOAD_CONTENT_LIST, + "Агледзець змесціва. Для загрузкі неабходны 'ядро' ды файл са змесцівам.\nЗадайце 'каталог аглядальніка файлаў', дзе меню пачне агляд змесціва. Калі не зададзена, будзе пачынацца з кораню.\nАглядальнік будзе фільтраваць пашырэнні згодна з апошнім ядром, выбраным праз 'Загрузі[...]" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOAD_DISC, - "Загрузіць дыск" + "Загрузка дыска" ) MSG_HASH( MENU_ENUM_SUBLABEL_LOAD_DISC, @@ -98,7 +106,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_EJECT_DISC, - "Выняць дыск з фізічнага CD/DVD-прывада." + "Вымае дыск з фізічнага CD/DVD-прывада." ) #endif MSG_HASH( @@ -141,6 +149,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY, "Сеткавая гульня" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY, + "Далучыцца або стварыць сеанс сеткавай гульні." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS, "Налады" @@ -163,7 +175,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CONFIGURATIONS_LIST, - "Кіраванне і стварэнне файлаў канфігурацыі." + "Кіраваць ды ствараць файлы канфігурацыі." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_HELP_LIST, @@ -175,7 +187,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RESTART_RETROARCH, - "Перазапусціць RetroArch" + "Перазапуск RetroArch" ) MSG_HASH( MENU_ENUM_SUBLABEL_RESTART_RETROARCH, @@ -183,7 +195,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QUIT_RETROARCH, - "Выйсці з RetroArch" + "Выхад з RetroArch" ) MSG_HASH( MENU_ENUM_SUBLABEL_QUIT_RETROARCH, @@ -229,7 +241,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_OPEN_ARCHIVE, - "Праглядзець архіў" + "Агляд архіва" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOAD_ARCHIVE, @@ -272,12 +284,20 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_GOTO_EXPLORE, - "Агляд" + "Дослед" ) MSG_HASH( MENU_ENUM_SUBLABEL_GOTO_EXPLORE, "Агляд усяго адпаведнага базе даных змесціва праз інтэрфейс пошуку па катэгорыям." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_GOTO_CONTENTLESS_CORES, + "Бяззмесціўныя ядры" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_GOTO_CONTENTLESS_CORES, + "Тут адлюстроўваюцца ўсталяваныя ядры, якія могуць працаваць без загрузкі змесціва." + ) /* Main Menu > Online Updater */ @@ -315,7 +335,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_PL_THUMBNAILS_UPDATER_LIST, - "Сцягнуць мініяцюры для запісаў выбранага плэй-лісту." + "Сцягнуць мініяцюры для запісаў выбранага плэй-ліста." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_DOWNLOAD_CORE_CONTENT, @@ -329,6 +349,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_DOWNLOAD_CORE_SYSTEM_FILES, "Пампавальнік сістэмных файлаў ядраў" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_DOWNLOAD_CORE_SYSTEM_FILES, + "Сцягнуць дапаможныя сістэмныя файлы, патрэбных для слушнай/найлепшай працы ядра." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_UPDATE_CORE_INFO_FILES, "Абнавіць файлы звестак ядраў" @@ -337,10 +361,18 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_UPDATE_AUTOCONFIG_PROFILES, "Абнавіць профілі кантролераў" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_UPDATE_CHEATS, + "Абнавіць чыт-коды" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_UPDATE_DATABASES, "Абнавіць базы даных" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_UPDATE_OVERLAYS, + "Абнавіць накладкі" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_UPDATE_GLSL_SHADERS, "Абнавіць шэйдары GLSL" @@ -360,6 +392,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INFORMATION, "Звесткі аб ядры" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CORE_INFORMATION, + "Праглядзець датычныя да праграмы/ядра звесткі." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_DISC_INFORMATION, "Звесткі аб дыску" @@ -374,7 +410,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_NETWORK_INFORMATION, - "Праглядзець сеціўныя інтэрфейсы ды асацыяваныя адрасы IP." + "Праглядзець сеткавыя інтэрфейсы з асацыяванымі адрасамі IP." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFORMATION, @@ -459,6 +495,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INFO_SAVESTATE_BASIC, "Базавая (захаванне/загрузка)" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CORE_INFO_SAVESTATE_SERIALIZED, + "Серыялізаваная (захаванне/загрузка, перамотка)" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CORE_INFO_SAVESTATE_DETERMINISTIC, + "Дэтэрмінаваная (захаванне/загрузка, забяганне, сеткавая гульня)" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INFO_FIRMWARE, "Прашыўка" @@ -483,6 +527,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_LOCK, "Блакаванне ўсталяванага ядра" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CORE_SET_STANDALONE_EXEMPT, + "Выключыць з меню 'Бяззмесціўныя ядры'" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_DELETE, "Выдаленне ядра" @@ -578,17 +626,21 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_LIBRETRODB_SUPPORT, "Падтрымка LibretroDB" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_OVERLAY_SUPPORT, + "Падтрымка накладкі" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_COMMAND_IFACE_SUPPORT, "Падтрымка каманднага інтэрфейсу" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_NETWORK_COMMAND_IFACE_SUPPORT, - "Падтрымка сеціўнага каманднага інтэрфейсу" + "Падтрымка сеткавага каманднага інтэрфейсу" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_NETWORK_REMOTE_SUPPORT, - "Падтрымка сеціўнага кантролера" + "Падтрымка сеткавага кантролера" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_COCOA_SUPPORT, @@ -762,6 +814,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_STB_TRUETYPE_SUPPORT, "Падтрымка STB TrueType" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_NETPLAY_SUPPORT, + "Падтрымка сеткавай гульні (аднарангавай)" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_V4L2_SUPPORT, "Падтрымка Video4Linux2" @@ -932,6 +988,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_RDB_ENTRY_SERIAL, "Серыйны нумар" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_RDB_ENTRY_RUMBLE, + "Падтрымка груку" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_RDB_ENTRY_COOP, + "Падтрымка кааперацыі" + ) /* Main Menu > Configuration File */ @@ -961,7 +1025,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RESET_TO_DEFAULT_CONFIG, - "Скід да прадвызначэнняў" + "Скід да прадвызначанага" ) MSG_HASH( MENU_ENUM_SUBLABEL_RESET_TO_DEFAULT_CONFIG, @@ -1012,6 +1076,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_DRIVER_SETTINGS, "Драйверы" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_DRIVER_SETTINGS, + "Змяніць драйверы, ужытыя сістэмай." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_SETTINGS, "Відэа" @@ -1058,7 +1126,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SAVING_SETTINGS, - "Захоўванне" + "Захаванне" ) MSG_HASH( MENU_ENUM_SUBLABEL_SAVING_SETTINGS, @@ -1076,14 +1144,34 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_ENABLE, "Уключэнне воблачнай сінхранізацыі" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_DESTRUCTIVE, + "Дэструктыўная воблачная сінхранізацыя" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_DESTRUCTIVE, + "Калі адключана, файлы будуць пакладзеныя ў тэчку рэзервовай копіі перад іх перазапісам ці выдаленнем." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_URL, + "URL воблачнага сховішча" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_USERNAME, "Імя карыстальніка" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_USERNAME, + "Вашае імя карыстальніка да вашага акаўнта воблачнага сховішча." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_PASSWORD, "Пароль" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_PASSWORD, + "Ваш пароль да вашага акаўнта воблачнага сховішча." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOGGING_SETTINGS, "Журналяванне" @@ -1094,11 +1182,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_FILE_BROWSER_SETTINGS, - "Файлавы браўзер" + "Файлавы аглядальнік" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_FILE_BROWSER_SETTINGS, - "Змяніць налады файлавага браўзера." + "Змяніць налады файлавага аглядальніка." ) MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_CONFIG, @@ -1128,6 +1216,10 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_SHADER, "Файл шэйдара." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_CHEAT, + "Файл чыт-кодаў." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_OVERLAY, "Файл накладкі." @@ -1160,6 +1252,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_IMAGE_OPEN_WITH_VIEWER, "Відарыс. Выберыце, каб адкрыць гэты файл праз праглядальнік відарысаў." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_CORE_SELECT_FROM_COLLECTION, + "Ядро libretro. Выберыце, каб спалучыць гэтае ядро з гульнёй." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_CORE, + "Ядро libretro. Выберыце гэты файл каб RetroArch загружаў гэтае ядро." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_DIRECTORY, "Каталог. Выберыце, каб адкрыць гэты каталог." @@ -1180,6 +1280,10 @@ MSG_HASH( MENU_ENUM_SUBLABEL_USER_INTERFACE_SETTINGS, "Змяніць налады карыстальніцкага інтэрфейса." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_SETTINGS, + "Сэрвіс ШІ" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_SETTINGS, "Спецыяльныя магчымасці" @@ -1234,6 +1338,10 @@ MSG_HASH( ) /* Core option category placeholders for icons */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MAPPING_SETTINGS, + "Супастаўленні" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MEDIA_SETTINGS, "Носьбіт" @@ -1340,6 +1448,26 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_D3D12, "Драйвер Direct3D 12 з падтрымкай HDR ды фармату шэйдараў Slang." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_DISPMANX, + "Драйвер DispmanX. Ужывае DispmanX API для Videocore IV GPU у Raspberry Pi 0..3. Накладкі ды шэйдары не падтрымліваюцца." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_CACA, + "Драйвер LibCACA. Стварае сімвальны вывад замест графічнага. Не рэкамендуецца для практычнага выкарыстання." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_EXYNOS, + "Нізкаўзроўневы драйвер Exynos, які ўжывае блок G2D для аперацый блітавання ў аднакрыштальных сістэмах Samsung Exynos. Забяспечвае аптымальную прадукцыйнасць для ядраў з праграмным рэндэрынгам." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_DRM, + "Просты драйвер відэа DRM. Гэты нізкаўзроўневы драйвер відэа ўжывае libdrm для апаратнага маштабавання з дапамогай накладак GPU." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_SUNXI, + "Нізкаўзроўневы драйвер відэа Sunxi, які ўжывае блок G2D у аднакрыштальных сістэмах Allwinner." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_WIIU, "Драйвер Wii U. Падтрымлівае шэйдары Slang." @@ -1348,6 +1476,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_SWITCH, "Драйвер Switch. Падтрымлівае фармат шэйдараў GLSL." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_VG, + "Драйвер OpenVG. Ужывае графічны API вектарнай 2D-графікі з апаратнай акселерацыяй OpenVG." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_GDI, + "Драйвер GDI. Ужывае састарэлы інтэрфейс Windows. Не рэкамендуецца." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_NO_DETAILS, "Бягучы драйвер відэа." @@ -1413,7 +1549,23 @@ MSG_HASH( MENU_ENUM_SUBLABEL_MICROPHONE_DRIVER, "Ужыты драйвер мікрафона." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MICROPHONE_RESAMPLER_DRIVER, + "Перадыскрэтызацыя мікрафона" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MICROPHONE_RESAMPLER_DRIVER, + "Ужыты драйвер перадыскрэтызацыі мікрафона." + ) #endif +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AUDIO_RESAMPLER_DRIVER, + "Перадыскрэтызацыя гуку" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AUDIO_RESAMPLER_DRIVER, + "Ужыты драйвер перадыскрэтызацыі гуку." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CAMERA_DRIVER, "Камера" @@ -1505,11 +1657,35 @@ MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_SYNCHRONIZATION_SETTINGS, "Змяніць налады сінхранізацыі відэа." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_SUSPEND_SCREENSAVER_ENABLE, + "Прыпыніць ахоўнік экрана" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_SUSPEND_SCREENSAVER_ENABLE, + "Прадухіліць актыўнасць вашага сістэмнага ахоўніка экрана." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_SUSPEND_SCREENSAVER_ENABLE, + "Прыпыняе ахоўнік экрана. Служыць падказкай драйверу відэа; неабавязкова павінна выконвацца." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_BLACK_FRAME_INSERTION, + "Устаўка чорнага кадра" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_GPU_SCREENSHOT, + "Здымак экрана GPU" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_SMOOTH, "Білінейная фільтрацыя" ) #if defined(DINGUX) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_IPU_FILTER_TYPE, + "Інтэрпаляцыя выявы" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_IPU_FILTER_BICUBIC, "Бікубічная" @@ -1525,7 +1701,7 @@ MSG_HASH( #if defined(RS90) || defined(MIYOO) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_RS90_SOFTFILTER_TYPE, - "Інтэрпаляцыя паказу" + "Інтэрпаляцыя выявы" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_RS90_SOFTFILTER_POINT, @@ -1537,13 +1713,53 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FILTER, "Відэафільтр" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FILTER, + "Ужыць апрацоўчы ЦП відэафільтр. Можа значна знізіць прадукцыйнасць. Некаторыя відэафільтры працуюць толькі з ядрамі, якія выкарыстоўваюць 32- або 16-бітны колер." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_FILTER, + "Ужыць апрацоўчы ЦП відэафільтр. Можа значна знізіць прадукцыйнасць. Некаторыя відэафільтры працуюць толькі з ядрамі, якія выкарыстоўваюць 32- або 16-бітны колер. Могуць быць выбраныя дынамічна звязаныя бібліятэкі відэафільтраў." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_FILTER_BUILTIN, + "Ужыць апрацоўчы ЦП відэафільтр. Можа значна знізіць прадукцыйнасць. Некаторыя відэафільтры працуюць толькі з ядрамі, якія выкарыстоўваюць 32- або 16-бітны колер. Могуць быць выбраныя ўбудаваныя бібліятэкі відэафільтраў." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FILTER_REMOVE, + "Выдаліць відэафільтр" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FILTER_REMOVE, + "Выгрузіць любы актыўны апрацоўчы ЦП відэафільтр." + ) /* Settings > Video > CRT SwitchRes */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CRT_SWITCH_RESOLUTION_SUPER, + "Суперраздзяляльнасць ЭПТ" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CRT_SWITCH_RESOLUTION_SUPER, + "Пераключэнне паміж роднымі ды ўльтрашырокімі суперраздзяляльнасцямі." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CRT_SWITCH_X_AXIS_CENTERING, + "Цэнтраванне па восі X" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CRT_SWITCH_HIRES_MENU, "Ужываць меню высокай раздзяляльнасці" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CRT_SWITCH_RESOLUTION_USE_CUSTOM_REFRESH_RATE, + "Уласная частата абнаўлення" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CRT_SWITCH_RESOLUTION_USE_CUSTOM_REFRESH_RATE, + "Ужываць уласную частату абнаўлення, адзначаную ў файле канфігурацыі." + ) /* Settings > Video > Output */ @@ -1551,8 +1767,20 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_MONITOR_INDEX, "Індэкс манітора" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_MONITOR_INDEX, + "Выбраць, які экран дысплэя будзе выкарыстоўвацца." + ) #if defined (WIIU) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_WIIU_PREFER_DRC, + "Аптымізаваць для Wii U GamePad (патрабуецца перазапуск)" + ) #endif +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_ROTATION, + "Паварот відэа" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SCREEN_ORIENTATION, "Арыентацыя экрана" @@ -1585,10 +1813,22 @@ MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_REFRESH_RATE_AUTO, "Дакладная ацэнка частаты абнаўлення экрана ў Гц." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_REFRESH_RATE_POLLED, + "Вызначаная дысплэем частата абнаўлення" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_REFRESH_RATE_POLLED, + "Частата абнаўлення, нададзеная драйверам дысплэя." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_REFRESH_RATE, "Аўтаматычнае пераключэнне частаты абнаўлення" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_AUTOSWITCH_REFRESH_RATE, + "Пераключаць частату абнаўлення экрана на падставе бягучага змесціва." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_REFRESH_RATE_EXCLUSIVE_FULLSCREEN, "Толькі ў эксклюзіўным поўнаэкранным рэжыме" @@ -1605,6 +1845,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_PAL_THRESHOLD, "Парог аўтаматычнай частаты абнаўлення PAL" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_AUTOSWITCH_PAL_THRESHOLD, + "Максімальная частата абнаўлення, якую падтрымлівае PAL." + ) #if defined(DINGUX) && defined(DINGUX_BETA) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_REFRESH_RATE, @@ -1642,13 +1886,29 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FULLSCREEN_X, "Поўнаэкранная шырыня" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FULLSCREEN_X, + "Задаць уласны памер шырыні для неаконнага поўнаэкраннага рэжыму. Пры пакінутым нявызначаным будзе ўжыта раздзяляльнасць працоўнага стала." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FULLSCREEN_Y, "Поўнаэкранная вышыня" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FULLSCREEN_Y, + "Задаць уласны памер вышыні для неаконнага поўнаэкраннага рэжыму. Пры пакінутым нявызначаным будзе ўжыта раздзяляльнасць працоўнага стала." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FORCE_RESOLUTION, + "Прымусовая раздзяляльнасць на UWP" + ) /* Settings > Video > Windowed Mode */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_SCALE, + "Маштаб акна" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_WINDOW_OPACITY, "Непразрыстасць акна" @@ -1726,7 +1986,11 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_SCALE_INTEGER, - "Цэлалікавая шкала" + "Цэлалікавы маштаб" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_SCALE_INTEGER_OVERSCALE, + "Павялічванне цэлалікавага маштабу" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_ASPECT_RATIO_INDEX, @@ -1812,6 +2076,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_ADAPTIVE_VSYNC, "Адаптыўны VSync" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_DELAY_AUTO, + "Аўтаматычная затрымка кадраў" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_DELAY_AUTOMATIC, "Аўта" @@ -1825,13 +2093,17 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_AUDIO_OUTPUT_SETTINGS, - "Змяніць налады вываду аўдыя." + "Змяніць налады вываду гука." ) #ifdef HAVE_MICROPHONE MSG_HASH( MENU_ENUM_LABEL_VALUE_MICROPHONE_SETTINGS, "Мікрафон" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MICROPHONE_SETTINGS, + "Змяніць налады ўводу гука." + ) #endif MSG_HASH( MENU_ENUM_LABEL_VALUE_AUDIO_SYNCHRONIZATION_SETTINGS, @@ -1902,16 +2174,20 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_AUDIO_ENABLE, - "Аўдыя" + "Гук" ) MSG_HASH( MENU_ENUM_SUBLABEL_AUDIO_ENABLE, - "Уключыць вывад аўдыя." + "Уключыць вывад гука." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AUDIO_DEVICE, "Прылада" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AUDIO_LATENCY, + "Затрымка гука (мс)" + ) #ifdef HAVE_MICROPHONE /* Settings > Audio > Input */ @@ -2074,13 +2350,17 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_AUTO_GAME_FOCUS_DETECT, "Выявіць" ) +MSG_HASH( + MSG_INPUT_BIND_TIMEOUT, + "Тайм-аўт" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_TURBO_MODE_CLASSIC, "Класічны" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_TURBO_MODE_SINGLEBUTTON, - "Адна кнопка (Пераключэнне)" + "Адна кнопка (пераключэнне)" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_TURBO_MODE_SINGLEBUTTON_HOLD, @@ -2111,6 +2391,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_CLOSE_CONTENT_KEY, "Закрыць змесціва" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_RESET, + "Скінуць змесціва" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_REWIND, + "Перамотка" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_PAUSE_TOGGLE, "Паўза" @@ -2139,7 +2427,7 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_LOAD_STATE_KEY, - "Загрузка стану" + "Загрузіць стан" ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_LOAD_STATE_KEY, @@ -2147,7 +2435,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_SAVE_STATE_KEY, - "Захоўванне стану" + "Захаваць стан" ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_SAVE_STATE_KEY, @@ -2170,6 +2458,10 @@ MSG_HASH( "Паніжае бягучы індэкс слота захавання стану." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_DISK_EJECT_TOGGLE, + "Выняць дыск (пераключэнне)" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_DISK_NEXT, "Наступны дыск" @@ -2181,7 +2473,7 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_SHADER_TOGGLE, - "Шэйдары (пераключальнік)" + "Шэйдары (пераключэнне)" ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_SHADER_TOGGLE, @@ -2196,6 +2488,26 @@ MSG_HASH( "Папярэдні шэйдар" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_CHEAT_TOGGLE, + "Чыт-коды (пераключэнне)" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_CHEAT_INDEX_PLUS, + "Наступны чыт-код" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_CHEAT_INDEX_PLUS, + "Павялічвае бягучы індэкс чыт-кода." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_CHEAT_INDEX_MINUS, + "Папярэдні чыт-код" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_CHEAT_INDEX_MINUS, + "Паніжае бягучы індэкс чыт-кода." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_SCREENSHOT, @@ -2203,15 +2515,15 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_RECORDING_TOGGLE, - "Запіс (пераключальнік)" + "Запіс (пераключэнне)" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_STREAMING_TOGGLE, - "Стрымінг (пераключальнік)" + "Стрымінг (пераключэнне)" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_FULLSCREEN_TOGGLE_KEY, - "Поўнаэкранны (пераключальнік)" + "Поўнаэкранны (пераключэнне)" ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_FULLSCREEN_TOGGLE_KEY, @@ -2219,6 +2531,10 @@ MSG_HASH( ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_AI_SERVICE, + "Сэрвіс ШІ" + ) MSG_HASH( @@ -2282,7 +2598,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_JOYPAD_X, - "Кнопка X (уверсе)" + "Кнопка X (угары)" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_JOYPAD_L2, @@ -2343,6 +2659,14 @@ MSG_HASH( #endif /* Settings > Configuration */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONFIG_SAVE_ON_EXIT, + "Захоўваць канфігурацыю падчас выхаду" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONFIG_SAVE_ON_EXIT, + "Захоўваць змены ў файл канфігурацыі падчас выхаду." + ) /* Settings > Saving */ @@ -2392,12 +2716,28 @@ MSG_HASH( /* Settings > File Browser */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NAVIGATION_BROWSER_FILTER_SUPPORTED_EXTENSIONS_ENABLE, + "Фільтраваць невядомыя пашырэнні" + ) /* Settings > Frame Throttle */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_REWIND_SETTINGS, + "Перамотка" + ) /* Settings > Frame Throttle > Rewind */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_REWIND_ENABLE, + "Падтрымка перамоткі" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_REWIND_GRANULARITY, + "Кадры перамоткі" + ) /* Settings > Frame Throttle > Frame Time Counter */ @@ -2534,6 +2874,10 @@ MSG_HASH( MENU_ENUM_SUBLABEL_MEMORY_SHOW, "Адлюстроўваць ужыты ды агульны аб'ём памяці ў сістэме." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NOTIFICATION_SHOW_CHEATS_APPLIED, + "Апавяшчэнні аб чыт-кодах" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NOTIFICATION_SHOW_SCREENSHOT, "Апавяшчэнні аб здымках экрана" @@ -2562,6 +2906,18 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_NOTIFICATION_SHOW_SCREENSHOT_DURATION_INSTANT, "Імгненная" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NOTIFICATION_SHOW_SCREENSHOT_FLASH, + "Эфект успышкі здымка экрана" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NOTIFICATION_SHOW_SCREENSHOT_FLASH_NORMAL, + "УКЛ (звычайна)" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NOTIFICATION_SHOW_SCREENSHOT_FLASH_FAST, + "УКЛ (хутка)" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FONT_PATH, "Шрыфт апавяшчэнняў" @@ -2609,16 +2965,28 @@ MSG_HASH( /* Settings > User Interface */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_VIEWS_SETTINGS, + "Бачнасць пунктаў меню" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MENU_VIEWS_SETTINGS, + "Змяніць адлюстраванне элементаў меню RetroArch." + ) #ifdef _3DS #endif MSG_HASH( MENU_ENUM_LABEL_VALUE_SHOW_ADVANCED_SETTINGS, - "Паказаць пашыраныя налады" + "Паказ пашыраных налад" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_ENABLE_KIOSK_MODE, "Рэжым кіёска" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUIT_ON_CLOSE_CONTENT, + "Выхад падчас закрыцця змесціва" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SCREENSAVER_ANIMATION_SNOW, "Снег" @@ -2639,6 +3007,22 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_POINTER_ENABLE, "Падтрымка дотыкаў" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_SCROLL_FAST, + "Паскарэнне прагорткі меню" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_SCROLL_DELAY, + "Затрымка прагорткі меню" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_DESKTOP_MENU_ENABLE, + "Меню працоўнага стала (патрабуецца перазапуск)" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_UI_COMPANION_TOGGLE, + "Адкрываць меню працоўнага стала падчас запуску" + ) /* Settings > User Interface > Menu Item Visibility */ @@ -2646,13 +3030,17 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_VIEWS_SETTINGS, "Хуткае меню" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_VIEWS_SETTINGS, + "Змяніць адлюстраванне элементаў хуткага меню." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_VIEWS_SETTINGS, "Налады" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_LOAD_CORE, - "Паказваць 'Загрузіць ядро'" + "Паказ 'Загрузіць ядро'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_LOAD_CORE, @@ -2660,7 +3048,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_LOAD_CONTENT, - "Паказваць 'Загрузіць змесціва'" + "Паказ 'Загрузіць змесціва'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_LOAD_CONTENT, @@ -2668,16 +3056,24 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_LOAD_DISC, - "Паказваць 'Загрузіць дыск'" + "Паказ 'Загрузіць дыск'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_LOAD_DISC, "Паказваць опцыю 'Загрузіць дыск' у галоўным меню." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_SHOW_DUMP_DISC, + "Паказ 'Дамп дыска'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MENU_SHOW_DUMP_DISC, + "Паказваць опцыю 'Дамп дыска' у галоўным меню." + ) #ifdef HAVE_LAKKA MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_EJECT_DISC, - "Паказваць 'Выняць дыск'" + "Паказ 'Выняць дыск'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_EJECT_DISC, @@ -2686,7 +3082,7 @@ MSG_HASH( #endif MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_ONLINE_UPDATER, - "Паказваць 'Анлайнавы абнаўляльнік'" + "Паказ 'Анлайнавы абнаўляльнік'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_ONLINE_UPDATER, @@ -2694,11 +3090,15 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_CORE_UPDATER, - "Паказваць 'пампавальнік ядраў'" + "Паказ 'Пампавальнік ядраў'" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_SHOW_LEGACY_THUMBNAIL_UPDATER, + "Паказваць састарэлы 'Абнаўляльнік мініяцюр'" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_INFORMATION, - "Паказваць 'Звесткі'" + "Паказ 'Звесткі'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_INFORMATION, @@ -2706,7 +3106,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_CONFIGURATIONS, - "Паказваць 'Файл канфігурацыі'" + "Паказ 'Файл канфігурацыі'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_CONFIGURATIONS, @@ -2714,7 +3114,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_HELP, - "Паказваць 'Даведка'" + "Паказ 'Даведка'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_HELP, @@ -2722,7 +3122,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_QUIT_RETROARCH, - "Паказваць 'Выйсці з RetroArchArch'" + "Паказ 'Выйсці з RetroArch'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_QUIT_RETROARCH, @@ -2730,7 +3130,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_RESTART_RETROARCH, - "Паказваць 'Перазапусціць RetroArch'" + "Паказ 'Перазапусціць RetroArch'" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_RESTART_RETROARCH, @@ -2738,7 +3138,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_SETTINGS, - "Паказваць 'Налады'" + "Паказ 'Налады'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_SETTINGS, @@ -2746,7 +3146,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_FAVORITES, - "Паказваць 'Абранае'" + "Паказ 'Абранае'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_FAVORITES, @@ -2754,15 +3154,15 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_IMAGES, - "Паказваць 'Выявы'" + "Паказ 'Відарысы'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_IMAGES, - "Паказваць меню 'Выявы' (патрабуецца перазапуск на Ozone/XMB)." + "Паказваць меню 'Відарысы' (патрабуецца перазапуск на Ozone/XMB)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_MUSIC, - "Паказваць 'Музыка'" + "Паказ 'Музыка'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_MUSIC, @@ -2770,15 +3170,23 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_VIDEO, - "Паказваць 'Відэа'" + "Паказ 'Відэа'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_VIDEO, "Паказваць меню 'Відэа' (патрабуецца перазапуск на Ozone/XMB)." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_NETPLAY, + "Паказ 'Сеткавая гульня'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_SHOW_NETPLAY, + "Паказваць меню 'Сеткавая гульня' (патрабуецца перазапуск на Ozone/XMB)." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_HISTORY, - "Паказваць 'Гісторыя'" + "Паказ 'Гісторыя'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_HISTORY, @@ -2786,7 +3194,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_ADD, - "Паказваць 'Імпартаваць змесціва'" + "Паказ 'Імпартаваць змесціва'" ) MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_SHOW_ADD, @@ -2794,7 +3202,7 @@ MSG_HASH( ) MSG_HASH( /* FIXME can now be replaced with MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_ADD */ MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_ADD_ENTRY, - "Паказваць 'Імпартаваць змесціва'" + "Паказ 'Імпартаваць змесціва'" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_ADD_CONTENT_ENTRY_DISPLAY_MAIN_TAB, @@ -2806,7 +3214,23 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_PLAYLISTS, - "Паказваць 'Плэй-лісты'" + "Паказ 'Плэй-лісты'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_SHOW_PLAYLISTS, + "Паказваць плэй-лісты (патрабуецца перазапуск на Ozone/XMB)." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_EXPLORE, + "Паказ 'Дослед'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_SHOW_EXPLORE, + "Паказваць опцыю даследчыка змесціва (патрабуецца перазапуск на Ozone/XMB)." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_CONTENTLESS_CORES, + "Паказ 'Бяззмесціўныя ядры'" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SHOW_CONTENTLESS_CORES_ALL, @@ -2822,7 +3246,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_TIMEDATE_ENABLE, - "Паказваць дату і час" + "Паказ даты і часу" ) MSG_HASH( MENU_ENUM_SUBLABEL_TIMEDATE_ENABLE, @@ -2842,7 +3266,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_BATTERY_LEVEL_ENABLE, - "Паказваць узровень акумулятара" + "Паказ узроўню акумулятара" ) MSG_HASH( MENU_ENUM_SUBLABEL_BATTERY_LEVEL_ENABLE, @@ -2850,7 +3274,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_ENABLE, - "Паказваць назву ядра" + "Паказ назвы ядра" ) MSG_HASH( MENU_ENUM_SUBLABEL_CORE_ENABLE, @@ -2858,7 +3282,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_SHOW_SUBLABELS, - "Паказваць тлумачэнні да меню" + "Паказ тлумачэнняў да меню" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_SHOW_SUBLABELS, @@ -2866,7 +3290,7 @@ MSG_HASH( ) MSG_HASH( /* FIXME Not RGUI specific */ MENU_ENUM_LABEL_VALUE_RGUI_SHOW_START_SCREEN, - "Адлюстроўваць застаўку" + "Адлюстраванне застаўкі" ) MSG_HASH( /* FIXME Not RGUI specific */ MENU_ENUM_SUBLABEL_RGUI_SHOW_START_SCREEN, @@ -2877,7 +3301,7 @@ MSG_HASH( /* FIXME Not RGUI specific */ MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_RESUME_CONTENT, - "Паказваць 'Працягнуць'" + "Паказ 'Працягнуць'" ) MSG_HASH( MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_RESUME_CONTENT, @@ -2885,7 +3309,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_RESTART_CONTENT, - "Паказваць 'Перазапусціць'" + "Паказ 'Перазапусціць'" ) MSG_HASH( MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_RESTART_CONTENT, @@ -2893,27 +3317,79 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_CLOSE_CONTENT, - "Паказваць 'Закрыць змесціва'" + "Паказ 'Закрыць змесціва'" ) MSG_HASH( MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_CLOSE_CONTENT, "Паказваць опцыю 'Закрыць змесціва'." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_OPTIONS, + "Паказ 'Опцыі ядра'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_OPTIONS, + "Паказваць опцыю 'Опцыі ядра'." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_TAKE_SCREENSHOT, - "Паказваць 'Стварыць здымак экрана'" + "Паказ 'Стварыць здымак экрана'" ) MSG_HASH( MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_TAKE_SCREENSHOT, "Паказваць опцыю 'Стварыць здымак экрана'." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_START_RECORDING, + "Паказ 'Пачаць запіс'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_START_RECORDING, + "Паказваць опцыю 'Пачаць запіс'." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_START_STREAMING, + "Паказ 'Пачаць стрымінг'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_START_STREAMING, + "Паказваць опцыю 'Пачаць стрымінг'." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENT_SHOW_REWIND, + "Паказ 'Перамотка'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_SHOW_REWIND, + "Паказваць опцыю 'Перамотка'." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_SHADERS, - "Паказаць 'Шэйдары'" + "Паказ 'Шэйдары'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_SHADERS, + "Паказваць опцыю 'Шэйдары'." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_ADD_TO_FAVORITES, + "Паказ 'Дадаць у абранае'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_ADD_TO_FAVORITES, + "Паказваць опцыю 'Дадаць у абранае'." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_DOWNLOAD_THUMBNAILS, + "Паказ 'Сцягнуць мініяцюры'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_DOWNLOAD_THUMBNAILS, + "Паказваць опцыю 'Сцягнуць мініяцюры', калі не выконваецца змесціва." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_SHOW_INFORMATION, - "Паказваць 'Звесткі'" + "Паказ 'Звесткі'" ) MSG_HASH( MENU_ENUM_SUBLABEL_QUICK_MENU_SHOW_INFORMATION, @@ -2924,7 +3400,7 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_DRIVERS, - "Паказваць 'Драйверы'" + "Паказ 'Драйверы'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_DRIVERS, @@ -2932,23 +3408,23 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_VIDEO, - "Паказваць 'Відэа'" + "Паказ 'Відэа'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_VIDEO, - "Паказаць налады 'Відэа'." + "Паказваць налады 'Відэа'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_AUDIO, - "Паказваць 'Аўдыя'" + "Паказ 'Аўдыя'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_AUDIO, - "Паказаць налады 'Аўдыя'." + "Паказваць налады 'Аўдыя'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_INPUT, - "Паказваць 'Увод'" + "Паказ 'Увод'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_INPUT, @@ -2956,47 +3432,55 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_CORE, - "Паказаць 'Ядро'" + "Паказ 'Ядро'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_CORE, - "Паказаць налады 'Ядра'." + "Паказваць налады 'Ядро'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_CONFIGURATION, - "Паказаць 'Канфігурацыю'" + "Паказ 'Канфігурацыя'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_CONFIGURATION, - "Паказаць налады 'Канфігурацыі'." + "Паказваць налады 'Канфігурацыі'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_SAVING, - "Паказаць 'Захоўванне'" + "Паказ 'Захаванне'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_SAVING, - "Паказаць налады 'Захоўвання'." + "Паказваць налады 'Захоўвання'." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_LOGGING, + "Паказ 'Журналяванне'" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_SETTINGS_SHOW_LOGGING, + "Паказваць налады 'Журналяванне'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_FILE_BROWSER, - "Паказваць 'Файлавы браўзер'" + "Паказ 'Файлавы аглядальнік'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_FILE_BROWSER, - "Паказваць налады 'Файлавы браўзер'." + "Паказваць налады 'Файлавы аглядальнік'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_RECORDING, - "Паказваць 'Запіс'" + "Паказ 'Запіс'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_RECORDING, - "Паказаць налады 'Запіс'." + "Паказваць налады 'Запіс'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_USER_INTERFACE, - "Паказваць 'Карыстальніцкі інтэрфейс'" + "Паказ 'Карыстальніцкі інтэрфейс'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_USER_INTERFACE, @@ -3004,7 +3488,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_POWER_MANAGEMENT, - "Паказваць 'Кіраванне сілкаваннем''" + "Паказ 'Кіраванне сілкаваннем''" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_POWER_MANAGEMENT, @@ -3012,52 +3496,52 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_ACHIEVEMENTS, - "Паказаць 'Дасягненні'" + "Паказ 'Дасягненні'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_ACHIEVEMENTS, - "Паказаць налады 'дасягненняў'." + "Паказваць налады 'Дасягненні'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_NETWORK, - "Паказаць 'Сеціва'" + "Паказ 'Сеціва'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_NETWORK, - "Паказаць налады 'Сеціва'." + "Паказваць налады 'Сеціва'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_PLAYLISTS, - "Паказаць 'Плэй-ліст'" + "Паказ 'Плэй-лісты'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_PLAYLISTS, - "Паказаць налады 'Плэй-ліста'." + "Паказваць налады 'Плэй-лісты'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_USER, - "Паказаць 'Карыстальнік'" + "Паказ 'Карыстальнік'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_USER, - "Паказаць налады 'Карыстальнік'." + "Паказваць налады 'Карыстальнік'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_DIRECTORY, - "Паказаць 'Каталог'" + "Паказ 'Каталог'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_DIRECTORY, - "Паказаць налады 'Каталога'." + "Паказваць налады 'Каталог'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_STEAM, - "Паказаць 'Стрым'" + "Паказ 'Стрым'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_STEAM, - "Паказаць налады 'Стрыму'." + "Паказваць налады 'Стрым'." ) /* Settings > User Interface > Appearance */ @@ -3066,10 +3550,34 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_WALLPAPER, "Фонавы відарыс" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MENU_WALLPAPER, + "Выбраць відарыс для ўжывання ў якасці фону меню." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_WALLPAPER_OPACITY, "Непразрыстасць фону" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MENU_WALLPAPER_OPACITY, + "Змяніць непразрыстасць фонавага відарысу." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_FRAMEBUFFER_OPACITY, + "Непразрыстасць" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MENU_FRAMEBUFFER_OPACITY, + "Змяніць непразрыстасць прадвызначанага фону меню." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_USE_PREFERRED_SYSTEM_COLOR_THEME, + "Ужыванне пажаданай сістэмнай колеравай тэмы" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MENU_USE_PREFERRED_SYSTEM_COLOR_THEME, + "Ужываць колеравую тэму аперацыйнай сістэмы (калі існуе). Перавызначае налады тэмы." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_THUMBNAILS, "Першасная мініяцяюра" @@ -3097,6 +3605,14 @@ MSG_HASH( /* Settings > AI Service */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_PAUSE, + "Паўза падчас перакладу" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_PAUSE, + "Прыпыняць ядро пакуль перакладаецца экран." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_SOURCE_LANG, "Мова арыгінала" @@ -3117,13 +3633,37 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_ENABLE, "Дасягненні" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_HARDCORE_MODE_ENABLE, + "Рэжым хардкору" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_LEADERBOARDS_ENABLE, + "Дошкі лідараў" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_BADGES_ENABLE, + "Значкі дасягненняў" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_BADGES_ENABLE, + "Адлюстроўваць значкі ў спісе дасягненняў." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_TEST_UNOFFICIAL, + "Тэставыя неафіцыйныя дасягненні" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_TEST_UNOFFICIAL, + "Выкарыстоўваць неафіцыйныя дасягненні ды/або бэта функцыі ў мэтах тэставання." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCK_SOUND_ENABLE, - "Гук разблакавання" + "Гук раскрыцця" ) MSG_HASH( MENU_ENUM_SUBLABEL_CHEEVOS_UNLOCK_SOUND_ENABLE, - "Прайграваць гук пры разблакаванні дасягнення." + "Прайграваць гук пры раскрыцці дасягнення." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_AUTO_SCREENSHOT, @@ -3133,6 +3673,14 @@ MSG_HASH( MENU_ENUM_SUBLABEL_CHEEVOS_AUTO_SCREENSHOT, "Аўтаматычна ствараць здымак экрана пры набыцці дасягнення." ) +MSG_HASH( /* suggestion for translators: translate as 'Play Again Mode' */ + MENU_ENUM_LABEL_VALUE_CHEEVOS_START_ACTIVE, + "Рэжым перагульвання" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_START_ACTIVE, + "Пачынаць сеанс з актывацыяй усіх дасягненняў (нават з раней раскрытымі)." + ) /* Settings > Achievements > Appearance */ @@ -3160,35 +3708,203 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_APPEARANCE_ANCHOR_BOTTOMRIGHT, "Знізу справа" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_APPEARANCE_PADDING_AUTO, + "Выраўнаваны водступ" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_APPEARANCE_PADDING_H, + "Ручны гарызантальны водступ" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_APPEARANCE_PADDING_V, + "Ручны вертыкальны водступ" + ) /* Settings > Achievements > Visibility */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_SETTINGS, + "Бачнасць" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_SUMMARY, + "Зводка пры запуску" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_SUMMARY_ALLGAMES, + "Усе распазнаныя гульні" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_SUMMARY_HASCHEEVOS, + "Гульні з дасягненнямі" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_UNLOCK, + "Апавяшчэнні аб раскрыцці" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_UNLOCK, + "Паказвае апавяшчэнне пры раскрыцці дасягнення." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_MASTERY, + "Апавяшчэнні аб майстэрстве" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_MASTERY, + "Паказвае апавяшчэнне пры раскрыцці ўсіх дасягненняў гульні." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_PROGRESS_TRACKER, "Індыкатар хода выканання" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_LBOARD_START, + "Паведамленні аб адкрыцці дошкі лідараў" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_LBOARD_START, + "Паказвае апісанне дошкі лідараў, калі яна становіцца актыўнай." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_LBOARD_SUBMIT, + "Паведамленні аб унясенні на дошку лідараў" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_LBOARD_SUBMIT, + "Паказвае паведамленне з унесеным на дошку лідараў значэннем пры паспяховай спробе." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_LBOARD_CANCEL, + "Паведамленні аб няўдачы з дошкай лідараў" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_LBOARD_CANCEL, + "Паказвае паведамленне пры няўдалай спробе трапіць на дошку лідараў." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_ACCOUNT, + "Паведамленне аб уваходзе" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_VERBOSE_ENABLE, + "Падрабязныя паведамленні" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CHEEVOS_VERBOSE_ENABLE, + "Паказвае дадатковыя дыягнастычныя паведамленні ды памылкі." + ) /* Settings > Network */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_PUBLIC_ANNOUNCE, + "Публічны анонс сеткавай гульні" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_MITM_SERVER_LOCATION_1, + "Паўночная Амерыка (усходняе ўзбярэжжа, ЗША)" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_MITM_SERVER_LOCATION_2, + "Заходняя Еўропа" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_MITM_SERVER_LOCATION_3, + "Паўднёвая Амерыка (паўднёвы ўсход, Бразілія)" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_MITM_SERVER_LOCATION_4, + "Паўднёва-Усходняя Азія" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_IP_ADDRESS, + "Адрас сервера" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_IP_ADDRESS, + "Адрас вузла для злучэння." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_TCP_UDP_PORT, + "Порт TCP сеткавай гульні" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_MAX_CONNECTIONS, + "Максімум адначасовых злучэнняў" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_PASSWORD, + "Пароль сервера" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_START_AS_SPECTATOR, + "Рэжым назіральніка сеткавай гульні" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_START_AS_SPECTATOR, + "Пачынаць сеткавую гульню ў рэжыме назіральніка." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_FADE_CHAT, + "Згасанне чата" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_FADE_CHAT, + "Згасаць паведамленні чата з цягам часу." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_CHAT_COLOR_NAME, + "Колер чата (мянушка)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_CHAT_COLOR_NAME, + "Фармат: #RRGGBB або RRGGBB" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_CHAT_COLOR_MSG, + "Колер чата (паведамленне)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_CHAT_COLOR_MSG, + "Фармат: #RRGGBB або RRGGBB" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_ALLOW_PAUSING, + "Дазвол паўзы" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_ALLOW_PAUSING, + "Дазваляць гульцу прыпыняцца падчас сеткавай гульні." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETPLAY_REQUEST_DEVICE_I, + "Запыт прылады %u" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETPLAY_REQUEST_DEVICE_I, + "Запытваць гульню з дадзенай прыладай уводу." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_CMD_ENABLE, - "Сеціўныя каманды" + "Сеткавыя каманды" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_CMD_PORT, - "Сеціўны камандны порт" + "Сеткавы камандны порт" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_REMOTE_ENABLE, - "Сеціўны RetroPad" + "Сеткавы RetroPad" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_REMOTE_PORT, - "Базавы порт сеціўнага RetroPad" + "Базавы порт сеткавага RetroPad" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_USER_REMOTE_ENABLE, - "Сеціўны RetroPad карыстальніка %d" + "Сеткавы RetroPad карыстальніка %d" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_STDIN_CMD_ENABLE, @@ -3198,12 +3914,36 @@ MSG_HASH( MENU_ENUM_SUBLABEL_STDIN_CMD_ENABLE, "Камандны інтэрфейс stdin." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_NETWORK_ON_DEMAND_THUMBNAILS, + "Сцягванне мініяцюр на запыт" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_NETWORK_ON_DEMAND_THUMBNAILS, + "Аўтаматычна сцягваць адсутныя мініяцюры пры аглядзе плэй-лістоў. Значна ўплывае на прадукцыйнасць." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_UPDATER_SETTINGS, + "Налады абнаўляльніка" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_UPDATER_SETTINGS, + "Дазвол да налад абнаўляльніка ядраў" + ) /* Settings > Network > Updater */ MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_UPDATER_SHOW_EXPERIMENTAL_CORES, - "Паказваць эксперыментальныя ядра" + "Паказваць эксперыментальныя ядры" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CORE_UPDATER_AUTO_BACKUP, + "Рэзервовае капіраванне ядраў пры абнаўленні" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CORE_UPDATER_AUTO_BACKUP_HISTORY_SIZE, + "Памер гісторыі рэзервовых копій ядраў" ) /* Settings > Playlists */ @@ -3212,17 +3952,61 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_HISTORY_LIST_ENABLE, "Гісторыя" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_HISTORY_LIST_ENABLE, + "Весці плэй-ліст нядаўна скарыстаных гульняў, відарысаў, музыкі ды відэа." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_HISTORY_SIZE, "Памер гісторыі" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_HISTORY_SIZE, + "Абмежаваць колькасць запісаў у плэй-лістах гісторыі гульняў, відарысаў, музыкі ды відэа." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENT_FAVORITES_SIZE, + "Памер абранага" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_RENAME, + "Дазвол змены назвы запісаў" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_PLAYLIST_ENTRY_RENAME, + "Дазволіць змяняць назву запісаў плэй-ліста." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE, + "Дазвол выдалення запісаў" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_PLAYLIST_ENTRY_REMOVE, + "Дазволіць выдаляць запісы плэй-ліста." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_SORT_ALPHABETICAL, + "Алфавітнае сартаванне плэй-лістоў" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_USE_OLD_FORMAT, + "Захаванне плэй-лістоў у старым фармаце" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_PLAYLIST_USE_OLD_FORMAT, + "Запісваць плэй-лісты ў састарэлым фармаце простага тэксту. Калі выключана, плэй-лісты будуць фарматаваныя ў JSON." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_COMPRESSION, + "Сціск плэй-лістоў" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_CORE, "Ядро:" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME, - "Час выканання:" + "Нагуляна:" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED, @@ -3288,6 +4072,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_TIME_UNIT_AGO, "таму" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_LIST, + "Кіраванне плэй-лістамі" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_PLAYLIST_MANAGER_LIST, + "Зрабіць справы па вядзенню плэй-лістоў." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MANAGE, "Кіраваць" @@ -3295,6 +4087,22 @@ MSG_HASH( /* Settings > Playlists > Playlist Management */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_CLEAN_PLAYLIST, + "Ачысціць плэй-ліст" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_REFRESH_PLAYLIST, + "Абнавіць плэй-ліст" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_DELETE_PLAYLIST, + "Выдаліць плэй-ліст" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_DELETE_PLAYLIST, + "Выдаліць плэй-ліст з файлавай сістэмы." + ) /* Settings > User */ @@ -3363,6 +4171,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_ASSETS_DIRECTORY, "Спампоўкі" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CORE_ASSETS_DIRECTORY, + "Сцягнутыя файлы захоўваюцца ў гэтым каталозе." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_DYNAMIC_WALLPAPERS_DIRECTORY, "Дынамічныя фоны" @@ -3373,12 +4185,28 @@ MSG_HASH( ) MSG_HASH( /* FIXME Not RGUI specific */ MENU_ENUM_LABEL_VALUE_RGUI_BROWSER_DIRECTORY, - "Файлавы браўзер" + "Файлавы аглядальнік" + ) +MSG_HASH( /* FIXME Not RGUI specific */ + MENU_ENUM_SUBLABEL_RGUI_BROWSER_DIRECTORY, + "Задаць пачатковы каталог для файлавага аглядальніка." + ) +MSG_HASH( /* FIXME Not RGUI specific */ + MENU_ENUM_LABEL_VALUE_RGUI_CONFIG_DIRECTORY, + "Канфігурацыі" + ) +MSG_HASH( /* FIXME Not RGUI specific */ + MENU_ENUM_SUBLABEL_RGUI_CONFIG_DIRECTORY, + "Задаць пачатковы каталог для аглядальніка меню канфігурацыі." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LIBRETRO_DIR_PATH, "Ядры" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_LIBRETRO_DIR_PATH, + "Ядры libretro захоўваюцца ў гэтым каталозе." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LIBRETRO_INFO_PATH, "Звесткі ядра" @@ -3387,6 +4215,18 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CONTENT_DATABASE_DIRECTORY, "Базы даных" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_DATABASE_DIRECTORY, + "Базы даных захоўваюцца ў гэтым каталозе." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FILTER_DIR, + "Відэафільтры" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AUDIO_FILTER_DIR, + "Аўдыяфільтры" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_SHADER_DIR, "Шэйдары відэа" @@ -3407,6 +4247,22 @@ MSG_HASH( MENU_ENUM_SUBLABEL_RECORDING_CONFIG_DIRECTORY, "Канфігурацыі запісу захоўваюцца ў гэтым каталозе." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_OVERLAY_DIRECTORY, + "Накладкі" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_OVERLAY_DIRECTORY, + "Накладкі захоўваюцца ў гэтым каталозе." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_OSK_OVERLAY_DIRECTORY, + "Накладкі клавіятур" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_OSK_OVERLAY_DIRECTORY, + "Накладкі клавіятур захоўваюцца ў гэтым каталозе." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SCREENSHOT_DIRECTORY, "Здымкі экрана" @@ -3737,7 +4593,7 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_RENAME_ENTRY, - "Пераназваць" + "Змяніць назву" ) MSG_HASH( MENU_ENUM_SUBLABEL_RENAME_ENTRY, @@ -3763,6 +4619,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_INFORMATION, "Звесткі" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_DOWNLOAD_PL_ENTRY_THUMBNAILS, + "Сцягнуць мініяцюры" + ) /* Playlist Item > Set Core Association */ @@ -3882,7 +4742,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CORE_OPTIONS, - "Змяніць опцыі бягучага запушчанага змесціва." + "Змяніць опцыі бягучага выканання змесціва." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INPUT_REMAPPING_OPTIONS, @@ -3890,7 +4750,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CORE_INPUT_REMAPPING_OPTIONS, - "Змяніць кіраванне бягучым запушчаным змесцівам." + "Змяніць кіраванне бягучым выкананнем змесціва." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_DISK_OPTIONS, @@ -4147,7 +5007,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NO_MUSIC_AVAILABLE, - "Музыкі адсутнічае" + "Няма даступнай музыкі" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NO_VIDEOS_AVAILABLE, @@ -4171,7 +5031,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NO_NETWORKS_FOUND, - "Сеціва не знойдзена" + "Сеткі не знойдзены" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SEARCH, @@ -4258,7 +5118,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_MODE, - "Рэжым паказу" + "Рэжым відарыса" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_SPEECH_MODE, @@ -4268,6 +5128,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Рэжым апавядальніка" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Рэжым тэксту" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Тэкст + апавядальнік" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Відарыс + апавядальнік" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Унізе" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Угары" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Гісторыя ды абранае" @@ -4454,11 +5334,15 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_LOCKED_ENTRY, - "Заблакаванае" + "Нераскрытае" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY, - "Разблакаванае" + "Раскрытыя" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CHEEVOS_UNLOCKED_ENTRY_HARDCORE, + "Хардкор" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_UNOFFICIAL_ENTRY, @@ -4470,7 +5354,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_RECENTLY_UNLOCKED_ENTRY, - "Нядаўна разблакаванае" + "Нядаўна раскрытыя" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_ACTIVE_CHALLENGES_ENTRY, @@ -4724,7 +5608,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RGUI_ASPECT_RATIO_LOCK_INTEGER, - "Цэлалікавая шкала" + "Цэлалікавы маштаб" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_CLASSIC_RED, @@ -5122,7 +6006,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_LOAD_CORE, - "Загрузіць ядро" + "Загрузка ядра" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_LOADING_CORE, @@ -5142,7 +6026,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_TAB_FILE_BROWSER, - "Файлавы браўзер" + "Файлавы аглядальнік" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_SCREENSHOT, @@ -5262,7 +6146,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_RENAME_PLAYLIST, - "Пераназваць плэй-ліст" + "Змяніць назву плэй-ліста" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_QUESTION, @@ -5274,7 +6158,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_RENAME_FILE, - "Немагчыма пераназваць файл." + "Немагчыма змяніць назву файла." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_ADDING_FILES_TO_PLAYLIST, @@ -5342,7 +6226,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_COULD_NOT_UPDATE_PLAYLIST_ENTRY, - "Падчас абнаўлення запісу плэй-ліста адбылася памылка." + "Памылка абнаўлення запісу плэй-ліста." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_PLEASE_FILL_OUT_REQUIRED_FIELDS, @@ -5388,6 +6272,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_MENU_VIEW_OPTIONS_THUMBNAIL_TYPE, "Мініяцюра" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QT_THUMBNAIL_PACK_DOWNLOADED_SUCCESSFULLY, + "Мініяцюры паспяхова сцягнутыя." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_CORE_OPTIONS, "Опцыі ядра" @@ -5518,6 +6406,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_DESCRIPTION, "Апісанне" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_LIST_HARDCORE, + "Дасягненні (хардкор)" + ) /* Unused (Needs Confirmation) */ @@ -5730,7 +6622,7 @@ MSG_HASH( ) MSG_HASH( MSG_PLAYLIST_MANAGER_CLEANING_PLAYLIST, - "Ачыстка плэй-лісту: " + "Ачыстка плэй-ліста: " ) MSG_HASH( MSG_PLAYLIST_MANAGER_PLAYLIST_CLEANED, @@ -5780,6 +6672,10 @@ MSG_HASH( MSG_BYTES, "байтаў" ) +MSG_HASH( + MSG_CHEEVOS_HARDCORE_MODE_ENABLE, + "Уключаны рэжым хардкору, захаванне стану ды перамотка адключаныя." + ) MSG_HASH( MSG_CORE_OPTIONS_FILE_CREATED_SUCCESSFULLY, "Файл опцый ядра паспяховы створаны." @@ -5838,7 +6734,7 @@ MSG_HASH( ) MSG_HASH( MSG_RESET, - "Скінуць" + "Скід" ) MSG_HASH( MSG_SHADER, @@ -5852,6 +6748,14 @@ MSG_HASH( MSG_SCREENSHOT_SAVED, "Здымак экрана захаваны" ) +MSG_HASH( + MSG_ACHIEVEMENT_UNLOCKED, + "Раскрыта дасягненне" + ) +MSG_HASH( + MSG_PRESS_AGAIN_TO_QUIT, + "Націсніце зноў каб выйсці..." + ) MSG_HASH( MSG_UNKNOWN, "Невядомы" @@ -5860,6 +6764,10 @@ MSG_HASH( MSG_SCANNING_WIRELESS_NETWORKS, "Сканаванне бяздротавых сетак..." ) +MSG_HASH( + MSG_CHEEVOS_HARDCORE_MODE_DISABLED, + "Загружана захаванне стану. Для бягучага сеанса дасягненняў рэжым хардкору адключаны." + ) /* Lakka */ @@ -5868,15 +6776,19 @@ MSG_HASH( MSG_HASH( MENU_ENUM_LABEL_VALUE_WIFI_NETWORK_SCAN, - "Злучыцца з сецівам" + "Злучыцца з сеткай" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_WIFI_NETWORKS, - "Злучыцца з сецівам" + "Злучыцца з сеткай" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Спакой кадраў" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_RESTART_KEY, - "Перазапусціць RetroArch" + "Перазапуск RetroArch" ) #ifdef HAVE_LIBNX #endif diff --git a/intl/msg_hash_ca.h b/intl/msg_hash_ca.h index 66f4ad9a2a46..529ed1637f7d 100644 --- a/intl/msg_hash_ca.h +++ b/intl/msg_hash_ca.h @@ -4501,10 +4501,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Sortida del servei d’IA" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ - MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Mostra la traducció com una superposició de text (mode d’imatge) o reprodueix-la com a «text a parla» (mode de parla)." - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, "URL del servei d’IA" diff --git a/intl/msg_hash_chs.h b/intl/msg_hash_chs.h index 1f77a6c2b416..3ef2dca8bca1 100644 --- a/intl/msg_hash_chs.h +++ b/intl/msg_hash_chs.h @@ -4475,10 +4475,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_FASTFORWARD_FRAMESKIP, "快速转发帧数" ) -MSG_HASH( - MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "根据快速前进速率跳过帧。这将节省电力,并允许使用第三方帧限制。" - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, "慢放倍率" @@ -6313,10 +6309,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "AI 服务输出" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ - MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "将翻译显示为文本叠加图层(图像模式),或作为文本到语音播放 (语音模式)。" - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, "AI 服务 URL" @@ -9468,6 +9460,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "讲述人模式" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "顶部" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "历史和收藏" diff --git a/intl/msg_hash_cht.h b/intl/msg_hash_cht.h index 85cdae70b25a..ee1a1246c87c 100644 --- a/intl/msg_hash_cht.h +++ b/intl/msg_hash_cht.h @@ -4427,10 +4427,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_FASTFORWARD_FRAMESKIP, "快轉跳幀" ) -MSG_HASH( - MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "依據快轉倍速跳幀, 可節省裝置的電量, 並允許使用第三方的幀數限制應用程式。" - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, "慢動作倍速" @@ -6265,10 +6261,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "翻譯模式" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ - MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "設定翻譯時使用的模式。" - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, "翻譯服務網址" @@ -9448,6 +9440,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "自動朗讀模式" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "頂端" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "歷史和我的最愛" diff --git a/intl/msg_hash_cs.h b/intl/msg_hash_cs.h index 480c33cbcc8f..371625cd4034 100644 --- a/intl/msg_hash_cs.h +++ b/intl/msg_hash_cs.h @@ -4515,10 +4515,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_FASTFORWARD_FRAMESKIP, "Rychlost Posunu Vpřed Přeskočením Snímků" ) -MSG_HASH( - MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "Přeskakování snímků podle rychlosti převíjení. To šetří energii a umožňuje použití omezování snímků třetích stran." - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, "Hodnota Zpomalení" @@ -6397,10 +6393,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "AI Service Výstup" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ - MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Zobrazte překlad jako překryvný text (režim obrazu) nebo jej přehrajte jako převod textu na řeč (režim řeči)." - ) MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_URL, "Adresa URL http:// ukazující na překladatelskou službu, kterou chcete použít." @@ -9600,6 +9592,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Režim Vypravěč" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Spodní" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Nahoře" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Historie a Oblíbené Položky" diff --git a/intl/msg_hash_de.h b/intl/msg_hash_de.h index a9d5278d1eca..0171425b3a48 100644 --- a/intl/msg_hash_de.h +++ b/intl/msg_hash_de.h @@ -6357,9 +6357,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "KI-Dienst Ausgabeart" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Übersetzungen als Textüberlagerung anzeigen (Bildmodus) oder als Text-zu-Sprache abspielen (Sprachmodus)." + "Übersetzung als Bild-Overlay (Bildmodus), als direktes Audio (Sprache), Text-to-Speech (Erzähler) oder als Text-Overlay (Text) anzeigen." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6401,6 +6401,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "Die Sprache, in die der Dienst übersetzt. 'Standard' ist Englisch." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Automatische Abfrageverzögerung für KI-Dienst" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Mindestverzögerung in ms zwischen automatischen Aufrufen. Verringert die Reaktionszeit, erhöht aber die CPU-Leistung." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "KI-Dienst-Textposition überschreiben" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Die Position des Overlays übersteuern, wenn sich der Dienst im Textmodus befindet." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "KI-Dienst-Textauffüllung (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Vertikaler Abstand, der auf das Textoverlay angewendet wird, wenn der Dienst im Textmodus ist. Ein größeres Auffüllen schiebt den Text in die Mitte des Bildschirms." + ) /* Settings > Accessibility */ @@ -9516,6 +9540,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Erzählermodus" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Textmodus" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Text und Erzähler" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Bild und Erzähler" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Unten" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Oben" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Verlauf & Favoriten" @@ -12139,6 +12183,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Core Installation fehlgeschlagen" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Videotreiber wird für den KI-Dienst nicht unterstützt." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Automatische Übersetzung aktiviert." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Automatische Übersetzung deaktiviert." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Nichts zu übersetzen." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Drücke fünf Mal rechts, um alle Cheats zu löschen." @@ -14246,6 +14306,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Das Aktivieren von Linux GameMode kann die Latenzzeit verbessern, Audioknackser beheben und die Gesamtleistung maximieren, indem CPU und GPU automatisch für die beste Leistung konfiguriert werden.\nDie GameMode-Software muss installiert sein, damit dies funktioniert. Siehe https://github.com/FeralInteractive/gamemode für Informationen zur Installation von GameMode." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Bildpausen" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Versucht, die CPU-Last von vsync zu reduzieren, indem es nach der Framedarstellung so viel wie möglich ruht. In erster Linie für Scanline-Synchronisation von Drittanbietern entwickelt." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "Verwende PAL60-Modus" diff --git a/intl/msg_hash_es.h b/intl/msg_hash_es.h index ac20584c6e2a..6bd26734e14c 100644 --- a/intl/msg_hash_es.h +++ b/intl/msg_hash_es.h @@ -6469,9 +6469,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Salida del servicio de IA" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Muestra la traducción como una superposición de texto (imagen) o reproduce una conversión de texto a voz (voz)." + "Muestra las traducciones como una imagen superpuesta (Modo Imagen), audio directo (Voz), texto a voz (Narración) o una superposición de texto (Texto)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6513,6 +6513,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "Indica el idioma de destino de la traducción. En caso de seleccionar «Predeterminado», se traducirá a inglés." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Retraso al autosondear con el servicio de IA" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Indica el retraso mínimo (en ms) entre cada llamada automática. Reduce la reactividad, pero mejora el rendimiento de la CPU." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Personalizar posición de textos del servicio de IA" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Personaliza la posición de la superposición del modo Texto del servicio de IA." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Relleno de textos del servicio de IA (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Indica el relleno vertical que se añadirá a la superposición de textos cuando el servicio de IA esté configurado en el modo Texto. Si hay más relleno, el texto se desplazará hacia el centro de la pantalla." + ) /* Settings > Accessibility */ @@ -9716,6 +9740,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Modo narrador" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Modo Texto" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Texto + Narración" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Imagen + Narración" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Giro de 180°" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Inicio" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Historial y favoritos" @@ -12467,6 +12511,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Error al instalar el núcleo" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "El controlador de vídeo no es compatible con el servicio de IA." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Traducción automática activada." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Traducción automática desactivada." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "No hay nada que traducir." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Pulsa derecha cinco veces para eliminar todos los trucos." @@ -14570,6 +14630,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Activar el GameMode de Linux podría mejorar la latencia, corregir chasquidos en el audio y maximizar el rendimiento general configurando de forma automática tu CPU y GPU para sacarles el máximo partido.\nEs necesario tener instalado el software GameMode para que esta opción surta efecto. Para más información sobre cómo instalar GameMode, visita https://github.com/FeralInteractive/gamemode (en inglés)." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Poner fotogramas en reposo" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Intenta reducir el consumo de CPU de la sincronía vertical poniéndola en espera todo el tiempo posible tras presentar cada fotograma. Opción diseñada principalmente para sincronizarse con filtros de líneas de barrido («scanlines») de terceros." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "Usar modo PAL60" diff --git a/intl/msg_hash_fi.h b/intl/msg_hash_fi.h index fe1c447528a3..4a523acfb7e4 100644 --- a/intl/msg_hash_fi.h +++ b/intl/msg_hash_fi.h @@ -1188,18 +1188,50 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_ENABLE, "Ota pilvisynkronointi käyttöön" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_ENABLE, + "Yritä synkronoida asetukset, SRAM-tiedostot ja tilatallennukset pilvitallennuspalveluun." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_DESTRUCTIVE, + "Tuhoava pilvisynkronointi" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_DESTRUCTIVE, + "Kun pois käytöstä, tiedostot siirretään varmuuskopio-kansioon, ennen kuin niitä korvataan tai poistetaan." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_DRIVER, "Pilvisynkronoinnin taustaosa" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_DRIVER, + "Mitä pilvitallennusverkon protokollaa käytetään." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_URL, + "Pilvitallennustilan URL" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_URL, + "API:n aloituspisteen URL pilvitallennuspalvelussa." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_USERNAME, "Käyttäjänimi" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_USERNAME, + "Käyttäjänimesi pilvitallennustililläsi." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_PASSWORD, "Salasana" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CLOUD_SYNC_PASSWORD, + "Salasanasi pilvitallennustililläsi." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOGGING_SETTINGS, "Lokiin kirjaus" @@ -1520,6 +1552,26 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_D3D12, "Direct3D 12 -ajuri, joka tukee Slang ja HDR-varjostimien formaattia." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_DISPMANX, + "DispmanX-ajuri. Käyttää Videocore IV GPU:n DispmanX API:ta Raspberry Pi 0..3 -laitteissa. Ei päällyste- tai varjostintukea." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_CACA, + "LibCACA-ajuri. Tuottaa merkkiulostuloa grafiikan sijasta. Ei suositella käytettäväksi käytännön tarkoituksiin." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_EXYNOS, + "Alhaisen tason Exynos-videoajuri, joka käyttää Samsung Exynos SoC:ssa olevaa G2D-lohkoa blit-operaatioihin. Ohjelmistopohjaisten ydinten suorituskyky pitäisi olla optimaalinen." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_DRM, + "Tavallinen DRM-videon ajuri. Tämä on alhaisen tason videon ajuri, joka käyttää libdrm:ää laitteiston skaalaukseen käyttäen GPU-päällysteitä." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_SUNXI, + "Alhaisen tason Sunxi-videon ajuri, joka käyttää Allwinner SoC-laitteissa olevaa G2D-lohkoa." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_DRIVER_WIIU, "Wii U -ajuri. Tukee Slang-varjostimia." @@ -1548,6 +1600,14 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AUDIO_DRIVER, "Käytettävä ääniajuri." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_RSOUND, + "RSound-ajuri verkkoon perustuville äänijärjestelmille." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_OSS, + "Perinteinen Open Sound System -ajuri." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_ALSA, "Oletusarvoinen ALSA-ajuri." @@ -1560,6 +1620,10 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_TINYALSA, "ALSA-ajuri toteutettuna ilman riippuvuuksia." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_ROAR, + "RoarAudio-äänijärjestelmäajuri." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_AL, "OpenAL-ajuri." @@ -1568,6 +1632,22 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_SL, "OpenSL-ajuri." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_DSOUND, + "DirectSound-ajuri. DirectSoundia käytettiin pääasiassa Windows 95:stä Windows XP:hen." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_WASAPI, + "Windows Audio Session API -ajuri. WASAPIa käytetään pääasiassa Windows 7:ssä ja sitä uudemmissa Windows versioissa." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_PULSE, + "PulseAudio-ajuri. Jos järjestelmä käyttää PulseAudio-äänijärjestelmää, varmista, että käytät tätä ajuria, esimerkiksi ALSA:n sijaan." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_JACK, + "Jack Audio Connection Kit -ajuri." + ) #ifdef HAVE_MICROPHONE MSG_HASH( MENU_ENUM_LABEL_VALUE_MICROPHONE_DRIVER, @@ -1577,6 +1657,18 @@ MSG_HASH( MENU_ENUM_SUBLABEL_MICROPHONE_DRIVER, "Mikrofonin ajuri." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MICROPHONE_RESAMPLER_DRIVER, + "Mikrofonin näytteenottaja" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MICROPHONE_RESAMPLER_DRIVER, + "Mikrofonin näytteenottajan ajuri." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MICROPHONE_BLOCK_FRAMES, + "Mikrofonin estokuvat" + ) #endif MSG_HASH( MENU_ENUM_LABEL_VALUE_AUDIO_RESAMPLER_DRIVER, @@ -1925,6 +2017,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_REFRESH_RATE, "Automaattinen virkistystaajuuden vaihto" ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_AUTOSWITCH_REFRESH_RATE, + "Vaihda näytön virkistystaajuutta automaattisesti nykyisen sisällön perusteella." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_REFRESH_RATE_EXCLUSIVE_FULLSCREEN, "Vain yksinomaisessa koko näytön tilassa" @@ -1937,6 +2033,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_REFRESH_RATE_ALL_FULLSCREEN, "Kaikissa koko näytön tiloissa" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_AUTOSWITCH_PAL_THRESHOLD, + "Automaattisen virkistystaajuuden PAL-kynnysarvo" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_AUTOSWITCH_PAL_THRESHOLD, + "Maksimivirkistystaajuus, joka katsotaan PAL-standardin mukaiseksi." + ) #if defined(DINGUX) && defined(DINGUX_BETA) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_DINGUX_REFRESH_RATE, @@ -2448,6 +2552,10 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_AUDIO_DEVICE, "Ohita ääniajurin käyttämä oletusäänilaite. Tämä asetus riippuu ajurista." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUDIO_DEVICE_RSOUND, + "RSound-ajurille mukautettu RSound-palvelimen IP-osoite." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AUDIO_LATENCY, "Äänen viive (ms)" @@ -2576,6 +2684,10 @@ MSG_HASH( /* Settings > Audio > Mixer Settings > Mixer Stream */ +MSG_HASH( + MENU_ENUM_LABEL_MIXER_STREAM, + "Mikserivirta #%d: %s" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MIXER_ACTION_PLAY, "Toista" @@ -5785,9 +5897,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Tekoälypalvelun tuloste" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Näytä käännös tekstipäällyksenä (kuvatila), tai toista puhesyntetisaattorilla (puhetila)." + "Näytä käännös kuvapäällyksenä (Kuva-tila), suorana äänenä (Puhe), tekstistä puheeksi (Lukija) tai tekstipäällyksenä (Teksti)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -5829,6 +5941,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "Kieli johon palvelu kääntää. 'Oletus' on englanti." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Tekoälypalvelun automaattinen kyselyviive" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Vähimmäisviive millisekunteina automaattisten kutsujen välillä. Laskee viivettä, mutta lisää prosessorin käyttöä." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Tekoälypalvelun tekstin sijainnin muutos" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Sijainnin muutos, kun palvelu on tekstitilassa." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Tekoälypalvelun tekstin marginaali (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Pystysuuntainen lisämarginaali, joka lisätään tekstipäällykseen tekstitilassa. Enemmän marginaalia työntää tekstin lähemmäksi näytön keskiosaa." + ) /* Settings > Accessibility */ @@ -8704,6 +8840,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Kertojatila" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Tekstitila" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Teksti + lukija" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Kuva + lukija" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Alhaalla" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Yläreuna" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Historia ja suosikit" @@ -11451,6 +11607,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Ytimen asennus epäonnistui" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Videoajuri ei tue tekoälyaplvelua." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Automaattinen käännös käytössä." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Automaattinen käännös pois käytöstä." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Ei mitään käännettävää." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Poista kaikki huijaukset painamalla oikealle viisi kertaa." diff --git a/intl/msg_hash_fr.h b/intl/msg_hash_fr.h index 731e62f8d185..c8834c894d9e 100644 --- a/intl/msg_hash_fr.h +++ b/intl/msg_hash_fr.h @@ -6248,11 +6248,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_AI_SERVICE, - "Afficher 'Service AI'" + "Afficher 'Service IA'" ) MSG_HASH( MENU_ENUM_SUBLABEL_SETTINGS_SHOW_AI_SERVICE, - "Afficher les réglages pour 'Service AI'." + "Afficher les réglages pour 'Service IA'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SETTINGS_SHOW_ACCESSIBILITY, @@ -6429,9 +6429,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Sortie du service IA " ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Afficher la traduction en tant que surimpression de texte (mode image), ou lue en tant que Text-To-Speech (mode parole)." + "Afficher la traduction en surimpression d'image (Mode image), en audio direct (Traduction vocale), en synthèse vocale (Narrateur) ou en superposition de texte (Texte)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6473,6 +6473,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "La langue vers laquelle le service va traduire. 'Par défaut' est anglais." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Délai d'interrogation automatique du service IA" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Délai minimum en ms entre les appels automatiques. Diminue la réactivité, mais augmente les performances du processeur." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Modification de la position du texte pour le service IA" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Modifier la position de la surimpression, lorsque le service est en mode texte." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Marge intérieure du texte pour le service IA (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Marge verticale à appliquer au texte en surimpression, lorsque le service est en mode texte. Plus de remplissage poussera le texte vers le centre de l'écran." + ) /* Settings > Accessibility */ @@ -9630,12 +9654,32 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_SPEECH_MODE, - "Mode parole" + "Mode traduction vocale" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Mode narrateur" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Mode texte" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Texte + narrateur" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Image + narrateur" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Bas" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Haut" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Historique et favoris" @@ -12407,6 +12451,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Installation du cœur échouée" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Pilote vidéo non pris en charge pour le service IA." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Traduction automatique activée." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Traduction automatique désactivée." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Rien à traduire." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Appuyez cinq fois sur Droite pour supprimer tous les cheats." @@ -14506,6 +14566,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Activer le GameMode de Linux peut améliorer la latence, corriger les problèmes de crépitements audio et maximiser les performances globales en configurant automatiquement votre processeur et votre processeur graphique pour les meilleures performances.\nLe logiciel GameMode doit être installé pour que cela fonctionne. Consultez https://github.com/FeralInteractive/gamemode pour plus d'informations sur l'installation de GameMode." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Repos de l'image" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Tenter de réduire l'utilisation processeur pour la synchronisation verticale (vsync) en se mettant le plus possible en veille après la présentation d'images. Conçu principalement pour les systèmes de synchronisation des lignes de balayage tiers." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "Utiliser le mode PAL60" diff --git a/intl/msg_hash_hu.h b/intl/msg_hash_hu.h index def810e503f8..7ea73b0e116b 100644 --- a/intl/msg_hash_hu.h +++ b/intl/msg_hash_hu.h @@ -4591,10 +4591,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_FASTFORWARD_FRAMESKIP, "Képkockák eldobása gyorsításkor" ) -MSG_HASH( - MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "A gyorsítás arányának megfelelő képkocka eldobása. Energiát takarít meg és megengedi külső képfrissítés-korlátozás használatát." - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, "Lassítás aránya" @@ -6461,9 +6457,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Az AI szolgáltatás eredménye" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "A fordítást szöveges rátétként jelenítse-e meg (Kép mód), vagy olvassa fel (Beszéd mód)." + "Fordítás megjelenítése képrátétként (Kép mód), közvetlen hangként (Beszéd), felolvasott szöveként (Narrátor), vagy szöveges rátétként (Szöveg)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6505,6 +6501,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "A fordítási szolgáltatás célnyelve. \"Alapértelmezett\" az angol." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Az AI szolgáltatás lekérdezési késleltetése" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Az automatikus hívások közti minimális késleltetés. Lassítja a reakcióidőt, de növeli a CPU teljesítményt." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Az AI szolgáltatás szöveg helyzetének felülbírálata" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "A rátét pozíciójának felülbírálása, amikor a szolgáltatás Szöveges módban van." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Az AI szolgáltatás szövegének kitöltése (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "A szöveges rátét függőleges kitöltése, amikor a szolgáltatás Szöveges módban van. Nagyobb kitöltés a szöveget a képernyő közepe felé mozdítja." + ) /* Settings > Accessibility */ @@ -9700,6 +9720,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Narrátor mód" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Szöveges mód" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Szöveg + narrátor" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Kép + narrátor" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Felfordítás" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Felül" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Előzmények és kedvencek" @@ -12231,6 +12271,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Mag telepítése sikertelen" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Az AI szolgáltatás nem támogatja ezt a videomeghajtót." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Automatikus fordítás engedélyezve." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Automatikus fordítás letiltva." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Nincs mit lefordítani." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Nyomja meg a jobbra gombot ötször minden csalás törléséhez." @@ -14334,6 +14390,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "A Linux GameMode engedélyezése segíthet a késleltetésen, megjavíthatja a hang recsegését és az általános teljesítményt maximalizálhatja, a CPU és a GPU konfigurálásával a legjobb teljesítményhez.\nA GameMode programot ehhez telepíteni kell. A GameMode telepítési információi: https://github.com/FeralInteractive/gamemode." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Képkocka pihentetés" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "A függőleges szinkron CPU használatának csökkentése, a képmegjelenítés után alvó módba állítás, ameddig csak lehetséges. Elsősorban külső scanline szinkronizáló programokhoz." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "PAL60 mód használata" diff --git a/intl/msg_hash_it.h b/intl/msg_hash_it.h index 113b2d4d4eba..cd7ecfbd3150 100644 --- a/intl/msg_hash_it.h +++ b/intl/msg_hash_it.h @@ -6393,9 +6393,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Uscita del servizio IA" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Mostra la traduzione come una sovrapposizione di testo (modalità immagine), o gioca come sintesi vocale (modalità vocale)." + "Mostra la traduzione come sovrapposizione immagine (modalità immagine), come audio diretto (Speech), sintesi vocale (Narratore), o sovrapposizione testo (Testo)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6437,6 +6437,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "La lingua in cui il servizio si tradurrà. 'Predefinito' è l'inglese." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Ritardo Auto-Polling Servizio IA" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Ritardo minimo in ms tra le chiamate automatiche. Abbassa la reattività ma aumenta le prestazioni della CPU." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Sostituzione Posizione Testo Servizio IA" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Sovrascrivi la posizione dell'overlay, quando il servizio è in modalità Testo." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Riempimento testo Servizio IA (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Riempimento verticale da applicare al testo sovrapposto, quando il servizio è in modalità Testo. Più riempimento spingerà il testo verso il centro dello schermo." + ) /* Settings > Accessibility */ @@ -9600,6 +9624,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Modalità Narratore" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Modalità Testo" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Testo + Narratore" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Immagine + Narratore" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Inferiore" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Alto" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Cronologia & Preferiti" @@ -12287,6 +12331,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Installazione core fallita" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Il driver video non è supportato per il servizio IA." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Traduzione automatica abilitata." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Traduzione automatica disabilitata." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Niente da tradurre." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Premi a destra cinque volte per eliminare tutti i trucchi." @@ -14390,6 +14450,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Abilitare Linux GameMode può migliorare la latenza, correggere i problemi di crackling audio e massimizzare le prestazioni complessive configurando automaticamente la CPU e la GPU per le migliori prestazioni.\nIl software GameMode deve essere installato per funzionare. Vedi https://github.com/FeralInteractive/gamemode per informazioni su come installare GameMode." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Riposo fotogramma" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Prova a ridurre l'utilizzo della CPU vsync dormendo il più possibile dopo la presentazione del quadro. Progettato principalmente per la sincronizzazione di scanline di terze parti." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "Usa modalità PAL60" diff --git a/intl/msg_hash_ja.h b/intl/msg_hash_ja.h index 0fe4a9673f56..20dd6afd7979 100644 --- a/intl/msg_hash_ja.h +++ b/intl/msg_hash_ja.h @@ -5121,10 +5121,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "AIサービス出力" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ - MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "テキストオーバーレイで翻訳を表示する(画像モード)か, テキスト読み上げで再生します(音声モード)." - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, "AIサービスURL" @@ -5564,6 +5560,30 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_TIME_UNIT_SECONDS_PLURAL, "秒" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_TIME_UNIT_MINUTES_SINGLE, + "分" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_TIME_UNIT_MINUTES_PLURAL, + "分" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_TIME_UNIT_HOURS_SINGLE, + "時間" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_TIME_UNIT_HOURS_PLURAL, + "時間" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_TIME_UNIT_DAYS_SINGLE, + "日" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_TIME_UNIT_DAYS_PLURAL, + "日" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_SHOW_ENTRY_IDX, "プレイリストの項目にインデックス番号を表示" @@ -7380,6 +7400,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "ナレーターモード" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "先頭" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "[履歴]と[お気に入り]で許可" diff --git a/intl/msg_hash_ko.h b/intl/msg_hash_ko.h index 4af36a6aab2d..647b929eb7f2 100644 --- a/intl/msg_hash_ko.h +++ b/intl/msg_hash_ko.h @@ -4633,7 +4633,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "빨리 감기 시 빨리 감기 비율에 따라 프레임을 건너뜁니다. 전력 소모를 줄이고 서드파티 프레임 리미터를 사용할 수 있게 합니다." + "빨리 감기 비율에 따라 프레임을 건너뜁니다. 전력 소모를 줄이고 서드파티 프레임 리미터를 사용할 수 있게 합니다." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, @@ -6513,9 +6513,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "AI 서비스 출력" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "번역 결과를 텍스트 오버레이로 표시(이미지 모드)하거나, TTS로 읽어줍니다(음성 모드)." + "번역 결과를 이미지 오버레이로 표시하거나(이미지 모드), 오디오로 직접 출력하거나(음성), TTS로 읽어주거나(나레이터), 텍스트 오버레이로 표시합니다(텍스트)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6557,6 +6557,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "서비스가 번역하여 표시할 언어입니다. '기본'을 선택하면 영어로 번역합니다." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "AI 서비스 자동 폴링 딜레이" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "자동 호출 사이의 최소 딜레이(ms)입니다. 반응성이 낮아지지만 CPU 성능을 높일 수 있습니다." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "AI 서비스 텍스트 위치 재정의" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "AI 서비스를 텍스트 모드로 사용할 때, 오버레이의 위치를 재정의합니다." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "AI 서비스 텍스트 패딩 (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "AI 서비스를 텍스트 모드로 사용할 때, 텍스트 오버레이에 적용될 수직 패딩입니다. 패딩을 늘리면 텍스트가 화면 가운데로 밀려납니다." + ) /* Settings > Accessibility */ @@ -9764,6 +9788,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "나레이터 모드" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "텍스트 모드" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "텍스트 + 나레이터" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "이미지 + 나레이터" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "아래" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "상단" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "최근 실행 & 즐겨찾기" @@ -12567,6 +12611,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "코어 설치 실패" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "이 비디오 드라이버에서는 AI 서비스를 사용할 수 없습니다." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "자동 번역이 활성화되었습니다." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "자동 번역이 비활성화되었습니다." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "번역할 것이 없습니다." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "모든 치트를 삭제하려면 오른쪽을 다섯 번 입력하십시오." @@ -14682,6 +14742,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Linux에서 GameMode를 활성화하면 자동으로 CPU와 GPU를 최고 성능 모드로 변경하여 지연 시간이 줄어들고, 오디오 깨짐이 고쳐지고, 전체적인 성능이 최대화되는 등의 효과를 볼 수 있습니다.\n사용하려면 GameMode 소프트웨어가 설치되어 있어야 합니다. GameMode를 설치하는 방법은 https://github.com/FeralInteractive/gamemode 에서 확인하실 수 있습니다." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "프레임 휴식" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "프레임 표시 후에 가능한 만큼 최대한 슬립하여 수직 동기 시 CPU 사용량을 줄입니다. 서드 파티 스캔라인 동기화를 위한 기능입니다." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "PAL60 모드 사용" diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index fbdef2afffc1..b0dedb079542 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -2303,10 +2303,6 @@ MSG_HASH( MENU_ENUM_LABEL_MENU_DRIVER_XMB, "xmb" ) -MSG_HASH( - MENU_ENUM_LABEL_MENU_ENUM_THROTTLE_FRAMERATE, - "menu_throttle_framerate" - ) MSG_HASH( MENU_ENUM_LABEL_MENU_FILE_BROWSER_SETTINGS, "menu_file_browser_settings" @@ -4216,6 +4212,10 @@ MSG_HASH( MENU_ENUM_LABEL_VIDEO_FRAME_DELAY_AUTO, "video_frame_delay_auto" ) +MSG_HASH( + MENU_ENUM_LABEL_VIDEO_FRAME_REST, + "video_frame_rest" + ) MSG_HASH( MENU_ENUM_LABEL_VIDEO_SHADER_DELAY, "video_shader_delay" @@ -4744,10 +4744,6 @@ MSG_HASH( MENU_ENUM_LABEL_DEFERRED_RPL_ENTRY_ACTIONS, "deferred_rpl_entry_actions" ) -MSG_HASH( - MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE, - "menu_throttle_framerate" - ) MSG_HASH( MENU_ENUM_LABEL_OVERLAY_SETTINGS, "overlay_settings" @@ -6031,6 +6027,18 @@ MSG_HASH( MENU_ENUM_LABEL_AI_SERVICE_SOURCE_LANG, "ai_service_source_lang" ) +MSG_HASH( + MENU_ENUM_LABEL_AI_SERVICE_POLL_DELAY, + "ai_service_poll_delay" + ) +MSG_HASH( + MENU_ENUM_LABEL_AI_SERVICE_TEXT_POSITION, + "ai_service_text_position" + ) +MSG_HASH( + MENU_ENUM_LABEL_AI_SERVICE_TEXT_PADDING, + "ai_service_text_padding" + ) MSG_HASH( MENU_ENUM_LABEL_SETTINGS_SHOW_DRIVERS, "settings_show_drivers" diff --git a/intl/msg_hash_or.h b/intl/msg_hash_or.h index dc7612e2bb5f..24aa48beaf57 100644 --- a/intl/msg_hash_or.h +++ b/intl/msg_hash_or.h @@ -770,6 +770,10 @@ MSG_HASH( /* MaterialUI: Settings > User Interface > Appearance */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MATERIALUI_SHOW_NAV_BAR, + "ଦିଗଚଳନ ଦଣ୍ଡିକା ଦେଖାଇବା" + ) /* MaterialUI: Settings Options */ diff --git a/intl/msg_hash_pl.h b/intl/msg_hash_pl.h index bf72a1fdcfab..71e1a8f3b350 100644 --- a/intl/msg_hash_pl.h +++ b/intl/msg_hash_pl.h @@ -5841,9 +5841,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Wyjście usługi AI" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Pokaż tłumaczenie jako nakładkę tekstową (tryb obrazu) lub odtwarzaj jako tekst - To-Speech (tryb Speech)." + "Pokaż tłumaczenie jako nakładkę obrazu (tryb obrazu), jako bezpośredni dźwięk (Speech), tekst na mowę (Narrator) lub nakładkę tekstu (Text)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -5885,6 +5885,22 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "Język, który serwis przetłumaczy. \"Domyślnie\" to język angielski." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Opóźnienie automatycznego sprawdzania usług AI" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Minimalne opóźnienie w ms między połączeniami automatycznymi. Obniża reaktywność, ale zwiększa wydajność procesora." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Zastępowanie pozycji tekstowej usługi AI" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Zastąp pozycję nakładki, gdy usługa jest w trybie tekstowym." + ) /* Settings > Accessibility */ @@ -8680,6 +8696,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Tryb Narratora" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Dolny" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Historia i Ulubione" diff --git a/intl/msg_hash_pt_br.h b/intl/msg_hash_pt_br.h index 19fe981c8db9..979329a59d82 100644 --- a/intl/msg_hash_pt_br.h +++ b/intl/msg_hash_pt_br.h @@ -1192,6 +1192,10 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_CHEAT, "Arquivo de trapaça." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_OVERLAY, + "Arquivo de sobreposição." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_RDB, "Arquivo de banco de dados." @@ -1200,6 +1204,10 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_FONT, "Arquivo de fonte TrueType." ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_FILE_BROWSER_PLAIN_FILE, + "Arquivo simples." + ) MSG_HASH( MENU_ENUM_LABEL_HELP_FILE_BROWSER_MOVIE_OPEN, "Vídeo. Selecione para abrir este arquivo com o reprodutor de vídeo." @@ -2746,6 +2754,10 @@ MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_BIND_HOLD, "Quantidade de segundos para manter uma entrada para vinculá-la." ) +MSG_HASH( + MSG_INPUT_BIND_PRESS, + "Pressione uma tecla do teclado, mouse ou controle" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_TURBO_PERIOD, "Período do turbo" @@ -5697,10 +5709,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Formato de saída do serviço de IA" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ - MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Mostre a tradução como uma sobreposição de texto (modo imagem) ou reproduzir como conversão de texto em fala (modo de fala)." - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, "URL do serviço de IA" @@ -8484,6 +8492,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Modo narrador" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Embaixo" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Topo" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Histórico e favoritos" @@ -11051,6 +11067,18 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Falha na instalação do núcleo" ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Tradução automática ativada." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Tradução automática desativada." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Nada para traduzir." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Pressione direita cinco vezes para excluir todas as trapaças." diff --git a/intl/msg_hash_ru.h b/intl/msg_hash_ru.h index 948a49be10d2..2960bcb55867 100644 --- a/intl/msg_hash_ru.h +++ b/intl/msg_hash_ru.h @@ -73,7 +73,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_HELP_CORE_LIST, - "Выбор ядра libretro. На старте браузер отображает путь, указанный в качестве Каталога ядер. Если путь не задан, обзор начинается с корневого каталога.\nЕсли Каталог ядер является папкой, меню будет использовать её в качестве папки верхнего уровня. Если Каталог ядер представляе[...]" + "Выбор ядра libretro. При просмотре браузер открывает путь, указанный для каталога хранения ядер. Если путь не задан, просмотр начинается с корневого каталога. Если каталог хранения ядер является папкой, меню будет использовать её в качестве папки верхнего уровня. Если каталог [...]" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOAD_CONTENT_LIST, @@ -85,7 +85,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_HELP_LOAD_CONTENT_LIST, - "Выбор контента. Для загрузки контента требуется 'ядро' и непосредственно файл контента.\nУкажите в настройках каталог 'Браузер файлов', чтобы установить путь, с которого меню будет начинать обзор. Если путь не задан, меню открывается в корневом каталоге.\nБраузер фильтрует ф[...]" + "Для запуска контента требуется 'ядро' и файл с содержимым. Укажите в настройках каталогов путь для 'Браузера файлов', чтобы использовать его в качестве начальной папки. Если путь не задан, просмотр будет начинаться в корневом каталоге. Браузер будет использовать загруженно[...]" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LOAD_DISC, @@ -101,7 +101,7 @@ MSG_HASH( ) MSG_HASH( /* FIXME Is a specific image format used? Is it determined automatically? User choice? */ MENU_ENUM_SUBLABEL_DUMP_DISC, - "Скопировать физический диск во внутреннее хранилище. Копия будет сохранена в виде образа." + "Скопировать физический диск во внутреннюю память. Копия будет сохранена в файл образа." ) #ifdef HAVE_LAKKA MSG_HASH( @@ -367,7 +367,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_UPDATE_CORE_INFO_FILES, - "Обновить информационные файлы ядер" + "Обновить файлы с описаниями ядер" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_UPDATE_ASSETS, @@ -469,7 +469,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_NAME, - "Название системы" + "Наименование системы" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_MANUFACTURER, @@ -1202,7 +1202,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CLOUD_SYNC_ENABLE, - "Пытаться синхронизировать файлы конфигураций и сохранений с облачным сервисом хранения данных." + "Пытаться синхронизировать файлы конфигураций и сохранений с сервисом облачного хранения." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CLOUD_SYNC_DESTRUCTIVE, @@ -1362,7 +1362,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_ONSCREEN_DISPLAY_SETTINGS, - "Настройки оверлеев и экранных уведомлений." + "Настройки оверлея, экранной клавиатуры и уведомлений." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_USER_INTERFACE_SETTINGS, @@ -2002,7 +2002,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CRT_SWITCH_RESOLUTION_USE_CUSTOM_REFRESH_RATE, - "При необходимости использовать ручное значение частоты обновления, заданное в файле конфигурации." + "Использовать значение частоты обновления, заданное в файле конфигурации." ) /* Settings > Video > Output */ @@ -2290,11 +2290,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_SCALE_INTEGER_OVERSCALE, - "Повышать целочисленное масштабирование" + "Повышение целочисленного масштабирования" ) MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_SCALE_INTEGER_OVERSCALE, - "При целочисленном масштабировании делать округление в сторону ближайшего большего целого числа." + "Делать округление при целочисленном масштабировании в сторону большего, а не меньшего целого числа." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_ASPECT_RATIO_INDEX, @@ -2326,7 +2326,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_ASPECT_RATIO_FULL, - "Полная" + "На весь экран" ) #if defined(DINGUX) MSG_HASH( @@ -2376,7 +2376,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_CROP_OVERSCAN, - "Срезает несколько пикселей по краям изображения, которые обычно не используется разработчиками и могут содержать пиксельный мусор." + "Срезает несколько пикселей по краю картинки, которые, как правило, не используется при разработке и могут вносить искажения." ) /* Settings > Video > HDR */ @@ -2458,7 +2458,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_FRAME_DELAY, - "Уменьшает задержку ввода, но может влиять на плавность изображения. Вносит запаздывание (в миллисекундах) между выводом изображения и кадром ядра." + "Уменьшает задержку ввода, но может ухудшать плавность изображения. Вносит количество миллисекунд задержки между выводом изображения и временем кадра ядра." ) MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_FRAME_DELAY, @@ -2470,11 +2470,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_FRAME_DELAY_AUTO, - "Временно уменьшает 'Задержку кадра' для избежания падения частоты. Если 'Задержка кадра' равна 0, начальное значение соответствует половине времени кадра." + "Временно уменьшает действующую 'Задержку кадра', чтобы предотвратить выпадение кадров. При 'Задержке кадра' равной 0 точка отсчёта соответствует половине времени кадра." ) MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_FRAME_DELAY_AUTO, - "Временно уменьшает действующую 'Задержку кадра' до достижения стабильной целевой частоты обновления. Измерения начинаются с середины времени кадра, когда 'Задержка кадра' равна 0. Напр., 8 для NTSC и 10 для PAL." + "Временно уменьшает действующую 'Задержку кадра' до стабилизации целевой частоты обновления. При 'Задержке кадра' равной 0 измерение начинается с середины времени кадра. Например 8 для NTSC и 10 для PAL." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_DELAY_AUTOMATIC, @@ -2498,7 +2498,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_VIDEO_HARD_SYNC_FRAMES, - "Количество кадров, которое CPU может обрабатывать до GPU, если включена 'Жёсткая синхронизация с GPU'." + "Количество кадров, которое CPU может обрабатывать с опережением GPU при вкл. опции 'Жёсткая синхронизация с GPU'." ) MSG_HASH( MENU_ENUM_LABEL_HELP_VIDEO_HARD_SYNC_FRAMES, @@ -2863,7 +2863,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_HELP_MIDI_OUTPUT, - "Выбор устройства вывода (зависит от драйвера). Если выкл., вывод MIDI будет неактивен. Также можно указать имя устройства. Если включено и вывод MIDI поддерживается приложением/игрой, некоторые звуки (в зависимости от игры/приложения) будут генерироваться MIDI-устройством. Значе[...]" + "Выбор устройства вывода (зависит от драйвера). Если выкл., вывод MIDI будет неактивен. Также может быть задано имя устройства. При включении и наличии поддержки MIDI в приложении/игре, часть звуков (в зависимости от игры/приложения) будет генерироваться MIDI-устройством. При устан[...]" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MIDI_VOLUME, @@ -3035,7 +3035,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_SELECT_PHYSICAL_KEYBOARD, - "Использовать данное устройство как физическую клавиатуру, а не геймпад." + "Выбранное устройство будет использоваться как физ. клавиатура, а не геймпад." ) MSG_HASH( MENU_ENUM_LABEL_HELP_INPUT_SELECT_PHYSICAL_KEYBOARD, @@ -3048,7 +3048,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_SENSORS_ENABLE, - "Включить ввод с акселерометра, гироскопа и датчика освещённости, если поддерживается устройством. Может влиять на производительность и энергопотребление на ряде платформ." + "При поддержке устройством включает ввод с акселерометра, гироскопа и датчика освещённости. Может влиять на производительность и энергопотребление на ряде платформ." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_AUTO_MOUSE_GRAB, @@ -3064,7 +3064,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_AUTO_GAME_FOCUS, - "Активировать режим 'Игрового фокуса' при запуске и возобновлении контента. При значении 'Определять' опция будет включаться, если ядро поддерживает обратный вызов клавиатуры фронтендом." + "Всегда включать режим 'Игрового фокуса' при запуске и возобновлении контента. При выборе значения 'Определять' настройка будет вкл. только если в текущем ядре реализована поддержка обратного вызова клавиатуры фронтендом." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_AUTO_GAME_FOCUS_OFF, @@ -3084,15 +3084,15 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_PAUSE_ON_DISCONNECT, - "Приостанавливать контент при отключении любого контроллера. Для продолжения нажмите Start." + "Приостанавливать контент при отключении любого контроллера. Для продолжения требуется нажать Start." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_BUTTON_AXIS_THRESHOLD, - "Отклонение оси для нажатия кнопки" + "Смещение оси для нажатия кнопки" ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_BUTTON_AXIS_THRESHOLD, - "Отклонение оси, требуемое для срабатывания нажатия кнопки при использовании опции 'Аналого-цифровой режим'." + "Значение отклонения оси при котором будет фиксироваться нажатие кнопки, если исп. 'Аналого-цифровой режим'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_ANALOG_DEADZONE, @@ -3302,11 +3302,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_DISABLE_INFO_BUTTON, - "Отключить кнопку информации" + "Отключить кнопку подсказки" ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_DISABLE_INFO_BUTTON, - "Если включено, нажатия кнопки информации игнорируются." + "При включении будут игнорироваться нажатия кнопки вызова подсказки." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_DISABLE_SEARCH_BUTTON, @@ -3314,11 +3314,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_DISABLE_SEARCH_BUTTON, - "Если включено, нажатия кнопки поиска игнорируются." + "При включении будут игнорироваться нажатия кнопки поиска." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_INPUT_SWAP_OK_CANCEL, - "Поменять местами кнопки OK и Отмена" + "Поменять кнопки OK и Отмена" ) MSG_HASH( MENU_ENUM_SUBLABEL_MENU_INPUT_SWAP_OK_CANCEL, @@ -3349,7 +3349,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_ENABLE_HOTKEY, - "После привязки кнопку 'Активатор горячих клавиш' нужно удерживать для срабатывания горячих клавиш. Позволяет назначать на кнопки геймпада горячие клавиши, не влияя на стандартные действия. Привязка модификатора только к контроллеру не затрагивает сочетания клавиатуры и[...]" + "Удерживайте данную кнопку для активации горячих клавиш. Позволяет назначать доп. действия, не влияя на стандартные. Привязка модификатора только к геймпаду не затрагивает сочетания клавиатуры и наоборот, в то время как оба модификатора работают для обоих устройств." ) MSG_HASH( MENU_ENUM_LABEL_HELP_ENABLE_HOTKEY, @@ -4021,11 +4021,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RUN_AHEAD_ENABLED, - "Забегание для снижения задержки" + "Забегание для снижения задержки ввода" ) MSG_HASH( MENU_ENUM_SUBLABEL_RUN_AHEAD_ENABLED, - "Обрабатывать логику ядра на один или более кадров вперёд и затем загружать сохранение для уменьшения полученной задержки ввода." + "Обрабатывать логику ядра на один или более кадров вперёд с последующим откатом состояния для уменьшения полученной задержки ввода." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RUN_AHEAD_FRAMES, @@ -4065,7 +4065,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_PREEMPT_ENABLE, - "Повторно обрабатывать логику ядра с последними событиями ввода при изменении состояния контроллера. Действует быстрее забегания, но не исключает появление у ядер проблем со звуком при загрузке сохранений." + "Повторно обрабатывать логику ядра с последними событиями ввода при изменении состояния контроллера. Действует быстрее забегания, но может вызывать проблемы со звуком при загрузке сохранений." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PREEMPT_FRAMES, @@ -4136,11 +4136,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CORE_OPTION_CATEGORY_ENABLE, - "Разрешить ядрам отображение опций в виде подменю по категориям. Примечание: для применения изменений требуется перезагрузка ядра." + "Разрешить отображение опций ядер в виде подменю по категориям. Требуется перезагрузка ядра для применения изменений." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CORE_INFO_CACHE_ENABLE, - "Кэшировать информационные файлы ядер" + "Кэшировать файлы с описаниями ядер" ) MSG_HASH( MENU_ENUM_SUBLABEL_CORE_INFO_CACHE_ENABLE, @@ -4372,7 +4372,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_SAVESTATE_MAX_KEEP, - "Ограничивает количество сохранений, если включено 'Автоматически повышать слот сохранения'. При превышении значения создание нового сохранения удалит существующее сохранение с наименьшим индексом. Значение '0' снимает ограничение на количество сохранений." + "Ограничение количества сохранений, создаваемых при вкл. опции 'Автоматически повышать слот сохранения'. При превышении установленного значения новое сохранение удалит существующее сохранение с наименьшим индексом. Значение '0' отменяет данное ограничение." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_REPLAY_MAX_KEEP, @@ -4621,7 +4621,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "Пропуск кадров в соответствии с коэффициентом ускорения. Экономит ресурсы и позволяет использовать сторонние ограничители кадров." + "Пропускать кадры согласно коэффициенту ускоренной перемотки. Экономит ресурсы и позволяет использовать сторонние ограничители кадров." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, @@ -6501,13 +6501,13 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Режим вывода для AI-сервиса" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Выводить перевод наложением текста (Графический режим) или с помощью синтеза речи (Голосовой режим)." + "Выводить перевод графическим наложением (Режим изображения), голосом (Режим озвучивания), синтезом речи (Режим диктора) или текстовым наложением (Режим текста)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, - "URL AI-сервиса" + "Сетевой адрес AI-сервиса" ) MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_URL, @@ -6519,7 +6519,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_ENABLE, - "Активировать AI-сервис при нажатии горячей клавиши." + "Запуск AI-сервиса при нажатии горячей клавиши." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_PAUSE, @@ -6545,6 +6545,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "Язык, на который будет осуществляться перевод. По умолчанию используется английский." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Задержка автоопроса AI-сервиса" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Минимальная задержка (в мс) между автом. вызовами. Снижает быстродействие, но повышает производительность CPU." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Переопределение положения текста" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Изменяет положение оверлея при использовании AI-сервиса в режиме текста." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Смещение текста (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Отступ по вертикали для наложения при использовании AI-сервиса в режиме текста. При увеличении отступа текст будет смещаться к центру экрана." + ) /* Settings > Accessibility */ @@ -6587,7 +6611,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_LEADERBOARDS_ENABLE, - "Списки лидеров" + "Таблицы лидеров" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_RICHPRESENCE_ENABLE, @@ -6753,7 +6777,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_CHEEVOS_CHALLENGE_INDICATORS, - "Показывать на экране индикаторы, когда есть возможность открыть определённые достижения." + "Показывать на экране значки достижений для которых выполняются условия получения." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_PROGRESS_TRACKER, @@ -6765,7 +6789,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_LBOARD_START, - "Сообщение об активации таблицы лидеров" + "Сообщение о запуске таблицы лидеров" ) MSG_HASH( MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_LBOARD_START, @@ -6773,11 +6797,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_LBOARD_SUBMIT, - "Сообщение о записи в таблицу лидеров" + "Сообщение о занесении в таблицу лидеров" ) MSG_HASH( MENU_ENUM_SUBLABEL_CHEEVOS_VISIBILITY_LBOARD_SUBMIT, - "Показывать отправленное значение при занесении в таблицу лидеров." + "Показывать значение, отправленное при успешной попытке войти в таблицу лидеров." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEEVOS_VISIBILITY_LBOARD_CANCEL, @@ -6924,7 +6948,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_HELP_NETPLAY_START_AS_SPECTATOR, - "Включает режим наблюдателя для Netplay. Если включено, сетевая игра будет запускаться в режиме наблюдателя. Режим можно изменить в любое время." + "Установка режима наблюдателя для сеансов сетевой игры. При включении сетевая игра будет запускаться в режиме наблюдателя. Может быть изменено в любое время." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY_FADE_CHAT, @@ -6952,11 +6976,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY_ALLOW_PAUSING, - "Разрешать паузу" + "Разрешить паузу" ) MSG_HASH( MENU_ENUM_SUBLABEL_NETPLAY_ALLOW_PAUSING, - "Разрешать игрокам паузу во время сетевой игры." + "Разрешать игрокам приостанавливать эмуляцию во время сетевой игры." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY_ALLOW_SLAVES, @@ -6996,7 +7020,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_HELP_NETPLAY_INPUT_LATENCY_FRAMES_MIN, - "Количество кадров задержки ввода для маскировки запаздывания сетевой игры. Вносит локальную задержку ввода для макс. совмещения текущего кадра с кадрами, получаемыми из сети. Уменьшает неровность и снижает нагрузку на CPU, но увеличивает задержку ввода." + "Количество кадров задержки ввода для маскировки запаздывания во время сетевой игры. Вносит локальную задержку ввода для макс. синхронизации между текущим кадром и кадрами, получаемыми из сети. Повышает плавность и снижает нагрузку на CPU, но увеличивает задержку ввода." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY_INPUT_LATENCY_FRAMES_RANGE, @@ -7008,7 +7032,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_HELP_NETPLAY_INPUT_LATENCY_FRAMES_RANGE, - "Диапазон кадров задержки ввода для маскировки запаздывания сетевой игры. Если вкл., netplay динамически подстраивает кадры задержки ввода, чтобы сбалансировать процессорное время, задержку ввода и запаздывание сети. Уменьшает неровность и снижает нагрузку на CPU, но непредск[...]" + "Диапазон задержки ввода, используемой для маскировки запаздывания сети. Если вкл., netplay динамически подстраивает количество кадров задержки ввода для баланса между процессорным временем, задержкой ввода и запаздыванием сети. Повышает плавность и снижает нагрузку на CPU, н[...]" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY_NAT_TRAVERSAL, @@ -7601,7 +7625,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LIBRETRO_INFO_PATH, - "Информация о ядрах" + "Описания ядер" ) MSG_HASH( MENU_ENUM_SUBLABEL_LIBRETRO_INFO_PATH, @@ -7617,11 +7641,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CHEAT_DATABASE_PATH, - "Файлы чит-кодов" + "Чит-коды" ) MSG_HASH( MENU_ENUM_SUBLABEL_CHEAT_DATABASE_PATH, - "Каталог хранения файлов чит-кодов." + "Каталог хранения файлов с чит-кодами." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_FILTER_DIR, @@ -7689,7 +7713,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SCREENSHOT_DIRECTORY, - "Скриншот" + "Скриншоты" ) MSG_HASH( MENU_ENUM_SUBLABEL_SCREENSHOT_DIRECTORY, @@ -7781,11 +7805,11 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SAVESTATE_DIRECTORY, - "Сохранения" + "Быстрые сохранения" ) MSG_HASH( MENU_ENUM_SUBLABEL_SAVESTATE_DIRECTORY, - "В данном каталоге содержатся быстрые сохранения и повторы. Если не задано, файлы будут сохраняться в каталоге с контентом." + "Каталог хранения записей повторов и быстрых сохранений. Если не задано, файлы будут сохраняться в каталоге с контентом." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_CACHE_DIRECTORY, @@ -7912,7 +7936,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETPLAY_REFRESH_ROOMS, - "Обновить список Netplay-хостов" + "Обновить список сетевых хостов" ) MSG_HASH( MENU_ENUM_SUBLABEL_NETPLAY_REFRESH_ROOMS, @@ -8032,7 +8056,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM, - "Укажите своё 'название системы' для сканируемого контента. Учитывается только если для настройки 'Название системы' выбрано '<Ручной ввод>'." + "Укажите вручную 'название системы' для сканируемого контента. Применяется только если для опции 'Название системы' выбрано '<Ручной ввод>'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME, @@ -8040,7 +8064,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_CORE_NAME, - "Выберите ядро по умолчанию для запуска просканированного контента." + "Выбор ядра, используемого по умолчанию для запуска просканнированного контента." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_FILE_EXTS, @@ -8056,7 +8080,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SEARCH_RECURSIVELY, - "Если включено, в сканирование будут добавлены все подкаталоги заданного 'Каталога контента'." + "При включении настройки будут также сканироваться все подкаталоги в выбранном 'Каталоге контента'." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SEARCH_ARCHIVES, @@ -8064,7 +8088,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SEARCH_ARCHIVES, - "Если включено, сжатые файлы (.zip, .7z и пр.) будут включены в поиск совместимого контента. Может существенно влиять на скорость сканирования." + "При включении настройки в сканирование контента будут добавлены архивы (.zip, .7z и т.д.). Может существенно влиять на скорость сканирования." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_DAT_FILE, @@ -8088,7 +8112,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_OVERWRITE, - "Если включено, существующий плейлист будет удалён перед сканированием контента. Если выключено, в плейлист будут добавлены только отсутствующие записи, не затрагивая существующие." + "При включении настройки существующий плейлист будет удалён перед сканированием контента. Если выключено, в плейлист добавляются только новые записи, а существующие остаются без изменений." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_VALIDATE_ENTRIES, @@ -8146,7 +8170,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_EXPLORE_ADD_ADDITIONAL_FILTER, - "Добавить дополнительный фильтр" + "Добавить фильтр" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_EXPLORE_ITEMS_COUNT, @@ -8546,7 +8570,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SAVESTATE_LIST, - "Сохранения" + "Быстрые сохранения" ) MSG_HASH( MENU_ENUM_SUBLABEL_SAVESTATE_LIST, @@ -9734,16 +9758,36 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_MODE, - "Графический режим" + "Режим изображения" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_SPEECH_MODE, - "Голосовой режим" + "Режим озвучивания" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Режим диктора" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Режим текста" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Текст + диктор" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Изображение + диктор" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Внизу" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Вверху" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "История и избранное" @@ -10928,10 +10972,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME_LIME, "Зелёный лайм" ) -MSG_HASH( - MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME_PIKACHU_YELLOW, - "Жёлтый Пикачу" - ) MSG_HASH( MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME_GAMECUBE_PURPLE, "Фиолетовая GameCube" @@ -11422,7 +11462,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_TAB_FILE_BROWSER_TOP, - "Начало" + "Верх" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_QT_TAB_FILE_BROWSER_UP, @@ -12503,6 +12543,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Не удалось установить ядро" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Видеодрайвер не поддерживается AI-сервисом." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Автоматический перевод включен." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Автоматический перевод выключен." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Нет данных для перевода." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Нажмите вправо пять раз для удаления всех чит-кодов." @@ -12545,7 +12601,7 @@ MSG_HASH( ) MSG_HASH( MSG_NATIVE, - "Нативный" + "Нативное" ) MSG_HASH( MSG_UNKNOWN_NETPLAY_COMMAND_RECEIVED, @@ -13718,7 +13774,7 @@ MSG_HASH( ) MSG_HASH( MSG_LEADERBOARD_FAILED, - "Попытка войти в таблицу лидеров не удалась" + "Не удалось войти в таблицу лидеров" ) MSG_HASH( MSG_LEADERBOARD_SUBMISSION, @@ -14110,7 +14166,7 @@ MSG_HASH( ) MSG_HASH( MSG_CHEEVOS_HARDCORE_MODE_DISABLED_CHEAT, - "Активирован чит-код. Хардкорный режим достижений для текущего сеанса выключен." + "Активирован чит-код. Режим харкдора для достижений в текущем сеансе отключен." ) MSG_HASH( MSG_CHEEVOS_MASTERED_GAME, @@ -14357,7 +14413,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_FRONTEND_NAME, - "Название фронтенда" + "Наименование фронтенда" ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_LAKKA_VERSION, @@ -14618,6 +14674,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Включение Linux GameMode может уменьшить задержку, исправить проблемы со звуком и повысить общую производительность путём автонастройки CPU и GPU для оптимальной эффективности.\nТребуется установка программного обеспечения GameMode. Посетите https://github.com/FeralInteractive/gamemode для получения ин[...]" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Простой кадра" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Пытаться снижать нагрузку на CPU при верт. синхронизации путём максимального бездействия после вывода кадра. Предназначено главным образом для сторонних методов синхронизации строк развёртки." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "Использовать режим PAL60" diff --git a/intl/msg_hash_sk.h b/intl/msg_hash_sk.h index 3d58b4ca6108..5310c22c135f 100644 --- a/intl/msg_hash_sk.h +++ b/intl/msg_hash_sk.h @@ -3838,6 +3838,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_WALLPAPER_OPACITY, "Nepriehľadnosť pozadia" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_FRAMEBUFFER_OPACITY, + "Nepriehľadnosť" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_REMEMBER_SELECTION_ALWAYS, "Vždy" @@ -3994,6 +3998,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_CMD_ENABLE, "Sieťové príkazy" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_UPDATER_SETTINGS, + "Nastavenia aktualizátora" + ) /* Settings > Network > Updater */ @@ -4512,6 +4520,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CONTROLS, "Podľa ovládania" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_EXPLORE_BY_NARRATIVE, + "Podľa naratívu" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PERSPECTIVE, "Podľa perspektívy" @@ -4684,6 +4696,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_QUICK_MENU_START_STREAMING, "Spustiť streamovanie" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_QUICK_MENU_STOP_STREAMING, + "Ukončiť stream" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_QUICK_MENU_STOP_STREAMING, + "Ukončiť stream." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SAVESTATE_LIST, "Stavy uloženia" @@ -5020,6 +5040,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_SPEECH_MODE, "Režim reči" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Dole" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Hore" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "História & Obľúbené" @@ -5304,6 +5332,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_RIGHT_ANALOG, "Pravý analóg" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_KEY, + "Kláves %s" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_MOUSE_LEFT, "Myš 1" @@ -5343,10 +5375,22 @@ MSG_HASH( /* RGUI: Settings > User Interface > Appearance */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_LINEAR_FILTER, + "Lineárny filter" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MENU_RGUI_ASPECT_RATIO, "Pomer strán" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME, + "Farebná téma" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MENU_RGUI_TRANSPARENCY, + "Priehľadnosť" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_THUMBNAILS_RGUI, "Horná miniatúra" @@ -5454,6 +5498,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_APPLE_GREEN, "Jablková zelená" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_VOLCANIC_RED, + "Sopečná červená" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_LAGOON, "Lagúna" @@ -5462,6 +5510,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_DRACULA, "Drakula" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_FLATUI, + "Ploché UI" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_RGUI_MENU_COLOR_THEME_HACKING_THE_KERNEL, "Hackovanie jadra" @@ -5501,10 +5553,22 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_DYNAMIC_WALLPAPER, "Dynamické pozadie" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_XMB_FONT, + "Písmo" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_XMB_LAYOUT, + "Rozloženie" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_XMB_SWITCH_ICONS, "Vymeniť ikony" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME, + "Farebná téma" + ) MSG_HASH( MENU_ENUM_SUBLABEL_XMB_MENU_COLOR_THEME, "Vyberte iný motív farby pozadia." @@ -5584,6 +5648,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME_UNDERSEA, "Pod morom" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME_VOLCANIC_RED, + "Sopečná červená" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_XMB_MENU_COLOR_THEME_LIME, "Limetková zelená" @@ -5595,6 +5663,10 @@ MSG_HASH( /* Ozone: Settings > User Interface > Appearance */ +MSG_HASH( + MENU_ENUM_LABEL_VALUE_OZONE_MENU_COLOR_THEME, + "Farebná téma" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_OZONE_COLOR_THEME_BASIC_WHITE, "Základná biela" @@ -5638,6 +5710,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_MATERIALUI_SWITCH_ICONS, "Vymeniť ikony" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MATERIALUI_MENU_COLOR_THEME, + "Farebná téma" + ) MSG_HASH( MENU_ENUM_SUBLABEL_MATERIALUI_MENU_COLOR_THEME, "Vyberte iný motív farby pozadia." @@ -5665,6 +5741,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_MATERIALUI_MENU_COLOR_THEME_GREEN, "Zelená" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MATERIALUI_MENU_COLOR_THEME_NVIDIA_SHIELD, + "Štít" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_MATERIALUI_MENU_COLOR_THEME_RED, "Červená" @@ -6504,6 +6584,10 @@ MSG_HASH( MSG_FRAMES, "Snímky" ) +MSG_HASH( + MSG_INPUT_RENAME_ENTRY, + "Premenovať názov" + ) MSG_HASH( MSG_INTERFACE, "Rozhranie" @@ -6548,6 +6632,10 @@ MSG_HASH( MSG_PAUSED, "Pozastavené." ) +MSG_HASH( + MSG_RECORDING_TO, + "Záznam do" + ) MSG_HASH( MSG_REWINDING, "Pretáčam." @@ -6564,6 +6652,10 @@ MSG_HASH( MSG_SENDING_COMMAND, "Posielam príkaz" ) +MSG_HASH( + MSG_SLOW_MOTION, + "Spomalený záber." + ) MSG_HASH( MSG_FAST_FORWARD, "Pretočiť dopredu." @@ -6818,6 +6910,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_RESTART_KEY, "Reštartovať RetroArch" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AUDIO_BLOCK_FRAMES, + "Blokovať snímky" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_TOUCH_ENABLE, "Dotyk" diff --git a/intl/msg_hash_sv.h b/intl/msg_hash_sv.h index 832c3d008e61..826b81a72dc6 100644 --- a/intl/msg_hash_sv.h +++ b/intl/msg_hash_sv.h @@ -3792,6 +3792,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_MODE, "Bildläge" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Botten" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_ALL, "Alla spellistor" diff --git a/intl/msg_hash_tr.h b/intl/msg_hash_tr.h index ba079f1e1409..67cb555e1945 100644 --- a/intl/msg_hash_tr.h +++ b/intl/msg_hash_tr.h @@ -4597,7 +4597,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "Hızlı ileri sarma hızına göre kareleri atlayın. Bu güç tasarrufu sağlar ve 3. taraf kare sınırlamasının kullanılmasına izin verir." + "Hızlı ileri sarma hızına göre kareleri atlayın. Bu güç tasarrufu sağlar ve üçüncü taraf kare sınırlamasının kullanılmasına izin verir." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, @@ -6473,9 +6473,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "Çeviri Servisi Çıkışı" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Çeviriyi metin kaplama olarak gösterin (Resim Kipi) veya Metinden Konuşmaya (Konuşma Kipi) olarak oynatın." + "Çeviriyi görüntü katmanı (Görüntü Modu), doğrudan ses (Konuşma), metinden konuşmaya (Anlatıcı) veya metin katmanı (Metin) olarak gösterin." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6517,6 +6517,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "Hizmetin çevireceği dil. \"Varsayılan\" İngilizcedir." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "Çeviri Hizmeti Otomatik Seçim Gecikmesi" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Otomatik çağrılar arasında ms cinsinden asgari gecikme. Tepkimeyi azaltır ancak CPU performansını artırır." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "Çeviri Hizmeti Metin Konumunu Özelleştir" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Hizmet Metin modundayken kaplamanın konumunu özelleştirin." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "Çeviri Hizmeti Metin Dolgusu (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Hizmet Metin modundayken metin kaplamasına uygulanacak dikey dolgu. Daha fazla dolgu, metni ekranın ortasına doğru itecektir." + ) /* Settings > Accessibility */ @@ -9716,6 +9740,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Ekran Okuyucusu Kipi" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Metin Modu" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Metin + Anlatıcı" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Resim + Anlatıcı" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Alt" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Üst" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Geçmiş & Sık Kullanılanlar" @@ -12555,6 +12599,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Çekirdek kurulumu başarısız" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Video sürücüsü Çeviri Hizmeti için desteklenmiyor." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Otomatik çeviri etkin." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Otomatik çeviri devre dışı." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Çevirecek birşey yok." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Tüm hileleri silmek için beş kez sağa basın." @@ -14654,6 +14714,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Linux GameMode etkinleştirilmesi, en iyi performans için CPU ve GPU'nuzu otomatik olarak yapılandırarak gecikmeyi iyileştirebilir, ses cızırtı sorunlarını düzeltebilir ve genel performansı en üst düzeye çıkarabilir.\nBunun çalışması için GameMode yazılımının yüklenmesi gerekir. GameMode nasıl kurulacağı hakkında bilgi için https://github.com/FeralInteractive/gamemode adresine bakın." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Kare Desteği" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Kare sunumundan sonra mümkün olduğunca uyuyarak vsync CPU kullanımını azaltmaya çalışın. Öncelikle üçüncü taraf tarama hattı eşitlenmesi için tasarlandı." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "PAL60 Kipi Kullan" diff --git a/intl/msg_hash_uk.h b/intl/msg_hash_uk.h index b68347447827..acf624466514 100644 --- a/intl/msg_hash_uk.h +++ b/intl/msg_hash_uk.h @@ -3539,10 +3539,6 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_FASTFORWARD_RATIO, "Частота перемотування" ) -MSG_HASH( - MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "Пропускати кадри відповідно до швидкості перемотування. Це економить енергію і дозволяє використовувати сторонні обмежувачі кадрів." - ) /* Settings > Frame Throttle > Rewind */ @@ -5272,6 +5268,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Режим Оповідача" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Верх" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "Історія та Обране" diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 9da8361c63ca..3bdcf68947e0 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -4665,7 +4665,7 @@ MSG_HASH( ) MSG_HASH( MENU_ENUM_SUBLABEL_FASTFORWARD_FRAMESKIP, - "Skip frames according to fast-forward rate. This conserves power and allows the use of 3rd party frame limiting." + "Skip frames according to fast-forward rate. This conserves power and allows the use of third party frame limiting." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SLOWMOTION_RATIO, @@ -6565,9 +6565,9 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_MODE, "AI Service Output" ) -MSG_HASH( /* FIXME What does the Narrator mode do? */ +MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_MODE, - "Show translation as a text overlay (Image Mode), or play as Text-To-Speech (Speech Mode)." + "Show translation as an image overlay (Image Mode), as direct audio (Speech), text-to-speech (Narrator), or text overlay (Text)." ) MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_URL, @@ -6609,6 +6609,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_AI_SERVICE_TARGET_LANG, "The language the service will translate to. 'Default' is English." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + "AI Service Auto-Polling Delay" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY, + "Minimum delay in ms between automatic calls. Lowers reactivity but increases CPU performance." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + "AI Service Text Position Override" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION, + "Override for the position of the overlay, when the service is in Text mode." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + "AI Service Text Padding (%)" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING, + "Vertical padding to apply to the text overlay, when the service is in Text mode. More padding will push the text towards the center of the screen." + ) /* Settings > Accessibility */ @@ -9856,6 +9880,10 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_LANG_BASQUE, "Basque - Euskara" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_LANG_BELARUSIAN, + "Belarusian - Беларуская мова" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_LANG_BENGALI, "Bengali - বাংলা (Restart Required)" @@ -10176,6 +10204,26 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, "Narrator Mode" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + "Text Mode" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + "Text + Narrator" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + "Image + Narrator" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, + "Bottom" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + "Top" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_ENTRY_REMOVE_ENABLE_HIST_FAV, "History & Favorites" @@ -13239,6 +13287,22 @@ MSG_HASH( /* FIXME Should be MSG_ */ MENU_ENUM_LABEL_VALUE_SIDELOAD_CORE_ERROR, "Core installation failed" ) +MSG_HASH( + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + "Video driver not supported for AI Service." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_ENABLED, + "Automatic translation enabled." + ) +MSG_HASH( + MSG_AI_AUTO_MODE_DISABLED, + "Automatic translation disabled." + ) +MSG_HASH( + MSG_AI_NOTHING_TO_TRANSLATE, + "Nothing to translate." + ) MSG_HASH( MSG_CHEAT_DELETE_ALL_INSTRUCTIONS, "Press right five times to delete all cheats." @@ -15370,6 +15434,14 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_GAMEMODE_ENABLE, "Enabling Linux GameMode can improve latency, fix audio crackling issues and maximize overall performance by automatically configuring your CPU and GPU for best performance.\nThe GameMode software needs to be installed for this to work. See https://github.com/FeralInteractive/gamemode for information on how to install GameMode." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + "Frame Rest" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST, + "Try to reduce vsync CPU usage by sleeping as much as possible after frame presentation. Designed primarily for third party scanline sync." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAL60_ENABLE, "Use PAL60 Mode" diff --git a/intl/progress.h b/intl/progress.h index f70711c5ddab..bc38eed8e264 100644 --- a/intl/progress.h +++ b/intl/progress.h @@ -1,5 +1,5 @@ /* Arabic */ -#define LANGUAGE_PROGRESS_ARABIC_TRANSLATED 47 +#define LANGUAGE_PROGRESS_ARABIC_TRANSLATED 46 #define LANGUAGE_PROGRESS_ARABIC_APPROVED 0 /* Asturian */ @@ -7,7 +7,7 @@ #define LANGUAGE_PROGRESS_ASTURIAN_APPROVED 5 /* Belarusian */ -#define LANGUAGE_PROGRESS_BELARUSIAN_TRANSLATED 18 +#define LANGUAGE_PROGRESS_BELARUSIAN_TRANSLATED 24 #define LANGUAGE_PROGRESS_BELARUSIAN_APPROVED 0 /* Catalan */ @@ -35,7 +35,7 @@ #define LANGUAGE_PROGRESS_GREEK_APPROVED 0 /* English, United Kingdom */ -#define LANGUAGE_PROGRESS_ENGLISH_UNITED_KINGDOM_TRANSLATED 99 +#define LANGUAGE_PROGRESS_ENGLISH_UNITED_KINGDOM_TRANSLATED 98 #define LANGUAGE_PROGRESS_ENGLISH_UNITED_KINGDOM_APPROVED 0 /* Esperanto */ @@ -44,15 +44,15 @@ /* Spanish */ #define LANGUAGE_PROGRESS_SPANISH_TRANSLATED 100 -#define LANGUAGE_PROGRESS_SPANISH_APPROVED 95 +#define LANGUAGE_PROGRESS_SPANISH_APPROVED 94 /* Persian */ #define LANGUAGE_PROGRESS_PERSIAN_TRANSLATED 7 #define LANGUAGE_PROGRESS_PERSIAN_APPROVED 0 /* Finnish */ -#define LANGUAGE_PROGRESS_FINNISH_TRANSLATED 78 -#define LANGUAGE_PROGRESS_FINNISH_APPROVED 49 +#define LANGUAGE_PROGRESS_FINNISH_TRANSLATED 79 +#define LANGUAGE_PROGRESS_FINNISH_APPROVED 50 /* French */ #define LANGUAGE_PROGRESS_FRENCH_TRANSLATED 100 @@ -71,7 +71,7 @@ #define LANGUAGE_PROGRESS_CROATIAN_APPROVED 0 /* Hungarian */ -#define LANGUAGE_PROGRESS_HUNGARIAN_TRANSLATED 100 +#define LANGUAGE_PROGRESS_HUNGARIAN_TRANSLATED 99 #define LANGUAGE_PROGRESS_HUNGARIAN_APPROVED 0 /* Indonesian */ @@ -83,7 +83,7 @@ #define LANGUAGE_PROGRESS_ITALIAN_APPROVED 0 /* Japanese */ -#define LANGUAGE_PROGRESS_JAPANESE_TRANSLATED 63 +#define LANGUAGE_PROGRESS_JAPANESE_TRANSLATED 62 #define LANGUAGE_PROGRESS_JAPANESE_APPROVED 0 /* Korean */ @@ -104,10 +104,10 @@ /* Polish */ #define LANGUAGE_PROGRESS_POLISH_TRANSLATED 79 -#define LANGUAGE_PROGRESS_POLISH_APPROVED 25 +#define LANGUAGE_PROGRESS_POLISH_APPROVED 24 /* Portuguese, Brazilian */ -#define LANGUAGE_PROGRESS_PORTUGUESE_BRAZILIAN_TRANSLATED 79 +#define LANGUAGE_PROGRESS_PORTUGUESE_BRAZILIAN_TRANSLATED 78 #define LANGUAGE_PROGRESS_PORTUGUESE_BRAZILIAN_APPROVED 3 /* Portuguese */ @@ -116,7 +116,7 @@ /* Russian */ #define LANGUAGE_PROGRESS_RUSSIAN_TRANSLATED 100 -#define LANGUAGE_PROGRESS_RUSSIAN_APPROVED 17 +#define LANGUAGE_PROGRESS_RUSSIAN_APPROVED 16 /* Slovak */ #define LANGUAGE_PROGRESS_SLOVAK_TRANSLATED 24 @@ -148,9 +148,9 @@ /* Chinese Simplified */ #define LANGUAGE_PROGRESS_CHINESE_SIMPLIFIED_TRANSLATED 96 -#define LANGUAGE_PROGRESS_CHINESE_SIMPLIFIED_APPROVED 49 +#define LANGUAGE_PROGRESS_CHINESE_SIMPLIFIED_APPROVED 48 /* Chinese Traditional */ -#define LANGUAGE_PROGRESS_CHINESE_TRADITIONAL_TRANSLATED 95 +#define LANGUAGE_PROGRESS_CHINESE_TRADITIONAL_TRANSLATED 94 #define LANGUAGE_PROGRESS_CHINESE_TRADITIONAL_APPROVED 83 diff --git a/intl/steam_ru.json b/intl/steam_ru.json index 81e53f1c8063..20e52064129d 100644 --- a/intl/steam_ru.json +++ b/intl/steam_ru.json @@ -10,5 +10,5 @@ "sameboy-desc": "[img]{STEAM_APP_IMAGE}/extras/SAMEBOY_(Phone).png[/img]\nSameBoy - это высокоточный эмулятор, успешно запускающий даже те игры, перед которыми пасуют все прочие эмуляторы. Помимо высокой точности, SameBoy имеет ряд приятных особенностей, таких как выбор эмулируемой модели устройства независимо от типа игры, цветовые палитры по выбору пользователя, встроенный HLE BIOS, а также возможность загрузки рамок для игр с их поддержкой.", "stella-desc": "[img]{STEAM_APP_IMAGE}/extras/stealla.png[/img]\n\nStella - это бесплатный open source эмулятор одной из самых популярных и знаковых игровых консолей 1970-х и 80-х. Изначально разработанный для операционных систем GNU/Linux, эмулятор со временем был портирован на множество других платформ, включая libretro, который запускает его посредством игровой мультимедийной системы RetroArch.\n\n[img]{STEAM_APP_IMAGE}/extras/2600.png[/img]\n\nЗа последние годы разработчики Stella добились значительных успехов в имитации многих особенностей поддерживаемых Stella консолей, обеспечив совместимость с массой трудноэмулируемых игр, homebrew программ и демок.\n\nRetroArch позволит придать этому классическому эмулятору современный облик с помощью перемотки в реальном времени, RetroAchievements, лучших в своём классе шейдеров для эмуляции ЭЛТ-дисплеев и других возможностей.", "requirements": "Процессор: Intel Pentium 4 или выше (с обязательной поддержкой SSE2). \nПроцессор (рекомендовано): Intel Core или эквивалентный AMD. \nГрафика: любой GPU с поддержкой OpenGL 2.x или Direct3D11. Поддержка Shader Model не ниже версии 2.0 для корректной работы шейдеров. \nГрафика (рекомендовано): Intel: не ниже Intel HD 4K для OpenGL, любой GPU, совместимый с D3D11 для Direct3D 11. Поддержка Shader Model не ниже 3.0 и/или 4.0. \nПримечание: для OpenGL: на Windows 10 графические чипы Intel HD 2K/3K будут использовать драйвер OpenGL 1.1.", - "legal-limits": "RetroArch является бесплатным программным обеспечением с открытым исходным кодом, распространяемым по лицензии GNU GPL 3.0. \nНе содержит материалы третьих лиц, защищённые авторским правом. RetroArch не поощряет любые проявления пиратства." + "legal-limits": "RetroArch является бесплатным программным обеспечением с открытым исходным кодом, распространяемым по лицензии GNU GPL 3.0. \nНе содержит материалы третьих лиц, защищённые авторским правом. RetroArch не поощряет пиратство в любой форме." } diff --git a/libretro-common/include/libretro.h b/libretro-common/include/libretro.h index 54c05ca62c48..cb1c2d25ef96 100644 --- a/libretro-common/include/libretro.h +++ b/libretro-common/include/libretro.h @@ -291,6 +291,7 @@ enum retro_language RETRO_LANGUAGE_CATALAN = 29, RETRO_LANGUAGE_BRITISH_ENGLISH = 30, RETRO_LANGUAGE_HUNGARIAN = 31, + RETRO_LANGUAGE_BELARUSIAN = 32, RETRO_LANGUAGE_LAST, /* Ensure sizeof(enum) == sizeof(int) */ diff --git a/libretro-common/include/lists/dir_list.h b/libretro-common/include/lists/dir_list.h index 60fef108fa17..39a0f75217f2 100644 --- a/libretro-common/include/lists/dir_list.h +++ b/libretro-common/include/lists/dir_list.h @@ -85,6 +85,15 @@ bool dir_list_initialize(struct string_list *list, **/ void dir_list_sort(struct string_list *list, bool dir_first); +/** + * dir_list_sort_ignore_ext: + * @list : pointer to the directory listing. + * @dir_first : move the directories in the listing to the top? + * + * Sorts a directory listing. File extensions are ignored. + **/ +void dir_list_sort_ignore_ext(struct string_list *list, bool dir_first); + /** * dir_list_free: * @list : pointer to the directory listing diff --git a/libretro-common/include/retro_miscellaneous.h b/libretro-common/include/retro_miscellaneous.h index 953ab1860017..a42ed176b683 100644 --- a/libretro-common/include/retro_miscellaneous.h +++ b/libretro-common/include/retro_miscellaneous.h @@ -90,7 +90,7 @@ static INLINE bool bits_any_different(uint32_t *a, uint32_t *b, uint32_t count) } #ifndef PATH_MAX_LENGTH -#if defined(_XBOX1) || defined(_3DS) || defined(PSP) || defined(PS2) || defined(GEKKO)|| defined(WIIU) || defined(__PSL1GHT__) || defined(__PS3__) +#if defined(_XBOX1) || defined(_3DS) || defined(PSP) || defined(PS2) || defined(GEKKO)|| defined(WIIU) || defined(__PSL1GHT__) || defined(__PS3__) || defined(HAVE_EMSCRIPTEN) #define PATH_MAX_LENGTH 512 #else #define PATH_MAX_LENGTH 4096 diff --git a/libretro-common/lists/dir_list.c b/libretro-common/lists/dir_list.c index e5e818295452..13a2b2267ca7 100644 --- a/libretro-common/lists/dir_list.c +++ b/libretro-common/lists/dir_list.c @@ -46,6 +46,22 @@ static int qstrcmp_plain(const void *a_, const void *b_) return strcasecmp(a->data, b->data); } +static int qstrcmp_plain_noext(const void *a_, const void *b_) +{ + const struct string_list_elem *a = (const struct string_list_elem*)a_; + const struct string_list_elem *b = (const struct string_list_elem*)b_; + + const char *ext_a = path_get_extension(a->data); + size_t l_a = string_is_empty(ext_a) ? strlen(a->data) : (ext_a - a->data - 1); + const char *ext_b = path_get_extension(b->data); + size_t l_b = string_is_empty(ext_b) ? strlen(b->data) : (ext_b - b->data - 1); + + int rv = strncasecmp(a->data, b->data, MIN(l_a, l_b)); + if (rv == 0 && l_a != l_b) + return (int)(l_a - l_b); + return rv; +} + static int qstrcmp_dir(const void *a_, const void *b_) { const struct string_list_elem *a = (const struct string_list_elem*)a_; @@ -59,6 +75,19 @@ static int qstrcmp_dir(const void *a_, const void *b_) return strcasecmp(a->data, b->data); } +static int qstrcmp_dir_noext(const void *a_, const void *b_) +{ + const struct string_list_elem *a = (const struct string_list_elem*)a_; + const struct string_list_elem *b = (const struct string_list_elem*)b_; + int a_type = a->attr.i; + int b_type = b->attr.i; + + /* Sort directories before files. */ + if (a_type != b_type) + return b_type - a_type; + return qstrcmp_plain_noext(a, b); +} + /** * dir_list_sort: * @list : pointer to the directory listing. @@ -73,6 +102,20 @@ void dir_list_sort(struct string_list *list, bool dir_first) dir_first ? qstrcmp_dir : qstrcmp_plain); } +/** + * dir_list_sort_ignore_ext: + * @list : pointer to the directory listing. + * @dir_first : move the directories in the listing to the top? + * + * Sorts a directory listing. File extensions are ignored. + **/ +void dir_list_sort_ignore_ext(struct string_list *list, bool dir_first) +{ + if (list) + qsort(list->elems, list->size, sizeof(struct string_list_elem), + dir_first ? qstrcmp_dir_noext : qstrcmp_plain_noext); +} + /** * dir_list_free: * @list : pointer to the directory listing @@ -141,6 +184,11 @@ static int dir_list_read(const char *dir, if (retro_dirent_is_dir(entry, NULL)) { + /* Exclude this frequent hidden dir on platforms which can not handle hidden attribute */ +#ifndef _WIN32 + if (!include_hidden && strcmp(name, "System Volume Information") == 0) + continue; +#endif if (recursive) dir_list_read(file_path, list, ext_list, include_dirs, include_hidden, include_compressed, recursive); diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index c847a475cafe..d5033438a550 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -247,6 +247,7 @@ static int (funcname)(const char *path, const char *label, unsigned type, size_t #ifdef HAVE_LAKKA static char *lakka_get_project(void) { +#ifndef HAVE_LAKKA_CANARY size_t len; static char lakka_project[128]; FILE *command_file = popen("cat /etc/release | cut -d - -f 1", "r"); @@ -259,6 +260,9 @@ static char *lakka_get_project(void) pclose(command_file); return lakka_project; +#else + return "/"; +#endif } #endif #endif diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 40d8eba0722d..6a534fbd7ae4 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -269,6 +269,9 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_target_lang, MENU_ENUM_S DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_source_lang, MENU_ENUM_SUBLABEL_AI_SERVICE_SOURCE_LANG) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_url, MENU_ENUM_SUBLABEL_AI_SERVICE_URL) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_enable, MENU_ENUM_SUBLABEL_AI_SERVICE_ENABLE) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_poll_delay, MENU_ENUM_SUBLABEL_AI_SERVICE_POLL_DELAY) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_text_position, MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_POSITION) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ai_service_text_padding, MENU_ENUM_SUBLABEL_AI_SERVICE_TEXT_PADDING) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_power_management_settings_list, MENU_ENUM_SUBLABEL_POWER_MANAGEMENT_SETTINGS) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_privacy_settings_list, MENU_ENUM_SUBLABEL_PRIVACY_SETTINGS) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_midi_settings_list, MENU_ENUM_SUBLABEL_MIDI_SETTINGS) @@ -1006,7 +1009,6 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_disk_tray_eject, DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_disk_tray_insert, MENU_ENUM_SUBLABEL_DISK_TRAY_INSERT) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_disk_index, MENU_ENUM_SUBLABEL_DISK_INDEX) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_disk_options, MENU_ENUM_SUBLABEL_DISK_OPTIONS) -DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_menu_throttle_framerate, MENU_ENUM_SUBLABEL_MENU_ENUM_THROTTLE_FRAMERATE) #ifdef HAVE_XMB DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_xmb_layout, MENU_ENUM_SUBLABEL_XMB_LAYOUT) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_xmb_icon_theme, MENU_ENUM_SUBLABEL_XMB_THEME) @@ -1229,6 +1231,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_gamemode_enable, MENU #endif #endif /*HAVE_LAKKA*/ +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_video_frame_rest, MENU_ENUM_SUBLABEL_VIDEO_FRAME_REST) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_brightness_control, MENU_ENUM_SUBLABEL_BRIGHTNESS_CONTROL) #ifdef _3DS @@ -3147,9 +3150,6 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_xmb_switch_icons); #endif break; - case MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE: - BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_throttle_framerate); - break; case MENU_ENUM_LABEL_DISK_IMAGE_APPEND: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_disk_image_append); break; @@ -5001,6 +5001,15 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_AI_SERVICE_ENABLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_ai_service_enable); break; + case MENU_ENUM_LABEL_AI_SERVICE_POLL_DELAY: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_ai_service_poll_delay); + break; + case MENU_ENUM_LABEL_AI_SERVICE_TEXT_POSITION: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_ai_service_text_position); + break; + case MENU_ENUM_LABEL_AI_SERVICE_TEXT_PADDING: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_ai_service_text_padding); + break; case MENU_ENUM_LABEL_AI_SERVICE_SETTINGS: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_ai_service_settings_list); break; @@ -5213,6 +5222,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_gamemode_enable); break; #endif /*HAVE_LAKKA*/ + case MENU_ENUM_LABEL_VIDEO_FRAME_REST: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_video_frame_rest); + break; case MENU_ENUM_LABEL_BRIGHTNESS_CONTROL: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_brightness_control); break; diff --git a/menu/drivers/ozone.c b/menu/drivers/ozone.c index b0fdae8c1d85..4475516fe4e0 100644 --- a/menu/drivers/ozone.c +++ b/menu/drivers/ozone.c @@ -373,9 +373,7 @@ typedef struct ozone_theme float *sidebar_top_gradient; float *sidebar_bottom_gradient; - /* - Fancy cursor colors - */ + /* Fancy cursor colors */ float *cursor_border_0; float *cursor_border_1; @@ -1982,7 +1980,7 @@ static uintptr_t ozone_entries_icon_get_texture( case MENU_ENUM_LABEL_LATENCY_SETTINGS: case MENU_ENUM_LABEL_CONTENT_SHOW_LATENCY: case MENU_ENUM_LABEL_SETTINGS_SHOW_LATENCY: - case MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE: + case MENU_ENUM_LABEL_VIDEO_FRAME_REST: return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_LATENCY]; case MENU_ENUM_LABEL_SAVING_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_SAVING: @@ -2005,6 +2003,7 @@ static uintptr_t ozone_entries_icon_get_texture( case MENU_ENUM_LABEL_FASTFORWARD_RATIO: case MENU_ENUM_LABEL_FRAME_THROTTLE_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_FRAME_THROTTLE: + case MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP: return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_FRAMESKIP]; case MENU_ENUM_LABEL_QUICK_MENU_START_RECORDING: case MENU_ENUM_LABEL_QUICK_MENU_SHOW_START_RECORDING: @@ -2061,7 +2060,6 @@ static uintptr_t ozone_entries_icon_get_texture( #endif case MENU_ENUM_LABEL_POWER_MANAGEMENT_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_POWER_MANAGEMENT: - case MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP: return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_POWER]; case MENU_ENUM_LABEL_RETRO_ACHIEVEMENTS_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_ACHIEVEMENTS: @@ -3366,8 +3364,8 @@ static void ozone_draw_sidebar( for (i = 0; i < (unsigned)(ozone->system_tab_end + 1); i++) { float *col = NULL; - bool selected = (ozone->categories_selection_ptr == i); - unsigned icon = ozone_system_tabs_icons[ozone->tabs[i]]; + bool selected = (ozone->categories_selection_ptr == i); + unsigned icon = ozone_system_tabs_icons[ozone->tabs[i]]; if (!(col = selected ? ozone->theme->text_selected : ozone->theme->entries_icon)) col = ozone->pure_white; @@ -3768,8 +3766,6 @@ static void ozone_entries_update_thumbnail_bar( { if (allow_animation) { - ozone->show_thumbnail_bar = true; - entry.cb = NULL; entry.userdata = NULL; entry.target_value = ozone->dimensions.thumbnail_bar_width; @@ -3780,9 +3776,9 @@ static void ozone_entries_update_thumbnail_bar( { ozone->animations.thumbnail_bar_position = ozone->dimensions.thumbnail_bar_width; - ozone->show_thumbnail_bar = true; } + ozone->show_thumbnail_bar = true; ozone->flags &= ~OZONE_FLAG_PENDING_HIDE_THUMBNAIL_BAR; /* Want thumbnails to load instantly when thumbnail @@ -7164,20 +7160,6 @@ static void ozone_draw_fullscreen_thumbnails( && (ozone->flags2 & OZONE_FLAG2_SHOW_FULLSCREEN_THUMBNAILS)) goto error; - /* Safety check: ensure that current - * selection matches the entry selected when - * fullscreen thumbnails were enabled - * > Note that we exclude this check if we are - * currently viewing the quick menu and the - * thumbnail view is fading out. This enables - * a smooth transition if the user presses - * RetroPad A or keyboard 'return' to enter the - * quick menu while fullscreen thumbnails are - * being displayed */ - if (((size_t)ozone->selection != ozone->fullscreen_thumbnail_selection) && - ((!(ozone->is_quick_menu)) || (ozone->flags2 & OZONE_FLAG2_SHOW_FULLSCREEN_THUMBNAILS))) - goto error; - /* Sanity check: Return immediately if the view * width/height is < 1 */ if ((view_width < 1) || (view_height < 1)) @@ -7202,11 +7184,10 @@ static void ozone_draw_fullscreen_thumbnails( num_thumbnails++; /* Prevent screen flashing when browsing in fullscreen thumbnail mode */ - if ( (ozone->flags & OZONE_FLAG_IS_PLAYLIST) - && (num_thumbnails < 1) + if ( (num_thumbnails < 1) && (ozone->flags2 & OZONE_FLAG2_WANT_FULLSCREEN_THUMBNAILS) - && ( (right_thumbnail->status != GFX_THUMBNAIL_STATUS_MISSING) - && (left_thumbnail->status != GFX_THUMBNAIL_STATUS_MISSING))) + && ( (right_thumbnail->status != GFX_THUMBNAIL_STATUS_MISSING) + && (left_thumbnail->status != GFX_THUMBNAIL_STATUS_MISSING))) { /* Darken background */ gfx_display_draw_quad( @@ -7505,7 +7486,8 @@ static void ozone_set_thumbnail_content(void *data, const char *s) case OZONE_SYSTEM_TAB_VIDEO: #endif case OZONE_SYSTEM_TAB_MUSIC: - ozone->flags &= ~OZONE_FLAG_WANT_THUMBNAIL_BAR; + if (ozone->categories_selection_ptr <= ozone->system_tab_end) + ozone->flags &= ~OZONE_FLAG_WANT_THUMBNAIL_BAR; break; default: @@ -8327,6 +8309,7 @@ static enum menu_action ozone_parse_menu_entry_action( { ozone->pending_cursor_in_sidebar = false; ozone->flags |= OZONE_FLAG_CURSOR_IN_SIDEBAR; + ozone_sidebar_goto(ozone, ozone->categories_selection_ptr); } break; @@ -9766,10 +9749,10 @@ static void ozone_render(void *data, width, height, false, false); thumbnail_scale_factor = settings->floats.ozone_thumbnail_scale_factor; - if ((scale_factor != ozone->last_scale_factor) || - (thumbnail_scale_factor != ozone->last_thumbnail_scale_factor) || - (width != ozone->last_width) || - (height != ozone->last_height)) + if ( (scale_factor != ozone->last_scale_factor) + || (thumbnail_scale_factor != ozone->last_thumbnail_scale_factor) + || (width != ozone->last_width) + || (height != ozone->last_height)) { ozone->last_scale_factor = scale_factor; ozone->last_thumbnail_scale_factor = thumbnail_scale_factor; @@ -11244,7 +11227,8 @@ static void ozone_selection_changed(ozone_handle_t *ozone, bool allow_animation) gfx_thumbnail_set_content(menu_st->thumbnail_path_data, NULL); ozone_unload_thumbnail_textures(ozone); update_thumbnails = true; - ozone->flags &= ~OZONE_FLAG_WANT_THUMBNAIL_BAR; + ozone->flags &= ~(OZONE_FLAG_WANT_THUMBNAIL_BAR + | OZONE_FLAG_FULLSCREEN_THUMBNAILS_AVAILABLE); } } @@ -11355,15 +11339,10 @@ static void ozone_frame(void *data, video_frame_info_t *video_info) } } - if ( ((ozone->flags & OZONE_FLAG_IS_PLAYLIST) || (ozone->flags & OZONE_FLAG_IS_STATE_SLOT)) - && (ozone->flags2 & OZONE_FLAG2_SHOW_FULLSCREEN_THUMBNAILS) - && (size_t)ozone->selection != ozone->fullscreen_thumbnail_selection) - { - ozone->flags |= OZONE_FLAG_NEED_COMPUTE; - ozone_show_fullscreen_thumbnails(ozone); - } - else if ( (!(ozone->flags2 & OZONE_FLAG2_SHOW_FULLSCREEN_THUMBNAILS)) - && (ozone->flags2 & OZONE_FLAG2_WANT_FULLSCREEN_THUMBNAILS)) + if ( ( (size_t)ozone->selection != ozone->fullscreen_thumbnail_selection + && (ozone->flags2 & OZONE_FLAG2_SHOW_FULLSCREEN_THUMBNAILS)) + || ( (ozone->flags2 & OZONE_FLAG2_WANT_FULLSCREEN_THUMBNAILS) + && !(ozone->flags2 & OZONE_FLAG2_SHOW_FULLSCREEN_THUMBNAILS))) { ozone->flags |= OZONE_FLAG_NEED_COMPUTE; ozone_show_fullscreen_thumbnails(ozone); @@ -12063,8 +12042,9 @@ static void ozone_populate_entries( case OZONE_SYSTEM_TAB_VIDEO: #endif case OZONE_SYSTEM_TAB_MUSIC: - ozone->flags &= ~(OZONE_FLAG_WANT_THUMBNAIL_BAR - | OZONE_FLAG_FULLSCREEN_THUMBNAILS_AVAILABLE); + if (ozone->categories_selection_ptr <= ozone->system_tab_end) + ozone->flags &= ~(OZONE_FLAG_WANT_THUMBNAIL_BAR + | OZONE_FLAG_FULLSCREEN_THUMBNAILS_AVAILABLE); break; } diff --git a/menu/drivers/rgui.c b/menu/drivers/rgui.c index d1c2ef973616..54827ff77d51 100644 --- a/menu/drivers/rgui.c +++ b/menu/drivers/rgui.c @@ -1522,6 +1522,7 @@ static bool rgui_fonts_init(rgui_t *rgui) } case RETRO_LANGUAGE_RUSSIAN: + case RETRO_LANGUAGE_BELARUSIAN: { rgui->fonts.eng_10x10 = bitmapfont_10x10_load(RETRO_LANGUAGE_ENGLISH); rgui->fonts.rus_10x10 = bitmapfont_10x10_load(RETRO_LANGUAGE_RUSSIAN); @@ -4343,6 +4344,7 @@ static void rgui_set_blit_functions( rgui_blit_line = rgui_blit_line_cjk_shadow; break; case RETRO_LANGUAGE_RUSSIAN: + case RETRO_LANGUAGE_BELARUSIAN: rgui_blit_line = rgui_blit_line_rus_shadow; break; case RETRO_LANGUAGE_ESPERANTO: @@ -4381,6 +4383,7 @@ static void rgui_set_blit_functions( rgui_blit_line = rgui_blit_line_cjk; break; case RETRO_LANGUAGE_RUSSIAN: + case RETRO_LANGUAGE_BELARUSIAN: rgui_blit_line = rgui_blit_line_rus; break; case RETRO_LANGUAGE_ESPERANTO: diff --git a/menu/drivers/xmb.c b/menu/drivers/xmb.c index 3f642d05da10..234f8c69d589 100644 --- a/menu/drivers/xmb.c +++ b/menu/drivers/xmb.c @@ -3182,7 +3182,7 @@ static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb, case MENU_ENUM_LABEL_LATENCY_SETTINGS: case MENU_ENUM_LABEL_CONTENT_SHOW_LATENCY: case MENU_ENUM_LABEL_SETTINGS_SHOW_LATENCY: - case MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE: + case MENU_ENUM_LABEL_VIDEO_FRAME_REST: return xmb->textures.list[XMB_TEXTURE_LATENCY]; case MENU_ENUM_LABEL_SAVING_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_SAVING: @@ -3205,6 +3205,7 @@ static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb, case MENU_ENUM_LABEL_FASTFORWARD_RATIO: case MENU_ENUM_LABEL_FRAME_THROTTLE_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_FRAME_THROTTLE: + case MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP: return xmb->textures.list[XMB_TEXTURE_FRAMESKIP]; case MENU_ENUM_LABEL_QUICK_MENU_START_RECORDING: case MENU_ENUM_LABEL_QUICK_MENU_SHOW_START_RECORDING: @@ -3261,7 +3262,6 @@ static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb, #endif case MENU_ENUM_LABEL_POWER_MANAGEMENT_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_POWER_MANAGEMENT: - case MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP: return xmb->textures.list[XMB_TEXTURE_POWER]; case MENU_ENUM_LABEL_RETRO_ACHIEVEMENTS_SETTINGS: case MENU_ENUM_LABEL_SETTINGS_SHOW_ACHIEVEMENTS: diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 094616d06e6f..01a371a5ae63 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -4152,7 +4152,7 @@ static unsigned menu_displaylist_parse_playlists( content_count = count; - dir_list_sort(&str_list, true); + dir_list_sort_ignore_ext(&str_list, true); list_size = str_list.size; @@ -5938,12 +5938,14 @@ void menu_displaylist_info_init(menu_displaylist_info_t *info) info->setting = NULL; } -typedef struct menu_displaylist_build_info { +typedef struct menu_displaylist_build_info +{ enum msg_hash_enums enum_idx; enum menu_displaylist_parse_type parse_type; } menu_displaylist_build_info_t; -typedef struct menu_displaylist_build_info_selective { +typedef struct menu_displaylist_build_info_selective +{ enum msg_hash_enums enum_idx; enum menu_displaylist_parse_type parse_type; bool checked; @@ -6683,7 +6685,8 @@ unsigned menu_displaylist_build_list( bool playlist_show_sublabels = settings->bools.playlist_show_sublabels; bool history_list_enable = settings->bools.history_list_enable; bool truncate_playlist = settings->bools.ozone_truncate_playlist_name; - menu_displaylist_build_info_selective_t build_list[] = { + menu_displaylist_build_info_selective_t build_list[] = + { {MENU_ENUM_LABEL_HISTORY_LIST_ENABLE, PARSE_ONLY_BOOL, true}, {MENU_ENUM_LABEL_CONTENT_HISTORY_SIZE, PARSE_ONLY_UINT, false}, {MENU_ENUM_LABEL_CONTENT_FAVORITES_SIZE, PARSE_ONLY_INT, true}, @@ -7736,12 +7739,15 @@ unsigned menu_displaylist_build_list( bool ai_service_enable = settings->bools.ai_service_enable; menu_displaylist_build_info_selective_t build_list[] = { - {MENU_ENUM_LABEL_AI_SERVICE_ENABLE, PARSE_ONLY_BOOL, true}, - {MENU_ENUM_LABEL_AI_SERVICE_MODE, PARSE_ONLY_UINT, false}, - {MENU_ENUM_LABEL_AI_SERVICE_URL, PARSE_ONLY_STRING, false}, - {MENU_ENUM_LABEL_AI_SERVICE_PAUSE, PARSE_ONLY_BOOL, false}, - {MENU_ENUM_LABEL_AI_SERVICE_SOURCE_LANG, PARSE_ONLY_UINT, false}, - {MENU_ENUM_LABEL_AI_SERVICE_TARGET_LANG, PARSE_ONLY_UINT, false}, + {MENU_ENUM_LABEL_AI_SERVICE_ENABLE, PARSE_ONLY_BOOL, true}, + {MENU_ENUM_LABEL_AI_SERVICE_MODE, PARSE_ONLY_UINT, false}, + {MENU_ENUM_LABEL_AI_SERVICE_URL, PARSE_ONLY_STRING, false}, + {MENU_ENUM_LABEL_AI_SERVICE_PAUSE, PARSE_ONLY_BOOL, false}, + {MENU_ENUM_LABEL_AI_SERVICE_POLL_DELAY, PARSE_ONLY_UINT, false}, + {MENU_ENUM_LABEL_AI_SERVICE_SOURCE_LANG, PARSE_ONLY_UINT, false}, + {MENU_ENUM_LABEL_AI_SERVICE_TARGET_LANG, PARSE_ONLY_UINT, false}, + {MENU_ENUM_LABEL_AI_SERVICE_TEXT_POSITION, PARSE_ONLY_UINT, false}, + {MENU_ENUM_LABEL_AI_SERVICE_TEXT_PADDING, PARSE_ONLY_UINT, false}, }; for (i = 0; i < ARRAY_SIZE(build_list); i++) @@ -7751,8 +7757,11 @@ unsigned menu_displaylist_build_list( case MENU_ENUM_LABEL_AI_SERVICE_MODE: case MENU_ENUM_LABEL_AI_SERVICE_URL: case MENU_ENUM_LABEL_AI_SERVICE_PAUSE: + case MENU_ENUM_LABEL_AI_SERVICE_POLL_DELAY: case MENU_ENUM_LABEL_AI_SERVICE_SOURCE_LANG: case MENU_ENUM_LABEL_AI_SERVICE_TARGET_LANG: + case MENU_ENUM_LABEL_AI_SERVICE_TEXT_POSITION: + case MENU_ENUM_LABEL_AI_SERVICE_TEXT_PADDING: if (ai_service_enable) build_list[i].checked = true; break; @@ -10425,6 +10434,8 @@ unsigned menu_displaylist_build_list( case DISPLAYLIST_POWER_MANAGEMENT_SETTINGS_LIST: { menu_displaylist_build_info_t build_list[] = { + {MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP, PARSE_ONLY_BOOL}, + {MENU_ENUM_LABEL_VIDEO_FRAME_REST, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_SUSTAINED_PERFORMANCE_MODE, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_CPU_PERFPOWER, PARSE_ACTION}, #ifdef HAVE_LAKKA @@ -10906,7 +10917,7 @@ unsigned menu_displaylist_build_list( {MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP, PARSE_ONLY_BOOL, true}, {MENU_ENUM_LABEL_SLOWMOTION_RATIO, PARSE_ONLY_FLOAT, true}, {MENU_ENUM_LABEL_VRR_RUNLOOP_ENABLE, PARSE_ONLY_BOOL, true}, - {MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE, PARSE_ONLY_BOOL , true}, + {MENU_ENUM_LABEL_VIDEO_FRAME_REST, PARSE_ONLY_BOOL, true}, }; #ifdef HAVE_REWIND @@ -11911,7 +11922,7 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type, else snprintf(mode_str + strlen_mode_str, sizeof(mode_str) - strlen_mode_str, - "Mode %d", toc->track[i].mode); + "Mode %d", toc->track[i].mode%10); if (menu_entries_append(info->list, mode_str, diff --git a/menu/menu_driver.c b/menu/menu_driver.c index 5711bae972ac..d816b65c5341 100644 --- a/menu/menu_driver.c +++ b/menu/menu_driver.c @@ -5239,11 +5239,6 @@ unsigned menu_event( ok_old = ok_current; - /* Menu must be alive, and input must be released after menu toggle. */ - if ( !(menu_st->flags & MENU_ST_FLAG_ALIVE) - || menu_st->input_driver_flushing_input > 0) - return ret; - /* Get pointer (mouse + touchscreen) input * Note: Must be done regardless of menu screensaver * state */ @@ -5633,6 +5628,11 @@ unsigned menu_event( menu_st->input_last_time_us = menu_st->current_time_us; } + /* Menu must be alive, and input must be released after menu toggle. */ + if ( !(menu_st->flags & MENU_ST_FLAG_ALIVE) + || menu_st->input_driver_flushing_input > 0) + return MENU_ACTION_NOOP; + return ret; } @@ -6266,7 +6266,7 @@ void menu_driver_toggle( runloop_state_t *runloop_st = runloop_state_get_ptr(); struct menu_state *menu_st = &menu_driver_state; bool runloop_shutdown_initiated = (runloop_st->flags & - RUNLOOP_FLAG_SHUTDOWN_INITIATED) ? true : false; + RUNLOOP_FLAG_SHUTDOWN_INITIATED) ? true : false; #ifdef HAVE_OVERLAY bool input_overlay_hide_in_menu = false; bool input_overlay_enable = false; @@ -6277,7 +6277,7 @@ void menu_driver_toggle( { #ifdef HAVE_NETWORKING pause_libretro = settings->bools.menu_pause_libretro && - netplay_driver_ctl(RARCH_NETPLAY_CTL_ALLOW_PAUSE, NULL); + netplay_driver_ctl(RARCH_NETPLAY_CTL_ALLOW_PAUSE, NULL); #else pause_libretro = settings->bools.menu_pause_libretro; #endif @@ -6285,7 +6285,6 @@ void menu_driver_toggle( input_overlay_hide_in_menu = settings->bools.input_overlay_hide_in_menu; input_overlay_enable = settings->bools.input_overlay_enable; #endif - video_adaptive_vsync = settings->bools.video_adaptive_vsync; } if (on) @@ -6329,23 +6328,26 @@ void menu_driver_toggle( IMEnableDim(); #endif - menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH; - - /* Menu should always run with vsync on and - * a video swap interval of 1 */ - if (current_video->set_nonblock_state) - current_video->set_nonblock_state( - video_driver_data, - false, - video_driver_test_all_flags(GFX_CTX_FLAGS_ADAPTIVE_VSYNC) && - video_adaptive_vsync, - 1 - ); + menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH; + + /* Always disable FF & SM when entering menu. */ + runloop_st->flags &= ~RUNLOOP_FLAG_FASTMOTION; + runloop_st->flags &= ~RUNLOOP_FLAG_SLOWMOTION; +#if defined(HAVE_GFX_WIDGETS) + video_state_get_ptr()->flags &= ~VIDEO_FLAG_WIDGETS_FAST_FORWARD; + video_state_get_ptr()->flags &= ~VIDEO_FLAG_WIDGETS_REWINDING; +#endif + input_state_get_ptr()->flags &= ~INP_FLAG_NONBLOCKING; + driver_set_nonblock_state(); + /* Stop all rumbling before entering the menu. */ command_event(CMD_EVENT_RUMBLE_STOP, NULL); if (pause_libretro) { +#ifdef PS2 + command_event(CMD_EVENT_AUDIO_STOP, NULL); +#endif #ifdef HAVE_MICROPHONE command_event(CMD_EVENT_MICROPHONE_STOP, NULL); #endif @@ -6376,6 +6378,9 @@ void menu_driver_toggle( if (pause_libretro) { +#ifdef PS2 + command_event(CMD_EVENT_AUDIO_START, NULL); +#endif #ifdef HAVE_MICROPHONE command_event(CMD_EVENT_MICROPHONE_START, NULL); #endif diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 9a4d14023d9f..0b9cac7fef0f 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -3014,6 +3014,42 @@ static void setting_get_string_representation_uint_ai_service_mode( case 2: enum_idx = MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE; break; + case 3: + enum_idx = MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE; + break; + case 4: + enum_idx = MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE; + break; + case 5: + enum_idx = MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE; + break; + default: + break; + } + + if (enum_idx != 0) + strlcpy(s, msg_hash_to_str(enum_idx), len); +} + +static void setting_get_string_representation_uint_ai_service_text_position( + rarch_setting_t *setting, + char *s, size_t len) +{ + enum msg_hash_enums enum_idx = MSG_UNKNOWN; + if (!setting) + return; + + switch (*setting->value.target.unsigned_integer) + { + case 0: + enum_idx = MENU_ENUM_LABEL_VALUE_NONE; + break; + case 1: + enum_idx = MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM; + break; + case 2: + enum_idx = MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP; + break; default: break; } @@ -3183,6 +3219,9 @@ static void setting_get_string_representation_uint_ai_service_lang( case TRANSLATION_LANG_UK: enum_idx = MENU_ENUM_LABEL_VALUE_LANG_UKRAINIAN; break; + case TRANSLATION_LANG_BE: + enum_idx = MENU_ENUM_LABEL_VALUE_LANG_BELARUSIAN; + break; case TRANSLATION_LANG_UR: enum_idx = MENU_ENUM_LABEL_VALUE_LANG_URDU; break; @@ -6888,6 +6927,7 @@ static void setting_get_string_representation_uint_user_language( translated[RETRO_LANGUAGE_BRITISH_ENGLISH] = LANGUAGE_PROGRESS_ENGLISH_UNITED_KINGDOM_TRANSLATED; LANG_DATA(HUNGARIAN) + LANG_DATA(BELARUSIAN) if (*msg_hash_get_uint(MSG_HASH_USER_LANGUAGE) == RETRO_LANGUAGE_ENGLISH) strlcpy(s, modes[*msg_hash_get_uint(MSG_HASH_USER_LANGUAGE)], len); @@ -13346,6 +13386,8 @@ static bool setting_append_list( general_read_handler); (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; menu_settings_list_current_add_range(list, list_info, 0, 5, 1, true, true); + MENU_SETTINGS_LIST_CURRENT_ADD_CMD(list, list_info, CMD_EVENT_REINIT); + SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_CMD_APPLY_AUTO); } } #endif @@ -15594,22 +15636,6 @@ static bool setting_append_list( menu_settings_list_current_add_range(list, list_info, 0, 4, 1, true, true); #endif - CONFIG_BOOL( - list, list_info, - &settings->bools.menu_throttle_framerate, - MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE, - MENU_ENUM_LABEL_VALUE_MENU_ENUM_THROTTLE_FRAMERATE, - true, - MENU_ENUM_LABEL_VALUE_OFF, - MENU_ENUM_LABEL_VALUE_ON, - &group_info, - &subgroup_info, - parent_group, - general_write_handler, - general_read_handler, - SD_FLAG_ADVANCED - ); - END_SUB_GROUP(list, list_info, parent_group); END_GROUP(list, list_info, parent_group); break; @@ -19116,6 +19142,21 @@ static bool setting_append_list( general_read_handler, SD_FLAG_NONE); + CONFIG_BOOL( + list, list_info, + &settings->bools.video_frame_rest, + MENU_ENUM_LABEL_VIDEO_FRAME_REST, + MENU_ENUM_LABEL_VALUE_VIDEO_FRAME_REST, + DEFAULT_FRAME_REST, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE); + END_SUB_GROUP(list, list_info, parent_group); END_GROUP(list, list_info, parent_group); break; @@ -19232,7 +19273,7 @@ static bool setting_append_list( (*list)[list_info->index - 1].get_string_representation = &setting_get_string_representation_uint_ai_service_mode; (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; - menu_settings_list_current_add_range(list, list_info, 0, 2, 1, true, true); + menu_settings_list_current_add_range(list, list_info, 0, 5, 1, true, true); CONFIG_STRING( list, list_info, @@ -19314,6 +19355,50 @@ static bool setting_append_list( &setting_get_string_representation_uint_ai_service_lang; (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; menu_settings_list_current_add_range(list, list_info, TRANSLATION_LANG_DONT_CARE, (TRANSLATION_LANG_LAST-1), 1, true, true); + + CONFIG_UINT( + list, list_info, + &settings->uints.ai_service_poll_delay, + MENU_ENUM_LABEL_AI_SERVICE_POLL_DELAY, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_POLL_DELAY, + DEFAULT_AI_SERVICE_POLL_DELAY, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler); + (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; + menu_settings_list_current_add_range(list, list_info, 0, MAXIMUM_AI_SERVICE_POLL_DELAY, 50, true, true); + + CONFIG_UINT( + list, list_info, + &settings->uints.ai_service_text_position, + MENU_ENUM_LABEL_AI_SERVICE_TEXT_POSITION, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION, + DEFAULT_AI_SERVICE_TEXT_POSITION, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler); + (*list)[list_info->index - 1].get_string_representation = + &setting_get_string_representation_uint_ai_service_text_position; + (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; + menu_settings_list_current_add_range(list, list_info, 0, 2, 1, true, true); + + CONFIG_UINT( + list, list_info, + &settings->uints.ai_service_text_padding, + MENU_ENUM_LABEL_AI_SERVICE_TEXT_PADDING, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_PADDING, + DEFAULT_AI_SERVICE_TEXT_PADDING, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler); + (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; + menu_settings_list_current_add_range(list, list_info, 0, 20, 1, true, true); END_SUB_GROUP(list, list_info, parent_group); END_GROUP(list, list_info, parent_group); diff --git a/msg_hash.c b/msg_hash.c index 8c278068e8bb..67496f6e2616 100644 --- a/msg_hash.c +++ b/msg_hash.c @@ -128,6 +128,8 @@ const char *get_user_language_iso639_1(bool limit) return "en_gb"; case RETRO_LANGUAGE_HUNGARIAN: return "hu"; + case RETRO_LANGUAGE_BELARUSIAN: + return "be"; } return "en"; } @@ -217,6 +219,18 @@ static const char *msg_hash_to_str_hu(enum msg_hash_enums msg) return "null"; } +static const char *msg_hash_to_str_be(enum msg_hash_enums msg) +{ + switch (msg) + { +#include "intl/msg_hash_be.h" + default: + break; + } + + return "null"; +} + static const char *msg_hash_to_str_en(enum msg_hash_enums msg) { switch (msg) @@ -607,6 +621,9 @@ const char *msg_hash_to_str(enum msg_hash_enums msg) case RETRO_LANGUAGE_HUNGARIAN: ret = msg_hash_to_str_hu(msg); break; + case RETRO_LANGUAGE_BELARUSIAN: + ret = msg_hash_to_str_be(msg); + break; default: break; } diff --git a/msg_hash.h b/msg_hash.h index 99fddc252f00..02a092fd3124 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -569,6 +569,10 @@ enum msg_hash_enums MSG_FAILED_TO_ENTER_GAMEMODE_LINUX, MSG_VRR_RUNLOOP_ENABLED, MSG_VRR_RUNLOOP_DISABLED, + MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED, + MSG_AI_AUTO_MODE_ENABLED, + MSG_AI_AUTO_MODE_DISABLED, + MSG_AI_NOTHING_TO_TRANSLATE, MSG_VIDEO_REFRESH_RATE_CHANGED, MSG_IOS_TOUCH_MOUSE_ENABLED, @@ -1664,7 +1668,6 @@ enum msg_hash_enums MENU_LABEL(ADD_TO_FAVORITES_PLAYLIST), MENU_LABEL(SET_CORE_ASSOCIATION), MENU_LABEL(RESET_CORE_ASSOCIATION), - MENU_LABEL(MENU_THROTTLE_FRAMERATE), MENU_LABEL(NO_ACHIEVEMENTS_TO_DISPLAY), MENU_LABEL(NOT_LOGGED_IN), @@ -1735,7 +1738,7 @@ enum msg_hash_enums MENU_ENUM_LABEL_CONNECT_NETPLAY_LAN, MENU_LABEL(MENU_ENUM_LINEAR_FILTER), - MENU_LABEL(MENU_ENUM_THROTTLE_FRAMERATE), + MENU_LABEL(MENU_ENUM_THROTTLE_FRAMERATE), /* deprecated */ MENU_LABEL(STATE_SLOT), MENU_ENUM_LABEL_PLAYLIST_SETTINGS_BEGIN, @@ -2785,6 +2788,9 @@ enum msg_hash_enums MENU_LABEL(AI_SERVICE_URL), MENU_LABEL(AI_SERVICE_ENABLE), MENU_LABEL(AI_SERVICE_PAUSE), + MENU_LABEL(AI_SERVICE_POLL_DELAY), + MENU_LABEL(AI_SERVICE_TEXT_POSITION), + MENU_LABEL(AI_SERVICE_TEXT_PADDING), MENU_LABEL(ON), MENU_LABEL(OFF), @@ -3464,6 +3470,7 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_LANG_TELUGU, MENU_ENUM_LABEL_VALUE_LANG_THAI, MENU_ENUM_LABEL_VALUE_LANG_UKRAINIAN, + MENU_ENUM_LABEL_VALUE_LANG_BELARUSIAN, MENU_ENUM_LABEL_VALUE_LANG_URDU, MENU_ENUM_LABEL_VALUE_LANG_CATALAN_VALENCIA, MENU_ENUM_LABEL_VALUE_LANG_WELSH, @@ -3477,6 +3484,11 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_MODE, MENU_ENUM_LABEL_VALUE_AI_SERVICE_SPEECH_MODE, MENU_ENUM_LABEL_VALUE_AI_SERVICE_NARRATOR_MODE, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_MODE, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_NARRATOR_MODE, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_IMAGE_NARRATOR_MODE, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_TOP, + MENU_ENUM_LABEL_VALUE_AI_SERVICE_TEXT_POSITION_BOTTOM, MENU_ENUM_LABEL_VALUE_NONE, MENU_ENUM_LABEL_VALUE_NO_INFORMATION_AVAILABLE, @@ -3806,6 +3818,7 @@ enum msg_hash_enums MENU_LABEL(CPU_MANAGED_MAX_FREQ), MENU_LBL_H(GAMEMODE_ENABLE), MENU_ENUM_SUBLABEL_GAMEMODE_ENABLE_LINUX, + MENU_LABEL(VIDEO_FRAME_REST), MENU_ENUM_LABEL_VALUE_CPU_PERF_MODE_MANAGED_PERF, MENU_ENUM_LABEL_VALUE_CPU_PERF_MODE_MANAGED_PER_CONTEXT, diff --git a/network/netplay/netplay_frontend.c b/network/netplay/netplay_frontend.c index 39c2a1a40ae1..054e82536ae0 100644 --- a/network/netplay/netplay_frontend.c +++ b/network/netplay/netplay_frontend.c @@ -1268,15 +1268,19 @@ static bool netplay_handshake_sync(netplay_t *netplay, if (netplay->local_paused || netplay->remote_paused) client_num |= NETPLAY_CMD_SYNC_BIT_PAUSED; - mem_info.id = RETRO_MEMORY_SAVE_RAM; + /* send sram unless running with netpacket interface */ + if (netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) + { + mem_info.id = RETRO_MEMORY_SAVE_RAM; #ifdef HAVE_THREADS - autosave_lock(); + autosave_lock(); #endif - core_get_memory(&mem_info); - sram_size = mem_info.size; + core_get_memory(&mem_info); + sram_size = mem_info.size; #ifdef HAVE_THREADS - autosave_unlock(); + autosave_unlock(); #endif + } /* Send basic sync info */ cmd[0] = htonl(NETPLAY_CMD_SYNC); @@ -1700,7 +1704,7 @@ static bool netplay_handshake_pre_sync(netplay_t *netplay, uint32_t cmd[2]; uint32_t cmd_size; uint32_t new_frame_count, client_num; - uint32_t local_sram_size, remote_sram_size; + uint32_t local_sram_size = 0, remote_sram_size; size_t i, j; ssize_t recvd; char new_nick[NETPLAY_NICK_LEN]; @@ -1825,16 +1829,20 @@ static bool netplay_handshake_pre_sync(netplay_t *netplay, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } - /* Now check the SRAM */ - mem_info.id = RETRO_MEMORY_SAVE_RAM; + /* Now check the SRAM, but ignore it when using netpacket interface */ + if (netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) + { + mem_info.id = RETRO_MEMORY_SAVE_RAM; #ifdef HAVE_THREADS - autosave_lock(); + autosave_lock(); #endif - core_get_memory(&mem_info); - local_sram_size = (unsigned)mem_info.size; + core_get_memory(&mem_info); + local_sram_size = (unsigned)mem_info.size; #ifdef HAVE_THREADS - autosave_unlock(); + autosave_unlock(); #endif + } + remote_sram_size = cmd_size - (2*sizeof(uint32_t) /* Controller devices */ + MAX_INPUT_DEVICES*sizeof(uint32_t) @@ -4161,11 +4169,29 @@ static void netplay_hangup(netplay_t *netplay, uint32_t client_num = (uint32_t) (connection - netplay->connections + 1); - /* This special mode keeps the connection object - alive long enough to send the disconnection - message at the correct time */ - connection->mode = NETPLAY_CONNECTION_DELAYED_DISCONNECT; - connection->delay_frame = netplay->read_frame_count[client_num]; + if (netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) + { + /* This special mode keeps the connection object + alive long enough to send the disconnection + message at the correct time */ + connection->mode = NETPLAY_CONNECTION_DELAYED_DISCONNECT; + connection->delay_frame = netplay->read_frame_count[client_num]; + } + else + { + /* With netpacket interface we can send the mode change now */ + struct mode_payload payload; + payload.frame = htonl(netplay->self_frame_count); + payload.mode = htonl(client_num); + payload.devices = 0; + memcpy(payload.share_modes, netplay->device_share_modes, + sizeof(payload.share_modes)); + memcpy(payload.nick, connection->nick, sizeof(payload.nick)); + netplay_send_raw_cmd_all(netplay, connection, + NETPLAY_CMD_MODE, &payload, sizeof(payload)); + + connection->mode = NETPLAY_CONNECTION_NONE; + } /* Mark them as not playing anymore */ netplay->connected_players &= ~(1L<device_share_modes, @@ -7769,10 +7796,6 @@ static bool netplay_poll(netplay_t *netplay, bool block_libretro_input) /* Read netplay input. */ netplay_poll_net_input(netplay); - /* Handle any delayed state changes */ - if (netplay->is_server) - netplay_delayed_state_change(netplay); - if (networking_driver_st.core_netpacket_interface && networking_driver_st.core_netpacket_interface->poll && netplay->self_mode == NETPLAY_CONNECTION_PLAYING) diff --git a/pkg/android/phoenix-common/jni/Android.mk b/pkg/android/phoenix-common/jni/Android.mk index d52184b32dfe..18300fb97ee1 100644 --- a/pkg/android/phoenix-common/jni/Android.mk +++ b/pkg/android/phoenix-common/jni/Android.mk @@ -132,6 +132,7 @@ DEFINES += -DRARCH_MOBILE \ -DENABLE_HLSL \ -DHAVE_AUDIOMIXER \ -DHAVE_RWAV \ + -DHAVE_ACCESSIBILITY \ -DHAVE_TRANSLATE \ -DWANT_IFADDRS \ -DHAVE_CORE_INFO_CACHE diff --git a/pkg/apple/RetroArchWidgetExtension/RetroArchWidgetExtension.swift b/pkg/apple/RetroArchWidgetExtension/RetroArchWidgetExtension.swift index 09bd53e9a803..903ad02bbc3c 100644 --- a/pkg/apple/RetroArchWidgetExtension/RetroArchWidgetExtension.swift +++ b/pkg/apple/RetroArchWidgetExtension/RetroArchWidgetExtension.swift @@ -34,10 +34,25 @@ struct SimpleEntry: TimelineEntry { struct RetroArchImageView : View { var body: some View { +#if swift(>=5.9) + if #available(iOSApplicationExtension 17.0, *) { + ZStack { + AccessoryWidgetBackground() + Image("logo") + } + .containerBackground(for: .widget) {} + } else { + ZStack { + AccessoryWidgetBackground() + Image("logo") + } + } +#else ZStack { AccessoryWidgetBackground() Image("logo") } +#endif } } diff --git a/pkg/apple/RetroArch_Metal.xcodeproj/project.pbxproj b/pkg/apple/RetroArch_Metal.xcodeproj/project.pbxproj index 815a7947d1b1..9c81b0a17c9e 100644 --- a/pkg/apple/RetroArch_Metal.xcodeproj/project.pbxproj +++ b/pkg/apple/RetroArch_Metal.xcodeproj/project.pbxproj @@ -1931,6 +1931,7 @@ PRODUCT_NAME = RetroArch; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; + STRIP_STYLE = debugging; }; name = Debug; }; @@ -2014,6 +2015,7 @@ PRODUCT_BUNDLE_IDENTIFIER = libretro.RetroArch; PRODUCT_NAME = RetroArch; SDKROOT = macosx; + STRIP_STYLE = debugging; }; name = Release; }; diff --git a/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj b/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj index 41c374ec0372..c762bdf503b2 100644 --- a/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj +++ b/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj @@ -8,8 +8,9 @@ /* Begin PBXBuildFile section */ 070A88432A4E7AA9003161C0 /* OpenAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 070A88422A4E7AA9003161C0 /* OpenAL.framework */; }; - 0714E7142983A5AC00E6B45B /* libMoltenVK.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */; }; 0714E7152983A5E500E6B45B /* libMoltenVK.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 0718BC632ABBAFB6001F2CBE /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0718BC5F2ABBA807001F2CBE /* Network.framework */; }; + 0734BB242ADB7FEE00EBDCAD /* libMoltenVK.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 92EDD1622982E40C00AD33B4 /* libMoltenVK.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 073734A42A093A5700BF7397 /* JITSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 92A1F81727006CAE00DEAD2A /* JITSupport.m */; }; 073734A62A093ACA00BF7397 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = 073734A52A093ACA00BF7397 /* AltKit */; }; 0789FC302A07847E00D042B7 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0789FC2F2A07847E00D042B7 /* AltKit */; }; @@ -110,8 +111,6 @@ 92CC05C721FEDD0B00FF79F0 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92CC05C621FEDD0B00FF79F0 /* MobileCoreServices.framework */; }; 92DAF33F277A370600FE2A9E /* EmulatorTouchMouse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DAF33E277A370600FE2A9E /* EmulatorTouchMouse.swift */; }; 92E5DCD4231A5786006491BF /* modules in Resources */ = {isa = PBXBuildFile; fileRef = 92E5DCD3231A5786006491BF /* modules */; }; - 92EDD1632982E40C00AD33B4 /* libMoltenVK.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 92EDD1622982E40C00AD33B4 /* libMoltenVK.dylib */; }; - 92EDD1642982E40D00AD33B4 /* libMoltenVK.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 92EDD1622982E40C00AD33B4 /* libMoltenVK.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -136,26 +135,26 @@ name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; }; - 9292D6F528F549D500E47A75 /* Embed Foundation Extensions */ = { + 0734BB252ADB7FEE00EBDCAD /* Embed Libraries */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; - dstSubfolderSpec = 13; + dstSubfolderSpec = 10; files = ( - 9292D6F128F549D200E47A75 /* RetroArchWidgetExtensionExtension.appex in Embed Foundation Extensions */, + 0734BB242ADB7FEE00EBDCAD /* libMoltenVK.dylib in Embed Libraries */, ); - name = "Embed Foundation Extensions"; + name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; }; - 92EDD1652982E40D00AD33B4 /* Embed Libraries */ = { + 9292D6F528F549D500E47A75 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; - dstSubfolderSpec = 10; + dstSubfolderSpec = 13; files = ( - 92EDD1642982E40D00AD33B4 /* libMoltenVK.dylib in Embed Libraries */, + 9292D6F128F549D200E47A75 /* RetroArchWidgetExtensionExtension.appex in Embed Foundation Extensions */, ); - name = "Embed Libraries"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -163,6 +162,7 @@ /* Begin PBXFileReference section */ 070A88422A4E7AA9003161C0 /* OpenAL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenAL.framework; path = System/Library/Frameworks/OpenAL.framework; sourceTree = SDKROOT; }; 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libMoltenVK.dylib; path = tvOS/modules/libMoltenVK.dylib; sourceTree = ""; }; + 0718BC5F2ABBA807001F2CBE /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS17.0.sdk/System/Library/Frameworks/Network.framework; sourceTree = DEVELOPER_DIR; }; 0789FC2E2A07845300D042B7 /* AltKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AltKit; path = Frameworks/AltKit; sourceTree = ""; }; 07B7872C29E8FE8F0088B74F /* filters */ = {isa = PBXFileReference; lastKnownFileType = folder; path = filters; sourceTree = ""; }; 07F7FB012A2DA8B800037C04 /* filters */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = filters; path = iOS/filters; sourceTree = SOURCE_ROOT; }; @@ -462,7 +462,6 @@ 92CC05C521FEDC9F00FF79F0 /* CFNetwork.framework in Frameworks */, 9204BE121D319EF300BD49DB /* libz.dylib in Frameworks */, 9204BE131D319EF300BD49DB /* QuartzCore.framework in Frameworks */, - 92EDD1632982E40C00AD33B4 /* libMoltenVK.dylib in Frameworks */, 9204BE141D319EF300BD49DB /* GameController.framework in Frameworks */, 9204BE151D319EF300BD49DB /* CoreText.framework in Frameworks */, 9204BE161D319EF300BD49DB /* CoreLocation.framework in Frameworks */, @@ -485,10 +484,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0714E7142983A5AC00E6B45B /* libMoltenVK.dylib in Frameworks */, 926C77F121FD26E800103EDE /* GameController.framework in Frameworks */, 926C77EF21FD263800103EDE /* AudioToolbox.framework in Frameworks */, 073734A62A093ACA00BF7397 /* AltKit in Frameworks */, + 0718BC632ABBAFB6001F2CBE /* Network.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1183,6 +1182,7 @@ 96AFAE2816C1D4EA009DE44C /* Frameworks */ = { isa = PBXGroup; children = ( + 0718BC5F2ABBA807001F2CBE /* Network.framework */, 070A88422A4E7AA9003161C0 /* OpenAL.framework */, 92EDD1622982E40C00AD33B4 /* libMoltenVK.dylib */, 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */, @@ -1254,7 +1254,7 @@ 9204BE271D319EF300BD49DB /* ShellScript */, 9204BE211D319EF300BD49DB /* Resources */, 9292D6F528F549D500E47A75 /* Embed Foundation Extensions */, - 92EDD1652982E40D00AD33B4 /* Embed Libraries */, + 0734BB252ADB7FEE00EBDCAD /* Embed Libraries */, ); buildRules = ( ); @@ -1572,105 +1572,30 @@ INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_NO_PIE = YES; + LD_NO_PIE = NO; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", + "@executable_path/modules", ); LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/iOS/modules"; MARKETING_VERSION = 1.16.0; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( - "-DDONT_WANT_ARM_OPTIMIZATIONS", - "-DENABLE_HLSL", - "-DGLES_SILENCE_DEPRECATION", - "-DGLSLANG_OSINCLUDE_UNIX", - "-DHAVE_7ZIP", - "-DHAVE_AL", - "-DHAVE_ALTKIT", - "-DHAVE_AUDIOMIXER", - "-DHAVE_BTSTACK", - "-DHAVE_BUILTINGLSLANG", - "-DHAVE_BUILTINMBEDTLS", - "-DHAVE_BUILTINMINIUPNPC", - "-DHAVE_CC_RESAMPLER", - "-DHAVE_CHD", - "-DHAVE_CHEATS", - "-DHAVE_CHEEVOS", - "-DHAVE_CLOUDSYNC", - "-DHAVE_COCOATOUCH", - "-DHAVE_COCOA_METAL", - "-DHAVE_CONFIGFILE", - "-DHAVE_COREAUDIO", "-DHAVE_COREMOTION", - "-DHAVE_DSP_FILTER", - "-DHAVE_DYNAMIC", - "-DHAVE_FILTERS_BUILTIN", - "-DHAVE_GFX_WIDGETS", - "-DHAVE_GLSL", - "-DHAVE_GLSLANG", - "-DHAVE_GRIFFIN", - "-DHAVE_HID", - "-DHAVE_IFINFO", - "-DHAVE_IMAGEVIEWER", "-DHAVE_IOS_CUSTOMKEYBOARD", "-DHAVE_IOS_SWIFT", "-DHAVE_IOS_TOUCHMOUSE", - "-DHAVE_KEYMAPPER", - "-DHAVE_LANGEXTRA", - "-DHAVE_LIBRETRODB", - "-DHAVE_MAIN", "-DHAVE_MATERIALUI", - "-DHAVE_MENU", - "-DHAVE_METAL", - "-DHAVE_MFI", - "-DHAVE_MINIUPNPC", - "-DHAVE_NETPLAYDISCOVERY", - "-DHAVE_NETPLAYDISCOVERY_NSNET", - "-DHAVE_NETWORKGAMEPAD", - "-DHAVE_NETWORKING", - "-DHAVE_ONLINE_UPDATER", - "-DHAVE_OPENGL", - "-DHAVE_OPENGLES", - "-DHAVE_OPENGLES2", "-DHAVE_OVERLAY", - "-DHAVE_OZONE", - "-DHAVE_PATCH", - "-DHAVE_RBMP", - "-DHAVE_REWIND", - "-DHAVE_RGUI", - "-DHAVE_RJPEG", - "-DHAVE_RPNG", - "-DHAVE_RTGA", - "-DHAVE_BSV_MOVIE", - "-DHAVE_RUNAHEAD", - "-DHAVE_RWAV", - "-DHAVE_SCREENSHOTS", - "-DHAVE_SHADERPIPELINE", - "-DHAVE_SLANG", - "-DHAVE_SPIRV_CROSS", - "-DHAVE_SSL", - "-DHAVE_STB_FONT", - "-DHAVE_STB_VORBIS", - "-DHAVE_THREADS", - "-DHAVE_TRANSLATE", - "-DHAVE_UPDATE_ASSETS", - "-DHAVE_UPDATE_CORE_INFO", - "-DHAVE_VIDEO_FILTER", + "$(inherited)", + ); + "OTHER_CFLAGS[sdk=iphoneos*]" = ( "-DHAVE_VULKAN", - "-DHAVE_XMB", - "-DHAVE_ZLIB", - "-DINLINE=inline", - "-DIOS", - "-DRARCH_INTERNAL", - "-DRARCH_MOBILE", - "-DRC_DISABLE_LUA", - "-DWANT_GLSLANG", - "-D_7ZIP_ST", - "-D_LZMA_UINT32_IS_ULONG", - "-D__LIBRETRO__", + "$(inherited)", ); + "OTHER_CFLAGS[sdk=iphonesimulator*]" = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.libretro.RetroArchiOS11; PRODUCT_NAME = RetroArch; PROVISIONING_PROFILE = ""; @@ -1723,106 +1648,29 @@ INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_NO_PIE = YES; + LD_NO_PIE = NO; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", + "@executable_path/modules", ); LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/iOS/modules"; MARKETING_VERSION = 1.16.0; MTL_FAST_MATH = YES; OTHER_CFLAGS = ( - "-DDONT_WANT_ARM_OPTIMIZATIONS", - "-DENABLE_HLSL", - "-DGLES_SILENCE_DEPRECATION", - "-DGLSLANG_OSINCLUDE_UNIX", - "-DHAVE_7ZIP", - "-DHAVE_AL", - "-DHAVE_ALTKIT", - "-DHAVE_AUDIOMIXER", - "-DHAVE_BTSTACK", - "-DHAVE_BUILTINGLSLANG", - "-DHAVE_BUILTINMBEDTLS", - "-DHAVE_BUILTINMINIUPNPC", - "-DHAVE_CC_RESAMPLER", - "-DHAVE_CHD", - "-DHAVE_CHEATS", - "-DHAVE_CHEEVOS", - "-DHAVE_CLOUDSYNC", - "-DHAVE_COCOATOUCH", - "-DHAVE_COCOA_METAL", - "-DHAVE_CONFIGFILE", - "-DHAVE_COREAUDIO", + "$(inherited)", "-DHAVE_COREMOTION", - "-DHAVE_DSP_FILTER", - "-DHAVE_DYNAMIC", - "-DHAVE_FILTERS_BUILTIN", - "-DHAVE_GFX_WIDGETS", - "-DHAVE_GLSL", - "-DHAVE_GLSLANG", - "-DHAVE_GRIFFIN", - "-DHAVE_HID", - "-DHAVE_IFINFO", - "-DHAVE_IMAGEVIEWER", "-DHAVE_IOS_CUSTOMKEYBOARD", "-DHAVE_IOS_SWIFT", "-DHAVE_IOS_TOUCHMOUSE", - "-DHAVE_KEYMAPPER", - "-DHAVE_LANGEXTRA", - "-DHAVE_LIBRETRODB", - "-DHAVE_MAIN", "-DHAVE_MATERIALUI", - "-DHAVE_MENU", - "-DHAVE_METAL", - "-DHAVE_MFI", - "-DHAVE_MINIUPNPC", - "-DHAVE_NETPLAYDISCOVERY", - "-DHAVE_NETPLAYDISCOVERY_NSNET", - "-DHAVE_NETWORKGAMEPAD", - "-DHAVE_NETWORKING", - "-DHAVE_ONLINE_UPDATER", - "-DHAVE_OPENGL", - "-DHAVE_OPENGLES", - "-DHAVE_OPENGLES2", "-DHAVE_OVERLAY", - "-DHAVE_OZONE", - "-DHAVE_PATCH", - "-DHAVE_RBMP", - "-DHAVE_REWIND", - "-DHAVE_RGUI", - "-DHAVE_RJPEG", - "-DHAVE_RPNG", - "-DHAVE_RTGA", - "-DHAVE_BSV_MOVIE", - "-DHAVE_RUNAHEAD", - "-DHAVE_RWAV", - "-DHAVE_SCREENSHOTS", - "-DHAVE_SHADERPIPELINE", - "-DHAVE_SLANG", - "-DHAVE_SPIRV_CROSS", - "-DHAVE_SSL", - "-DHAVE_STB_FONT", - "-DHAVE_STB_VORBIS", - "-DHAVE_THREADS", - "-DHAVE_TRANSLATE", - "-DHAVE_UPDATE_ASSETS", - "-DHAVE_UPDATE_CORE_INFO", - "-DHAVE_VIDEO_FILTER", + ); + "OTHER_CFLAGS[sdk=iphoneos*]" = ( "-DHAVE_VULKAN", - "-DHAVE_XMB", - "-DHAVE_ZLIB", - "-DINLINE=inline", - "-DIOS", - "-DNDEBUG", - "-DNS_BLOCK_ASSERTIONS=1", - "-DRARCH_INTERNAL", - "-DRARCH_MOBILE", - "-DRC_DISABLE_LUA", - "-DWANT_GLSLANG", - "-D_7ZIP_ST", - "-D_LZMA_UINT32_IS_ULONG", - "-D__LIBRETRO__", + "$(inherited)", ); + "OTHER_CFLAGS[sdk=iphonesimulator*]" = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.libretro.RetroArchiOS11; PRODUCT_NAME = RetroArch; PROVISIONING_PROFILE = ""; @@ -1909,90 +1757,12 @@ MARKETING_VERSION = 1.16.0; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = ( - "-DDONT_WANT_ARM_OPTIMIZATIONS", - "-DENABLE_HLSL", - "-DGLES_SILENCE_DEPRECATION", - "-DGLSLANG_OSINCLUDE_UNIX", - "-DHAVE_7ZIP", - "-DHAVE_AL", - "-DHAVE_ALTKIT", - "-DHAVE_AUDIOMIXER", - "-DHAVE_BTSTACK", - "-DHAVE_BUILTINGLSLANG", - "-DHAVE_BUILTINMBEDTLS", - "-DHAVE_BUILTINMINIUPNPC", - "-DHAVE_CC_RESAMPLER", - "-DHAVE_CHD", - "-DHAVE_CHEATS", - "-DHAVE_CHEEVOS", - "-DHAVE_CLOUDSYNC", - "-DHAVE_COCOATOUCH", - "-DHAVE_COCOA_METAL", - "-DHAVE_CONFIGFILE", - "-DHAVE_COREAUDIO", - "-DHAVE_DSP_FILTER", - "-DHAVE_DYNAMIC", - "-DHAVE_FILTERS_BUILTIN", - "-DHAVE_GFX_WIDGETS", - "-DHAVE_GLSL", - "-DHAVE_GLSLANG", - "-DHAVE_GRIFFIN", - "-DHAVE_HID", - "-DHAVE_IFINFO", - "-DHAVE_IMAGEVIEWER", - "-DHAVE_KEYMAPPER", - "-DHAVE_LANGEXTRA", - "-DHAVE_LIBRETRODB", - "-DHAVE_MAIN", - "-DHAVE_MENU", - "-DHAVE_METAL", - "-DHAVE_MFI", - "-DHAVE_MINIUPNPC", - "-DHAVE_NETPLAYDISCOVERY", - "-DHAVE_NETPLAYDISCOVERY_NSNET", - "-DHAVE_NETWORKGAMEPAD", - "-DHAVE_NETWORKING", - "-DHAVE_ONLINE_UPDATER", - "-DHAVE_OPENGL", - "-DHAVE_OPENGLES", - "-DHAVE_OPENGLES2", - "-DHAVE_OZONE", - "-DHAVE_PATCH", - "-DHAVE_RBMP", - "-DHAVE_REWIND", - "-DHAVE_RGUI", - "-DHAVE_RJPEG", - "-DHAVE_RPNG", - "-DHAVE_RTGA", - "-DHAVE_BSV_MOVIE", - "-DHAVE_RUNAHEAD", - "-DHAVE_RWAV", - "-DHAVE_SCREENSHOTS", - "-DHAVE_SHADERPIPELINE", - "-DHAVE_SLANG", - "-DHAVE_SPIRV_CROSS", - "-DHAVE_SSL", - "-DHAVE_STB_FONT", - "-DHAVE_STB_VORBIS", - "-DHAVE_THREADS", - "-DHAVE_TRANSLATE", - "-DHAVE_UPDATE_ASSETS", - "-DHAVE_UPDATE_CORE_INFO", - "-DHAVE_VIDEO_FILTER", + OTHER_CFLAGS = "$(inherited)"; + "OTHER_CFLAGS[sdk=appletvos*]" = ( "-DHAVE_VULKAN", - "-DHAVE_XMB", - "-DHAVE_ZLIB", - "-DINLINE=inline", - "-DIOS", - "-DRARCH_INTERNAL", - "-DRARCH_MOBILE", - "-DRC_DISABLE_LUA", - "-DWANT_GLSLANG", - "-D_7ZIP_ST", - "-D_LZMA_UINT32_IS_ULONG", - "-D__LIBRETRO__", + "$(inherited)", ); + "OTHER_CFLAGS[sdk=appletvsimulator*]" = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.libretro.dist.tvos.RetroArch; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2076,92 +1846,12 @@ MARKETING_VERSION = 1.16.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - OTHER_CFLAGS = ( - "-DDONT_WANT_ARM_OPTIMIZATIONS", - "-DENABLE_HLSL", - "-DGLES_SILENCE_DEPRECATION", - "-DGLSLANG_OSINCLUDE_UNIX", - "-DHAVE_7ZIP", - "-DHAVE_AL", - "-DHAVE_ALTKIT", - "-DHAVE_AUDIOMIXER", - "-DHAVE_BTSTACK", - "-DHAVE_BUILTINGLSLANG", - "-DHAVE_BUILTINMBEDTLS", - "-DHAVE_BUILTINMINIUPNPC", - "-DHAVE_CC_RESAMPLER", - "-DHAVE_CHD", - "-DHAVE_CHEATS", - "-DHAVE_CHEEVOS", - "-DHAVE_CLOUDSYNC", - "-DHAVE_COCOATOUCH", - "-DHAVE_COCOA_METAL", - "-DHAVE_CONFIGFILE", - "-DHAVE_COREAUDIO", - "-DHAVE_DSP_FILTER", - "-DHAVE_DYNAMIC", - "-DHAVE_FILTERS_BUILTIN", - "-DHAVE_GFX_WIDGETS", - "-DHAVE_GLSL", - "-DHAVE_GLSLANG", - "-DHAVE_GRIFFIN", - "-DHAVE_HID", - "-DHAVE_IFINFO", - "-DHAVE_IMAGEVIEWER", - "-DHAVE_KEYMAPPER", - "-DHAVE_LANGEXTRA", - "-DHAVE_LIBRETRODB", - "-DHAVE_MAIN", - "-DHAVE_MENU", - "-DHAVE_METAL", - "-DHAVE_MFI", - "-DHAVE_MINIUPNPC", - "-DHAVE_NETPLAYDISCOVERY", - "-DHAVE_NETPLAYDISCOVERY_NSNET", - "-DHAVE_NETWORKGAMEPAD", - "-DHAVE_NETWORKING", - "-DHAVE_ONLINE_UPDATER", - "-DHAVE_OPENGL", - "-DHAVE_OPENGLES", - "-DHAVE_OPENGLES2", - "-DHAVE_OZONE", - "-DHAVE_PATCH", - "-DHAVE_RBMP", - "-DHAVE_REWIND", - "-DHAVE_RGUI", - "-DHAVE_RJPEG", - "-DHAVE_RPNG", - "-DHAVE_RTGA", - "-DHAVE_BSV_MOVIE", - "-DHAVE_RUNAHEAD", - "-DHAVE_RWAV", - "-DHAVE_SCREENSHOTS", - "-DHAVE_SHADERPIPELINE", - "-DHAVE_SLANG", - "-DHAVE_SPIRV_CROSS", - "-DHAVE_SSL", - "-DHAVE_STB_FONT", - "-DHAVE_STB_VORBIS", - "-DHAVE_THREADS", - "-DHAVE_TRANSLATE", - "-DHAVE_UPDATE_ASSETS", - "-DHAVE_UPDATE_CORE_INFO", - "-DHAVE_VIDEO_FILTER", + OTHER_CFLAGS = "$(inherited)"; + "OTHER_CFLAGS[sdk=appletvos*]" = ( "-DHAVE_VULKAN", - "-DHAVE_XMB", - "-DHAVE_ZLIB", - "-DINLINE=inline", - "-DIOS", - "-DNDEBUG", - "-DNS_BLOCK_ASSERTIONS=1", - "-DRARCH_INTERNAL", - "-DRARCH_MOBILE", - "-DRC_DISABLE_LUA", - "-DWANT_GLSLANG", - "-D_7ZIP_ST", - "-D_LZMA_UINT32_IS_ULONG", - "-D__LIBRETRO__", + "$(inherited)", ); + "OTHER_CFLAGS[sdk=appletvsimulator*]" = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.libretro.dist.tvos.RetroArch; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2225,6 +1915,7 @@ LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../../Frameworks", + "@executable_path/../../modules", ); LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/iOS/modules"; MARKETING_VERSION = 1.16.0; @@ -2295,6 +1986,7 @@ LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../../Frameworks", + "@executable_path/../../modules", ); LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/iOS/modules"; MARKETING_VERSION = 1.16.0; @@ -2345,53 +2037,86 @@ ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DDONT_WANT_ARM_OPTIMIZATIONS", - "-DHAVE_NETWORKGAMEPAD", - "-DHAVE_STB_FONT", - "-DHAVE_HID", - "-DHAVE_NETWORKING", - "-DHAVE_IFINFO", - "-DHAVE_NETPLAYDISCOVERY", - "-DHAVE_BSV_MOVIE", - "-DHAVE_RUNAHEAD", - "-DHAVE_TRANSLATE", - "-DHAVE_GRIFFIN", - "-DHAVE_STB_VORBIS", - "-DHAVE_MINIUPNPC", + "-DENABLE_HLSL", + "-DGLES_SILENCE_DEPRECATION", + "-DGLSLANG_OSINCLUDE_UNIX", + "-DHAVE_7ZIP", + "-DHAVE_AL", + "-DHAVE_ALTKIT", + "-DHAVE_AUDIOMIXER", + "-DHAVE_BTSTACK", + "-DHAVE_BUILTINGLSLANG", + "-DHAVE_BUILTINMBEDTLS", "-DHAVE_BUILTINMINIUPNPC", - "-DHAVE_UPDATE_ASSETS", - "-DHAVE_ONLINE_UPDATER", - "-DHAVE_LANGEXTRA", + "-DHAVE_CC_RESAMPLER", + "-DHAVE_CHD", + "-DHAVE_CHEATS", "-DHAVE_CHEEVOS", - "-DRC_DISABLE_LUA", - "-DHAVE_IMAGEVIEWER", - "-DHAVE_RGUI", + "-DHAVE_CLOUDSYNC", + "-DHAVE_COCOATOUCH", + "-DHAVE_COCOA_METAL", "-DHAVE_CONFIGFILE", - "-DHAVE_MENU", + "-DHAVE_COREAUDIO", + "-DHAVE_DSP_FILTER", + "-DHAVE_DYNAMIC", + "-DHAVE_FILTERS_BUILTIN", "-DHAVE_GFX_WIDGETS", + "-DHAVE_GLSL", + "-DHAVE_GLSLANG", + "-DHAVE_GRIFFIN", + "-DHAVE_HID", + "-DHAVE_IFINFO", + "-DHAVE_IMAGEVIEWER", + "-DHAVE_KEYMAPPER", + "-DHAVE_LANGEXTRA", "-DHAVE_LIBRETRODB", - "-DHAVE_AUDIOMIXER", - "-DIOS", - "-DHAVE_DYNAMIC", + "-DHAVE_MAIN", + "-DHAVE_MENU", + "-DHAVE_METAL", + "-DHAVE_MFI", + "-DHAVE_MINIUPNPC", + "-DHAVE_NETPLAYDISCOVERY", + "-DHAVE_NETPLAYDISCOVERY_NSNET", + "-DHAVE_NETWORKGAMEPAD", + "-DHAVE_NETWORKING", + "-DHAVE_ONLINE_UPDATER", "-DHAVE_OPENGL", "-DHAVE_OPENGLES", - "-DHAVE_OPENGLES3", - "-DHAVE_CC_RESAMPLER", - "-DHAVE_GLSL", - "-DINLINE=inline", - "-DHAVE_THREADS", - "-D__LIBRETRO__", - "-DRARCH_MOBILE", - "-std=gnu99", - "-DHAVE_COREAUDIO", - "-DHAVE_OVERLAY", - "-DHAVE_ZLIB", - "-DHAVE_RPNG", - "-DHAVE_RJPEG", + "-DHAVE_OPENGLES2", + "-DHAVE_OZONE", + "-DHAVE_PATCH", "-DHAVE_RBMP", + "-DHAVE_REWIND", + "-DHAVE_RGUI", + "-DHAVE_RJPEG", + "-DHAVE_RPNG", "-DHAVE_RTGA", - "-DHAVE_COCOATOUCH", - "-DHAVE_MAIN", - "-DHAVE_VULKAN", + "-DHAVE_BSV_MOVIE", + "-DHAVE_RUNAHEAD", + "-DHAVE_RWAV", + "-DHAVE_SCREENSHOTS", + "-DHAVE_SHADERPIPELINE", + "-DHAVE_SLANG", + "-DHAVE_SPIRV_CROSS", + "-DHAVE_SSL", + "-DHAVE_STB_FONT", + "-DHAVE_STB_VORBIS", + "-DHAVE_THREADS", + "-DHAVE_TRANSLATE", + "-DHAVE_UPDATE_ASSETS", + "-DHAVE_UPDATE_CORE_INFO", + "-DHAVE_VIDEO_FILTER", + "-DHAVE_XMB", + "-DHAVE_ZLIB", + "-DINLINE=inline", + "-DIOS", + "-DRARCH_INTERNAL", + "-DRARCH_MOBILE", + "-DRC_DISABLE_LUA", + "-DWANT_GLSLANG", + "-D_7ZIP_ST", + "-D_LZMA_UINT32_IS_ULONG", + "-D__LIBRETRO__", ); "OTHER_LDFLAGS[arch=*]" = "-Wl,-segalign,4000"; SDKROOT = iphoneos; @@ -2428,55 +2153,89 @@ MTL_FAST_MATH = YES; MTL_IGNORE_WARNINGS = YES; OTHER_CFLAGS = ( - "-DNS_BLOCK_ASSERTIONS=1", - "-DNDEBUG", "-DDONT_WANT_ARM_OPTIMIZATIONS", - "-DHAVE_NETWORKGAMEPAD", - "-DHAVE_STB_FONT", - "-DHAVE_HID", - "-DHAVE_NETWORKING", - "-DHAVE_IFINFO", - "-DHAVE_NETPLAYDISCOVERY", - "-DHAVE_BSV_MOVIE", - "-DHAVE_RUNAHEAD", - "-DHAVE_TRANSLATE", - "-DHAVE_GRIFFIN", - "-DHAVE_STB_VORBIS", - "-DHAVE_MINIUPNPC", + "-DENABLE_HLSL", + "-DGLES_SILENCE_DEPRECATION", + "-DGLSLANG_OSINCLUDE_UNIX", + "-DHAVE_7ZIP", + "-DHAVE_AL", + "-DHAVE_ALTKIT", + "-DHAVE_AUDIOMIXER", + "-DHAVE_BTSTACK", + "-DHAVE_BUILTINGLSLANG", + "-DHAVE_BUILTINMBEDTLS", "-DHAVE_BUILTINMINIUPNPC", - "-DHAVE_UPDATE_ASSETS", - "-DHAVE_ONLINE_UPDATER", - "-DHAVE_LANGEXTRA", + "-DHAVE_CC_RESAMPLER", + "-DHAVE_CHD", + "-DHAVE_CHEATS", "-DHAVE_CHEEVOS", - "-DRC_DISABLE_LUA", - "-DHAVE_IMAGEVIEWER", - "-DHAVE_RGUI", + "-DHAVE_CLOUDSYNC", + "-DHAVE_COCOATOUCH", + "-DHAVE_COCOA_METAL", "-DHAVE_CONFIGFILE", - "-DHAVE_MENU", + "-DHAVE_COREAUDIO", + "-DHAVE_DSP_FILTER", + "-DHAVE_DYNAMIC", + "-DHAVE_FILTERS_BUILTIN", "-DHAVE_GFX_WIDGETS", + "-DHAVE_GLSL", + "-DHAVE_GLSLANG", + "-DHAVE_GRIFFIN", + "-DHAVE_HID", + "-DHAVE_IFINFO", + "-DHAVE_IMAGEVIEWER", + "-DHAVE_KEYMAPPER", + "-DHAVE_LANGEXTRA", "-DHAVE_LIBRETRODB", - "-DHAVE_AUDIOMIXER", - "-DIOS", - "-DHAVE_DYNAMIC", + "-DHAVE_MAIN", + "-DHAVE_MENU", + "-DHAVE_METAL", + "-DHAVE_MFI", + "-DHAVE_MINIUPNPC", + "-DHAVE_NETPLAYDISCOVERY", + "-DHAVE_NETPLAYDISCOVERY_NSNET", + "-DHAVE_NETWORKGAMEPAD", + "-DHAVE_NETWORKING", + "-DHAVE_ONLINE_UPDATER", "-DHAVE_OPENGL", "-DHAVE_OPENGLES", - "-DHAVE_CC_RESAMPLER", - "-DHAVE_GLSL", - "-DINLINE=inline", - "-DHAVE_THREADS", - "-D__LIBRETRO__", - "-DRARCH_MOBILE", - "-std=gnu99", - "-DHAVE_COREAUDIO", - "-DHAVE_OVERLAY", - "-DHAVE_ZLIB", - "-DHAVE_RPNG", - "-DHAVE_RJPEG", + "-DHAVE_OPENGLES2", + "-DHAVE_OZONE", + "-DHAVE_PATCH", "-DHAVE_RBMP", + "-DHAVE_REWIND", + "-DHAVE_RGUI", + "-DHAVE_RJPEG", + "-DHAVE_RPNG", "-DHAVE_RTGA", - "-DHAVE_COCOATOUCH", - "-DHAVE_MAIN", - "-DHAVE_VULKAN", + "-DHAVE_BSV_MOVIE", + "-DHAVE_RUNAHEAD", + "-DHAVE_RWAV", + "-DHAVE_SCREENSHOTS", + "-DHAVE_SHADERPIPELINE", + "-DHAVE_SLANG", + "-DHAVE_SPIRV_CROSS", + "-DHAVE_SSL", + "-DHAVE_STB_FONT", + "-DHAVE_STB_VORBIS", + "-DHAVE_THREADS", + "-DHAVE_TRANSLATE", + "-DHAVE_UPDATE_ASSETS", + "-DHAVE_UPDATE_CORE_INFO", + "-DHAVE_VIDEO_FILTER", + "-DHAVE_XMB", + "-DHAVE_ZLIB", + "-DINLINE=inline", + "-DIOS", + "-DNDEBUG", + "-DNS_BLOCK_ASSERTIONS=1", + "-DRARCH_INTERNAL", + "-DRARCH_MOBILE", + "-DRC_DISABLE_LUA", + "-DWANT_GLSLANG", + "-D_7ZIP_ST", + "-D_LZMA_UINT32_IS_ULONG", + "-D__LIBRETRO__", ); "OTHER_LDFLAGS[arch=*]" = "-Wl,-segalign,4000"; SDKROOT = iphoneos; diff --git a/pkg/emscripten/libretro/libretro.js b/pkg/emscripten/libretro/libretro.js index 48a3e233c83c..00eeaab0c178 100644 --- a/pkg/emscripten/libretro/libretro.js +++ b/pkg/emscripten/libretro/libretro.js @@ -6,6 +6,89 @@ var BrowserFS = BrowserFS; var afs; var initializationCount = 0; +var setImmediate; + +var Module = { + noInitialRun: true, + arguments: ["-v", "--menu"], + + encoder: new TextEncoder(), + message_queue:[], + message_out:[], + message_accum:"", + + retroArchSend: function(msg) { + let bytes = this.encoder.encode(msg+"\n"); + this.message_queue.push([bytes,0]); + }, + retroArchRecv: function() { + let out = this.message_out.shift(); + if(out == null && this.message_accum != "") { + out = this.message_accum; + this.message_accum = ""; + } + return out; + }, + preRun: [ + function(module) { + function stdin() { + // Return ASCII code of character, or null if no input + while(module.message_queue.length > 0){ + var msg = module.message_queue[0][0]; + var index = module.message_queue[0][1]; + if(index >= msg.length) { + module.message_queue.shift(); + } else { + module.message_queue[0][1] = index+1; + // assumption: msg is a uint8array + return msg[index]; + } + } + return null; + } + function stdout(c) { + if(c == null) { + // flush + if(module.message_accum != "") { + module.message_out.push(module.message_accum); + module.message_accum = ""; + } + } else { + let s = String.fromCharCode(c); + if(s == "\n") { + if(module.message_accum != "") { + module.message_out.push(module.message_accum); + module.message_accum = ""; + } + } else { + module.message_accum = module.message_accum+s; + } + } + } + module.FS.init(stdin, stdout); + } + ], + postRun: [], + onRuntimeInitialized: function() + { + appInitialized(); + }, + print: function(text) + { + console.log(text); + }, + printErr: function(text) + { + console.error(text); + }, + canvas: document.getElementById("canvas"), + totalDependencies: 0, + monitorRunDependencies: function(left) + { + this.totalDependencies = Math.max(this.totalDependencies, left); + } +}; + function cleanupStorage() { @@ -119,8 +202,8 @@ function setupFileSystem(backend) mfs.mount('/home/web_user/retroarch/bundle', xfs1); mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2); BrowserFS.initialize(mfs); - var BFS = new BrowserFS.EmscriptenFS(); - FS.mount(BFS, {root: '/home'}, '/home'); + var BFS = new BrowserFS.EmscriptenFS(Module.FS, Module.PATH, Module.ERRNO_CODES); + Module.FS.mount(BFS, {root: '/home'}, '/home'); console.log("WEBPLAYER: " + backend + " filesystem initialization successful"); } @@ -150,11 +233,12 @@ function startRetroArch() document.getElementById("btnMenu").disabled = false; document.getElementById("btnFullscreen").disabled = false; + Module["canvas"] = document.getElementById("canvas"); + Module["canvas"].addEventListener("click", () => Module["canvas"].focus()); Module['callMain'](Module['arguments']); Module['resumeMainLoop'](); - document.getElementById('canvas').focus(); + Module['canvas'].focus(); } - function selectFiles(files) { $('#btnAdd').addClass('disabled'); @@ -184,66 +268,13 @@ function selectFiles(files) function uploadData(data,name) { var dataView = new Uint8Array(data); - FS.createDataFile('/', name, dataView, true, false); + Module.FS.createDataFile('/', name, dataView, true, false); - var data = FS.readFile(name,{ encoding: 'binary' }); - FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data ,{ encoding: 'binary' }); - FS.unlink(name); + var data = Module.FS.readFile(name,{ encoding: 'binary' }); + Module.FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data ,{ encoding: 'binary' }); + Module.FS.unlink(name); } -var encoder = new TextEncoder(); -var message_queue = []; - -function retroArchSend(msg) { - var bytes = encoder.encode(msg+"\n"); - message_queue.push([bytes,0]); -} - -var Module = -{ - noInitialRun: true, - arguments: ["-v", "--menu"], - preRun: [ - function() { - function stdin() { - // Return ASCII code of character, or null if no input - while(message_queue.length > 0){ - var msg = message_queue[0][0]; - var index = message_queue[0][1]; - if(index >= msg.length) { - message_queue.shift(); - } else { - message_queue[0][1] = index+1; - // assumption: msg is a uint8array - return msg[index]; - } - } - return null; - } - FS.init(stdin); - } - ], - postRun: [], - onRuntimeInitialized: function() - { - appInitialized(); - }, - print: function(text) - { - console.log(text); - }, - printErr: function(text) - { - console.log(text); - }, - canvas: document.getElementById('canvas'), - totalDependencies: 0, - monitorRunDependencies: function(left) - { - this.totalDependencies = Math.max(this.totalDependencies, left); - } -}; - function switchCore(corename) { localStorage.setItem("core", corename); } @@ -258,7 +289,7 @@ function switchStorage(backend) { // When the browser has loaded everything. $(function() { - // Enable all available ToolTips. + // Enable all available ToolTips. $('.tooltip-enable').tooltip({ placement: 'right' }); @@ -273,74 +304,77 @@ $(function() { /** * Attempt to disable some default browser keys. */ - var keys = { - 9: "tab", - 13: "enter", - 16: "shift", - 18: "alt", - 27: "esc", - 33: "rePag", - 34: "avPag", - 35: "end", - 36: "home", - 37: "left", - 38: "up", - 39: "right", - 40: "down", - 112: "F1", - 113: "F2", - 114: "F3", - 115: "F4", - 116: "F5", - 117: "F6", - 118: "F7", - 119: "F8", - 120: "F9", - 121: "F10", - 122: "F11", - 123: "F12" - }; - window.addEventListener('keydown', function (e) { - if (keys[e.which]) { - e.preventDefault(); - } - }); + var keys = { + 9: "tab", + 13: "enter", + 16: "shift", + 18: "alt", + 27: "esc", + 33: "rePag", + 34: "avPag", + 35: "end", + 36: "home", + 37: "left", + 38: "up", + 39: "right", + 40: "down", + 112: "F1", + 113: "F2", + 114: "F3", + 115: "F4", + 116: "F5", + 117: "F6", + 118: "F7", + 119: "F8", + 120: "F9", + 121: "F10", + 122: "F11", + 123: "F12" + }; + window.addEventListener('keydown', function (e) { + if (keys[e.which]) { + e.preventDefault(); + } + }); // Switch the core when selecting one. $('#core-selector a').click(function () { var coreChoice = $(this).data('core'); switchCore(coreChoice); }); - // Find which core to load. var core = localStorage.getItem("core", core); if (!core) { core = 'gambatte'; } + loadCore(core); +}); + +function loadCore(core) { // Make the core the selected core in the UI. var coreTitle = $('#core-selector a[data-core="' + core + '"]').addClass('active').text(); $('#dropdownMenu1').text(coreTitle); - // Load the Core's related JavaScript. - $.getScript(core + '_libretro.js', function () - { - $('#icnRun').removeClass('fa-spinner').removeClass('fa-spin'); - $('#icnRun').addClass('fa-play'); - $('#lblDrop').removeClass('active'); - $('#lblLocal').addClass('active'); - idbfsInit(); - }); - }); + import("./"+core+"_libretro.js").then(script => { + script.default(Module).then(mod => { + Module = mod; + $('#icnRun').removeClass('fa-spinner').removeClass('fa-spin'); + $('#icnRun').addClass('fa-play'); + $('#lblDrop').removeClass('active'); + $('#lblLocal').addClass('active'); + idbfsInit(); + }).catch(err => { console.error("Couldn't instantiate module",err); throw err; }); + }).catch(err => { console.error("Couldn't load script",err); throw err; }); +} function keyPress(k) { + function kp(k, event) { + var oEvent = new KeyboardEvent(event, { code: k }); + + document.dispatchEvent(oEvent); + document.getElementById('canvas').focus(); + } kp(k, "keydown"); setTimeout(function(){kp(k, "keyup")}, 50); } - -kp = function(k, event) { - var oEvent = new KeyboardEvent(event, { code: k }); - - document.dispatchEvent(oEvent); - document.getElementById('canvas').focus(); -} diff --git a/retroarch.c b/retroarch.c index ddc0ca9eda09..2953c652086a 100644 --- a/retroarch.c +++ b/retroarch.c @@ -601,12 +601,24 @@ static float audio_driver_monitor_adjust_system_rates( double input_fps, float video_refresh_rate, unsigned video_swap_interval, + unsigned black_frame_insertion, float audio_max_timing_skew) { float inp_sample_rate = input_sample_rate; - float target_video_sync_rate = video_refresh_rate - / (float)video_swap_interval; - float timing_skew = + + /* This much like the auto swap interval algorithm and will + * find the correct desired target rate the majority of sane + * cases. Any failures should be no worse than the previous + * very incomplete high hz skew adjustments. */ + float refresh_ratio = video_refresh_rate/input_fps; + unsigned refresh_closest_multiple = (unsigned)(refresh_ratio + 0.5f); + float target_video_sync_rate = video_refresh_rate; + float timing_skew = 0.0f; + + if (refresh_closest_multiple > 1) + target_video_sync_rate /= (((float)black_frame_insertion + 1.0f) * (float)video_swap_interval); + + timing_skew = fabs(1.0f - input_fps / target_video_sync_rate); if (timing_skew <= audio_max_timing_skew) return (inp_sample_rate * target_video_sync_rate / input_fps); @@ -620,18 +632,23 @@ static bool video_driver_monitor_adjust_system_rates( bool vrr_runloop_enable, float audio_max_timing_skew, unsigned video_swap_interval, + unsigned black_frame_insertion, double input_fps) { float target_video_sync_rate = timing_skew_hz; - /* Divide target rate only when using Auto interval */ - if (_video_swap_interval == 0) - target_video_sync_rate /= (float)video_swap_interval; + /* Same concept as for audio driver adjust. */ + float refresh_ratio = target_video_sync_rate/input_fps; + unsigned refresh_closest_multiple = (unsigned)(refresh_ratio + 0.5f); + float timing_skew = 0.0f; + + if (refresh_closest_multiple > 1) + target_video_sync_rate /= (((float)black_frame_insertion + 1.0f) * (float)video_swap_interval); if (!vrr_runloop_enable) { - float timing_skew = fabs( - 1.0f - input_fps / target_video_sync_rate); + timing_skew = + fabs(1.0f - input_fps / target_video_sync_rate); /* We don't want to adjust pitch too much. If we have extreme cases, * just don't readjust at all. */ if (timing_skew <= audio_max_timing_skew) @@ -652,7 +669,8 @@ static void driver_adjust_system_rates( float video_refresh_rate, float audio_max_timing_skew, bool video_adaptive_vsync, - unsigned video_swap_interval) + unsigned video_swap_interval, + unsigned black_frame_insertion) { struct retro_system_av_info *av_info = &video_st->av_info; const struct retro_system_timing *info = @@ -666,6 +684,7 @@ static void driver_adjust_system_rates( vrr_runloop_enable, (video_st->flags & VIDEO_FLAG_CRT_SWITCHING_ACTIVE) ? true : false, video_swap_interval, + black_frame_insertion, audio_max_timing_skew, video_refresh_rate, input_fps); @@ -684,6 +703,7 @@ static void driver_adjust_system_rates( input_fps, video_refresh_rate, video_swap_interval, + black_frame_insertion, audio_max_timing_skew); RARCH_LOG("[Audio]: Set audio input rate to: %.2f Hz.\n", @@ -707,6 +727,7 @@ static void driver_adjust_system_rates( vrr_runloop_enable, audio_max_timing_skew, video_swap_interval, + black_frame_insertion, input_fps)) { /* We won't be able to do VSync reliably @@ -826,7 +847,8 @@ void drivers_init( settings->floats.video_refresh_rate, settings->floats.audio_max_timing_skew, settings->bools.video_adaptive_vsync, - settings->uints.video_swap_interval + settings->uints.video_swap_interval, + settings->uints.video_black_frame_insertion ); /* Initialize video driver */ @@ -1260,6 +1282,8 @@ bool driver_ctl(enum driver_ctl_state state, void *data) float audio_max_timing_skew = settings->floats.audio_max_timing_skew; bool video_adaptive_vsync = settings->bools.video_adaptive_vsync; unsigned video_swap_interval = settings->uints.video_swap_interval; + unsigned + black_frame_insertion = settings->uints.video_black_frame_insertion; video_monitor_set_refresh_rate(*hz); @@ -1273,7 +1297,8 @@ bool driver_ctl(enum driver_ctl_state state, void *data) video_refresh_rate, audio_max_timing_skew, video_adaptive_vsync, - video_swap_interval + video_swap_interval, + black_frame_insertion ); } break; @@ -2211,6 +2236,9 @@ bool command_event(enum event_command cmd, void *data) #if defined(HAVE_ACCESSIBILITY) || defined(HAVE_TRANSLATE) access_state_t *access_st = access_state_get_ptr(); #endif +#if defined(HAVE_TRANSLATE) && defined(HAVE_GFX_WIDGETS) + dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); +#endif #ifdef HAVE_MENU struct menu_state *menu_st = menu_state_get_ptr(); #endif @@ -2227,12 +2255,12 @@ bool command_event(enum event_command cmd, void *data) #ifdef HAVE_OVERLAY input_overlay_unload(); #endif -#if defined(HAVE_TRANSLATE) && defined(HAVE_GFX_WIDGETS) - /* Because the overlay is a display widget, - * it's going to be written - * over the menu, so we unset it here. */ - if (dispwidget_get_ptr()->ai_service_overlay_state != 0) +#ifdef HAVE_TRANSLATE + translation_release(true); +#ifdef HAVE_GFX_WIDGETS + if (p_dispwidget->ai_service_overlay_state != 0) gfx_widgets_ai_service_overlay_unload(); +#endif #endif break; case CMD_EVENT_OVERLAY_INIT: @@ -2306,6 +2334,11 @@ bool command_event(enum event_command cmd, void *data) accessibility_narrator_speech_speed, (char*)msg_hash_to_str(MSG_UNPAUSED), 10); #endif +#ifdef HAVE_GFX_WIDGETS + if (p_dispwidget->ai_service_overlay_state != 0) + gfx_widgets_ai_service_overlay_unload(); +#endif + translation_release(true); command_event(CMD_EVENT_UNPAUSE, NULL); } else /* Pause on call */ @@ -2324,17 +2357,25 @@ bool command_event(enum event_command cmd, void *data) * Also, this mode is required for "auto" translation * packages, since you don't want to pause for that. */ - if (access_st->ai_service_auto == 2) + if (access_st->ai_service_auto != 0) { /* Auto mode was turned on, but we pressed the * toggle button, so turn it off now. */ - access_st->ai_service_auto = 0; -#ifdef HAVE_MENU_WIDGETS - gfx_widgets_ai_service_overlay_unload(); + translation_release(true); +#ifdef HAVE_GFX_WIDGETS + if (p_dispwidget->ai_service_overlay_state != 0) + gfx_widgets_ai_service_overlay_unload(); #endif } - else - command_event(CMD_EVENT_AI_SERVICE_CALL, NULL); + else + { +#ifdef HAVE_GFX_WIDGETS + if (p_dispwidget->ai_service_overlay_state != 0) + gfx_widgets_ai_service_overlay_unload(); + else +#endif + command_event(CMD_EVENT_AI_SERVICE_CALL, NULL); + } } #endif break; @@ -3758,7 +3799,7 @@ bool command_event(enum event_command cmd, void *data) case CMD_EVENT_NETPLAY_INIT: { char tmp_netplay_server[256]; - char tmp_netplay_session[sizeof(tmp_netplay_server)]; + char tmp_netplay_session[256]; char *netplay_server = NULL; char *netplay_session = NULL; unsigned netplay_port = 0; @@ -3774,22 +3815,16 @@ bool command_event(enum event_command cmd, void *data) netplay_server = tmp_netplay_server; netplay_session = tmp_netplay_session; } - - if (p_rarch->connect_mitm_id) + + if (p_rarch->connect_mitm_id) netplay_session = strdup(p_rarch->connect_mitm_id); - - if (p_rarch->connect_host) + + if (p_rarch->connect_host) { free(p_rarch->connect_host); p_rarch->connect_host = NULL; } - if (p_rarch->connect_mitm_id) - { - free(p_rarch->connect_mitm_id); - p_rarch->connect_mitm_id = NULL; - } - if (string_is_empty(netplay_server)) netplay_server = settings->paths.netplay_server; if (!netplay_port) @@ -3798,9 +3833,24 @@ bool command_event(enum event_command cmd, void *data) if (!init_netplay(netplay_server, netplay_port, netplay_session)) { command_event(CMD_EVENT_NETPLAY_DEINIT, NULL); + if (p_rarch->connect_mitm_id) + { + free(p_rarch->connect_mitm_id); + free(netplay_session); + p_rarch->connect_mitm_id = NULL; + netplay_session = NULL; + } return false; } + if (p_rarch->connect_mitm_id) + { + free(p_rarch->connect_mitm_id); + free(netplay_session); + p_rarch->connect_mitm_id = NULL; + netplay_session = NULL; + } + /* Disable rewind & SRAM autosave if it was enabled * TODO/FIXME: Add a setting for these tweaks */ #ifdef HAVE_REWIND @@ -3816,7 +3866,7 @@ bool command_event(enum event_command cmd, void *data) case CMD_EVENT_NETPLAY_INIT_DIRECT: { char netplay_server[256]; - char netplay_session[sizeof(netplay_server)]; + char netplay_session[256]; unsigned netplay_port = 0; command_event(CMD_EVENT_NETPLAY_DEINIT, NULL); @@ -3853,7 +3903,7 @@ bool command_event(enum event_command cmd, void *data) case CMD_EVENT_NETPLAY_INIT_DIRECT_DEFERRED: { char netplay_server[256]; - char netplay_session[sizeof(netplay_server)]; + char netplay_session[256]; unsigned netplay_port = 0; command_event(CMD_EVENT_NETPLAY_DEINIT, NULL); @@ -4439,10 +4489,6 @@ bool command_event(enum event_command cmd, void *data) if (data) paused = *((bool*)data); - if ( (access_st->ai_service_auto == 0) - && !settings->bools.ai_service_pause) - access_st->ai_service_auto = 1; - run_translation_service(settings, paused); } #endif @@ -5871,6 +5917,10 @@ static bool retroarch_parse_input_and_config( if (!(p_rarch->flags & RARCH_FLAGS_BLOCK_CONFIG_READ)) #endif { + /* Workaround for libdecor 0.2.0 setting unwanted locale */ +#if defined(HAVE_WAYLAND) && defined(HAVE_DYNAMIC) + setlocale(LC_NUMERIC,"C"); +#endif /* If this is a static build, load salamander * config file first (sets RARCH_PATH_CORE) */ #if !defined(HAVE_DYNAMIC) @@ -6061,7 +6111,7 @@ static bool retroarch_parse_input_and_config( netplay_driver_ctl(RARCH_NETPLAY_CTL_ENABLE_CLIENT, NULL); p_rarch->connect_host = strdup(optarg); break; - + case 'T': p_rarch->connect_mitm_id = strdup(optarg); break; @@ -7131,6 +7181,9 @@ bool retroarch_main_quit(void) video_driver_state_t*video_st = video_state_get_ptr(); settings_t *settings = config_get_ptr(); bool config_save_on_exit = settings->bools.config_save_on_exit; +#ifdef HAVE_ACCESSIBILITY + access_state_t *access_st = access_state_get_ptr(); +#endif struct retro_system_av_info *av_info = &video_st->av_info; /* Restore video driver before saving */ @@ -7229,6 +7282,17 @@ bool retroarch_main_quit(void) retroarch_menu_running_finished(true); #endif +#ifdef HAVE_ACCESSIBILITY + translation_release(false); +#ifdef HAVE_THREADS + if (access_st->image_lock) + { + slock_free(access_st->image_lock); + access_st->image_lock = NULL; + } +#endif +#endif + return true; } @@ -7282,6 +7346,7 @@ enum retro_language retroarch_get_language_from_iso(const char *iso639) {"en_GB", RETRO_LANGUAGE_BRITISH_ENGLISH}, {"en", RETRO_LANGUAGE_ENGLISH}, {"hu", RETRO_LANGUAGE_HUNGARIAN}, + {"be", RETRO_LANGUAGE_BELARUSIAN}, }; if (string_is_empty(iso639)) diff --git a/runloop.c b/runloop.c index f2516e0f4283..e4e325becb2d 100644 --- a/runloop.c +++ b/runloop.c @@ -896,19 +896,19 @@ static void libretro_log_cb( switch (level) { case RETRO_LOG_DEBUG: - RARCH_LOG_V("[libretro DEBUG]", fmt, vp); + RARCH_LOG_V("libretro " FILE_PATH_LOG_DBG, fmt, vp); break; case RETRO_LOG_INFO: - RARCH_LOG_OUTPUT_V("[libretro INFO]", fmt, vp); + RARCH_LOG_OUTPUT_V("libretro " FILE_PATH_LOG_INFO, fmt, vp); break; case RETRO_LOG_WARN: - RARCH_WARN_V("[libretro WARN]", fmt, vp); + RARCH_WARN_V("libretro " FILE_PATH_LOG_WARN, fmt, vp); break; case RETRO_LOG_ERROR: - RARCH_ERR_V("[libretro ERROR]", fmt, vp); + RARCH_ERR_V("libretro " FILE_PATH_LOG_ERROR, fmt, vp); break; default: @@ -1996,8 +1996,9 @@ bool runloop_environment_cb(unsigned cmd, void *data) break; case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: - RARCH_LOG("[Environ]: GET_SAVE_DIRECTORY.\n"); *(const char**)data = runloop_st->savefile_dir; + RARCH_LOG("[Environ]: SAVE_DIRECTORY: \"%s\".\n", + runloop_st->savefile_dir); break; case RETRO_ENVIRONMENT_GET_USERNAME: @@ -4082,10 +4083,15 @@ void runloop_event_deinit_core(void) av_info->timing.fps = video_st->video_refresh_rate_original; video_display_server_restore_refresh_rate(); } + /* Recalibrate frame delay target */ if (settings->bools.video_frame_delay_auto) video_st->frame_delay_target = 0; + /* Reset frame rest counter */ + if (settings->bools.video_frame_rest) + video_st->frame_rest_time_count = video_st->frame_rest = 0; + driver_uninit(DRIVERS_CMD_ALL, 0); #ifdef HAVE_CONFIGFILE @@ -4399,6 +4405,7 @@ void runloop_set_video_swap_interval( bool vrr_runloop_enable, bool crt_switching_active, unsigned swap_interval_config, + unsigned black_frame_insertion, float audio_max_timing_skew, float video_refresh_rate, double input_fps) @@ -4425,11 +4432,13 @@ void runloop_set_video_swap_interval( * > If core fps is higher than display refresh rate, * set swap interval to 1 * > If core fps or display refresh rate are zero, - * set swap interval to 1 */ + * set swap interval to 1 + * > If BFI is active set swap interval to 1 */ if ( (vrr_runloop_enable) || (core_hz > timing_hz) || (core_hz <= 0.0f) - || (timing_hz <= 0.0f)) + || (timing_hz <= 0.0f) + || (black_frame_insertion)) { runloop_st->video_swap_interval_auto = 1; return; @@ -4628,7 +4637,7 @@ bool runloop_event_init_core( type, &runloop_st->current_core, NULL, NULL)) return false; #ifdef HAVE_RUNAHEAD - /* remember last core type created, so creating a + /* Remember last core type created, so creating a * secondary core will know what core type to use. */ runloop_st->last_core_type = type; #endif @@ -4687,7 +4696,7 @@ bool runloop_event_init_core( settings, &runloop_st->fastmotion_override.current); #ifdef HAVE_CHEEVOS - /* assume the core supports achievements unless it tells us otherwise */ + /* Assume the core supports achievements unless it tells us otherwise */ rcheevos_set_support_cheevos(true); #endif @@ -4698,9 +4707,13 @@ bool runloop_event_init_core( runloop_st->shader_delay_timer.timer_end = false; /* not expired */ #endif - /* reset video format to libretro's default */ + /* Reset video format to libretro's default */ video_st->pix_fmt = RETRO_PIXEL_FORMAT_0RGB1555; + /* Set save redirection paths */ + runloop_path_set_redirect(settings, old_savefile_dir, old_savestate_dir); + + /* Set core environment */ runloop_st->current_core.retro_set_environment(runloop_environment_cb); /* Load any input remap files @@ -4717,9 +4730,6 @@ bool runloop_event_init_core( config_load_remap(dir_input_remapping, &runloop_st->system); #endif - /* Per-core saves: reset redirection paths */ - runloop_path_set_redirect(settings, old_savefile_dir, old_savestate_dir); - video_st->frame_cache_data = NULL; runloop_st->current_core.retro_init(); @@ -6191,15 +6201,7 @@ static enum runloop_state_enum runloop_check_state( #ifdef HAVE_MENU /* Stop checking the rest of the hotkeys if menu is alive */ if (menu_st->flags & MENU_ST_FLAG_ALIVE) - { - float fastforward_ratio = runloop_get_fastforward_ratio(settings, - &runloop_st->fastmotion_override.current); - - if (!settings->bools.menu_throttle_framerate && !fastforward_ratio) - return RUNLOOP_STATE_MENU_ITERATE; - return RUNLOOP_STATE_END; - } #endif #ifdef HAVE_NETWORKING @@ -6944,13 +6946,14 @@ int runloop_iterate(void) netplay_driver_ctl(RARCH_NETPLAY_CTL_PAUSE, NULL); #endif #endif - goto end; - case RUNLOOP_STATE_MENU_ITERATE: -#ifdef HAVE_NETWORKING - /* FIXME: This is an ugly way to tell Netplay this... */ - netplay_driver_ctl(RARCH_NETPLAY_CTL_PAUSE, NULL); +#ifdef HAVE_MENU + /* Always run menu in 1x speed. */ + if (menu_state_get_ptr()->flags & MENU_ST_FLAG_ALIVE) + runloop_set_frame_limit(&video_st->av_info, 1.0f); + else + runloop_set_frame_limit(&video_st->av_info, settings->floats.fastforward_ratio); #endif - return 0; + goto end; case RUNLOOP_STATE_ITERATE: runloop_st->flags |= RUNLOOP_FLAG_CORE_RUNNING; break; @@ -7249,8 +7252,7 @@ int runloop_iterate(void) runloop_get_fastforward_ratio(settings, &runloop_st->fastmotion_override.current)); else - runloop_set_frame_limit(&video_st->av_info, - 1.0f); + runloop_set_frame_limit(&video_st->av_info, 1.0f); } /* if there's a fast forward limit, inject sleeps to keep from going too fast. */ @@ -7284,6 +7286,11 @@ int runloop_iterate(void) runloop_st->frame_limit_last_time = end_frame_time; } + /* Post-frame power saving sleep resting */ + if ( settings->bools.video_frame_rest + && !(input_st->flags & INP_FLAG_NONBLOCKING)) + video_frame_rest(video_st, settings, current_time); + /* Set paused state after x frames */ if (runloop_st->run_frames_and_pause > 0) { diff --git a/runloop.h b/runloop.h index 3ba255f7ba9a..9a8db87ed06c 100644 --- a/runloop.h +++ b/runloop.h @@ -76,11 +76,10 @@ */ #define RUNLOOP_TIME_TO_EXIT(quit_key_pressed) ((runloop_state.flags & RUNLOOP_FLAG_SHUTDOWN_INITIATED) || quit_key_pressed || !is_alive BSV_MOVIE_IS_EOF() || ((runloop_state.max_frames != 0) && (frame_count >= runloop_state.max_frames)) || runloop_exec) -enum runloop_state_enum +enum runloop_state_enum { RUNLOOP_STATE_ITERATE = 0, RUNLOOP_STATE_POLLED_AND_SLEEP, - RUNLOOP_STATE_MENU_ITERATE, RUNLOOP_STATE_PAUSE, RUNLOOP_STATE_END, RUNLOOP_STATE_QUIT @@ -363,7 +362,7 @@ bool libretro_get_system_info( bool *load_no_content); void runloop_performance_counter_register( - struct retro_perf_counter *perf); + struct retro_perf_counter *perf); void runloop_runtime_log_deinit( runloop_state_t *runloop_st, @@ -396,6 +395,7 @@ void runloop_set_video_swap_interval( bool vrr_runloop_enable, bool crt_switching_active, unsigned swap_interval_config, + unsigned black_frame_insertion, float audio_max_timing_skew, float video_refresh_rate, double input_fps); @@ -451,7 +451,7 @@ void runloop_path_deinit_subsystem(void); * @return true on success, or false if symbols could not be loaded. **/ bool runloop_init_libretro_symbols( - void *data, + void *data, enum rarch_core_type type, struct retro_core_t *current_core, const char *lib_path, diff --git a/tasks/task_cloudsync.c b/tasks/task_cloudsync.c index a1e453f5fb3e..a44baee22ddc 100644 --- a/tasks/task_cloudsync.c +++ b/tasks/task_cloudsync.c @@ -840,9 +840,12 @@ static void task_cloud_sync_diff_next(task_cloud_sync_state_t *sync_state) if (!CS_FILE_DELETED(server_file)) task_cloud_sync_delete_server_file(sync_state); else + { /* already deleted, oh well */ task_cloud_sync_add_to_updated_manifest(sync_state, CS_FILE_KEY(server_file), NULL, true); + task_cloud_sync_add_to_updated_manifest(sync_state, CS_FILE_KEY(server_file), NULL, false); /* we don't mark need_manifest_uploaded here, nothing has changed */ + } sync_state->local_idx++; sync_state->server_idx++; } diff --git a/tasks/task_save.c b/tasks/task_save.c index 859f1629c7ff..fd4b19750781 100644 --- a/tasks/task_save.c +++ b/tasks/task_save.c @@ -62,7 +62,11 @@ #include "../cheat_manager.h" #endif -#if defined(HAVE_LIBNX) || defined(_3DS) +#ifdef EMSCRIPTEN +/* Filesystem is in-memory anyway, use huge chunks since each + read/write is a possible suspend to JS code */ +#define SAVE_STATE_CHUNK 4096 * 4096 +#elif defined(HAVE_LIBNX) || defined(_3DS) #define SAVE_STATE_CHUNK 4096 * 10 #else #define SAVE_STATE_CHUNK 4096 diff --git a/tasks/task_translation.c b/tasks/task_translation.c index 7082a273caca..fafe3c29c50e 100644 --- a/tasks/task_translation.c +++ b/tasks/task_translation.c @@ -29,9 +29,11 @@ #include #include #include +#include #include #include #include +#include #include "../translation_defines.h" #ifdef HAVE_GFX_WIDGETS @@ -47,588 +49,290 @@ #include "../paths.h" #include "../runloop.h" #include "../verbosity.h" +#include "../msg_hash.h" #include "tasks_internal.h" -static void task_auto_translate_handler(retro_task_t *task) +static const char* ACCESS_INPUT_LABELS[] = +{ + "b", "y", "select", "start", "up", "down", "left", "right", + "a", "x", "l", "r", "l2", "r2", "l3", "r3" +}; + +static const char* ACCESS_RESPONSE_KEYS[] = +{ + "image", "sound", "text", "error", "auto", "press", "text_position" +}; + +typedef struct { - int *mode_ptr = (int*)task->user_data; - uint32_t runloop_flags = runloop_get_flags(); - access_state_t *access_st = access_state_get_ptr(); -#ifdef HAVE_ACCESSIBILITY - settings_t *settings = config_get_ptr(); -#endif + uint8_t *data; + unsigned size; + unsigned width; + unsigned height; + + unsigned content_x; + unsigned content_y; + unsigned content_width; + unsigned content_height; + unsigned viewport_width; + unsigned viewport_height; +} access_frame_t; + +typedef struct +{ + char *data; + int length; + char format[4]; +} access_base64_t; - if (task_get_cancelled(task)) - goto task_finished; +typedef struct +{ + char *inputs; + bool paused; +} access_request_t; - switch (*mode_ptr) - { - case 1: /* Speech Mode */ +typedef struct +{ + char *image; + int image_size; #ifdef HAVE_AUDIOMIXER - if (!audio_driver_is_ai_service_speech_running()) - goto task_finished; + void *sound; + int sound_size; #endif - break; - case 2: /* Narrator Mode */ + char *error; + char *text; + char *recall; + char *input; + int text_position; +} access_response_t; + +/* UTILITIES ---------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ + +/** + * Returns true if the accessibility narrator is currently playing audio. + */ #ifdef HAVE_ACCESSIBILITY - if (!is_narrator_running( - settings->bools.accessibility_enable)) - goto task_finished; -#endif - break; - default: - break; - } - - return; - -task_finished: - if (access_st->ai_service_auto == 1) - access_st->ai_service_auto = 2; - - task_set_finished(task, true); - - if (*mode_ptr == 1 || *mode_ptr == 2) +bool is_narrator_running(bool accessibility_enable) +{ + access_state_t *access_st = access_state_get_ptr(); + if (is_accessibility_enabled( + accessibility_enable, + access_st->enabled)) { - bool was_paused = (runloop_flags & RUNLOOP_FLAG_PAUSED) ? true : false; - command_event(CMD_EVENT_AI_SERVICE_CALL, &was_paused); + frontend_ctx_driver_t *frontend = + frontend_state_get_ptr()->current_frontend_ctx; + if (frontend && frontend->is_narrator_running) + return frontend->is_narrator_running(); } - if (task->user_data) - free(task->user_data); + return false; } +#endif -static void call_auto_translate_task( - settings_t *settings, - bool *was_paused) +/** + * Returns true if array {a} and {b}, both of the same size {size} are equal. + * This method prevents a potential bug with memcmp on some platforms. + */ +static bool u8_array_equal(uint8_t *a, uint8_t *b, int size) { - int ai_service_mode = settings->uints.ai_service_mode; - access_state_t *access_st = access_state_get_ptr(); - - /*Image Mode*/ - if (ai_service_mode == 0) - { - if (access_st->ai_service_auto == 1) - access_st->ai_service_auto = 2; - - command_event(CMD_EVENT_AI_SERVICE_CALL, was_paused); - } - else /* Speech or Narrator Mode */ + int i = 0; + for (; i < size; i++) { - int* mode = NULL; - retro_task_t *t = task_init(); - if (!t) - return; - - mode = (int*)malloc(sizeof(int)); - *mode = ai_service_mode; - - t->handler = task_auto_translate_handler; - t->user_data = mode; - t->mute = true; - task_queue_push(t); + if (a[i] != b[i]) + return false; } + return true; } -static void handle_translation_cb( - retro_task_t *task, void *task_data, - void *user_data, const char *error) +/** + * Helper method to simplify accessibility speech usage. This method will only + * use TTS to read the provided text if accessibility has been enabled in the + * frontend or by RetroArch's internal override mechanism. + */ +static void accessibility_speak(const char *text) { - uint8_t* raw_output_data = NULL; - char* raw_image_file_data = NULL; - struct scaler_ctx* scaler = NULL; - http_transfer_data_t *data = (http_transfer_data_t*)task_data; - int new_image_size = 0; -#ifdef HAVE_AUDIOMIXER - int new_sound_size = 0; -#endif - void* raw_image_data = NULL; - void* raw_image_data_alpha = NULL; - void* raw_sound_data = NULL; - rjson_t *json = NULL; - int json_current_key = 0; - char* err_str = NULL; - char* txt_str = NULL; - char* auto_str = NULL; - char* key_str = NULL; - settings_t* settings = config_get_ptr(); - uint32_t runloop_flags = runloop_get_flags(); -#ifdef HAVE_ACCESSIBILITY - input_driver_state_t *input_st = input_state_get_ptr(); -#endif - video_driver_state_t - *video_st = video_state_get_ptr(); - const enum retro_pixel_format - video_driver_pix_fmt = video_st->pix_fmt; - access_state_t *access_st = access_state_get_ptr(); -#ifdef HAVE_GFX_WIDGETS - bool gfx_widgets_paused = (video_st->flags & - VIDEO_FLAG_WIDGETS_PAUSED) ? true : false; - dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); -#endif #ifdef HAVE_ACCESSIBILITY - bool accessibility_enable = settings->bools.accessibility_enable; - unsigned accessibility_narrator_speech_speed = settings->uints.accessibility_narrator_speech_speed; -#ifdef HAVE_GFX_WIDGETS - /* When auto mode is on, we turn off the overlay - * once we have the result for the next call.*/ - if (p_dispwidget->ai_service_overlay_state != 0 - && access_st->ai_service_auto == 2) - gfx_widgets_ai_service_overlay_unload(); -#endif + settings_t *settings = config_get_ptr(); + unsigned speed = settings->uints.accessibility_narrator_speech_speed; + bool narrator_on = settings->bools.accessibility_enable; + + accessibility_speak_priority(narrator_on, speed, text, 10); #endif +} -#ifdef DEBUG - if (access_st->ai_service_auto != 2) - RARCH_LOG("RESULT FROM AI SERVICE...\n"); +/** + * Speaks the provided text using TTS. This only happens if the narrator has + * been enabled or the service is running in Narrator mode, in which case it + * must been used even if the user has disabled it. + */ +static void translation_speak(const char *text) +{ +#ifdef HAVE_ACCESSIBILITY + settings_t *settings = config_get_ptr(); + access_state_t *access_st = access_state_get_ptr(); + + unsigned mode = settings->uints.ai_service_mode; + unsigned speed = settings->uints.accessibility_narrator_speech_speed; + bool narrator_on = settings->bools.accessibility_enable; + + /* Force the use of the narrator in Narrator modes (TTS) */ + if (mode == 2 || mode == 4 || mode == 5 || narrator_on || access_st->enabled) + accessibility_speak_priority(true, speed, text, 10); #endif +} - if (!data || error || !data->data) - goto finish; - - if (!(json = rjson_open_buffer(data->data, data->len))) - goto finish; - - /* Parse JSON body for the image and sound data */ - for (;;) +/** + * Displays the given message on screen and returns true. Returns false if no + * {message} is provided (i.e. it is NULL). The message will be displayed as + * information or error depending on the {error} boolean. In addition, it will + * be logged if {error} is true, or if this is a debug build. The message will + * also be played by the accessibility narrator if the user enabled it. + */ +static bool translation_user_message(const char *message, bool error) +{ + if (message) { - static const char* keys[] = { "image", "sound", "text", "error", "auto", "press" }; - - const char *str = NULL; - size_t str_len = 0; - enum rjson_type json_type = rjson_next(json); - - if (json_type == RJSON_DONE || json_type == RJSON_ERROR) - break; - if (json_type != RJSON_STRING) - continue; - if (rjson_get_context_type(json) != RJSON_OBJECT) - continue; - str = rjson_get_string(json, &str_len); - - if ((rjson_get_context_count(json) & 1) == 1) - { - int i; - json_current_key = -1; - - for (i = 0; i < (int)ARRAY_SIZE(keys); i++) - { - if (string_is_equal(str, keys[i])) - { - json_current_key = i; - break; - } - } - } + accessibility_speak(message); + runloop_msg_queue_push( + message, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, + error ? MESSAGE_QUEUE_CATEGORY_ERROR : MESSAGE_QUEUE_CATEGORY_INFO); + if (error) + RARCH_ERR("[Translate] %s\n", message); +#ifdef DEBUG else - { - switch (json_current_key) - { - case 0: /* image */ - raw_image_file_data = (char*)unbase64(str, - (int)str_len, &new_image_size); - break; -#ifdef HAVE_AUDIOMIXER - case 1: /* sound */ - raw_sound_data = (void*)unbase64(str, - (int)str_len, &new_sound_size); - break; + RARCH_LOG("[Translate] %s\n", message); #endif - case 2: /* text */ - txt_str = strdup(str); - break; - case 3: /* error */ - err_str = strdup(str); - break; - case 4: /* auto */ - auto_str = strdup(str); - break; - case 5: /* press */ - key_str = strdup(str); - break; - } - json_current_key = -1; - } + return true; } + return false; +} - if (string_is_equal(err_str, "No text found.")) +/** + * Displays the given hash on screen and returns true. Returns false if no + * {hash} is provided (i.e. it is NULL). The message will be displayed as + * information or error depending on the {error} boolean. In addition, it will + * be logged if {error} is true, or if this is a debug build. The message will + * also be played by the accessibility narrator if the user enabled it. + */ +static bool translation_hash_message(enum msg_hash_enums hash, bool error) +{ + if (hash) { + const char *message = msg_hash_to_str(hash); + const char *intl = msg_hash_to_str_us(hash); + + accessibility_speak(message); + runloop_msg_queue_push( + message, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, + error ? MESSAGE_QUEUE_CATEGORY_ERROR : MESSAGE_QUEUE_CATEGORY_INFO); + if (error) + RARCH_ERR("[Translate] %s\n", intl); #ifdef DEBUG - RARCH_LOG("No text found...\n"); -#endif - if (txt_str) - { - free(txt_str); - txt_str = NULL; - } - - txt_str = (char*)malloc(15); - strlcpy(txt_str, err_str, 15); -#ifdef HAVE_GFX_WIDGETS - if (gfx_widgets_paused) - { - /* In this case we have to unpause and then repause for a frame */ - p_dispwidget->ai_service_overlay_state = 2; - command_event(CMD_EVENT_UNPAUSE, NULL); - } -#endif - } - - if ( !raw_image_file_data - && !raw_sound_data - && !txt_str - && !key_str - && (access_st->ai_service_auto != 2)) - { - error = "Invalid JSON body."; - goto finish; - } - - if (raw_image_file_data) - { - unsigned image_width, image_height; - /* Get the video frame dimensions reference */ - const void *dummy_data = video_st->frame_cache_data; - unsigned width = video_st->frame_cache_width; - unsigned height = video_st->frame_cache_height; - - /* try two different modes for text display * - * In the first mode, we use display widget overlays, but they require - * the video poke interface to be able to load image buffers. - * - * The other method is to draw to the video buffer directly, which needs - * a software core to be running. */ -#ifdef HAVE_GFX_WIDGETS - if ( video_st->poke - && video_st->poke->load_texture - && video_st->poke->unload_texture) - { - enum image_type_enum image_type; - /* Write to overlay */ - if ( raw_image_file_data[0] == 'B' - && raw_image_file_data[1] == 'M') - image_type = IMAGE_TYPE_BMP; - else if ( raw_image_file_data[1] == 'P' - && raw_image_file_data[2] == 'N' - && raw_image_file_data[3] == 'G') - image_type = IMAGE_TYPE_PNG; - else - { - RARCH_LOG("Invalid image type returned from server.\n"); - goto finish; - } - - if (!gfx_widgets_ai_service_overlay_load( - raw_image_file_data, (unsigned)new_image_size, - image_type)) - { - RARCH_LOG("Video driver not supported for AI Service."); - runloop_msg_queue_push( - /* msg_hash_to_str(MSG_VIDEO_DRIVER_NOT_SUPPORTED), */ - "Video driver not supported.", - 1, 180, true, - NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); - } - else if (gfx_widgets_paused) - { - /* In this case we have to unpause and then repause for a frame */ - /* Unpausing state */ - p_dispwidget->ai_service_overlay_state = 2; - command_event(CMD_EVENT_UNPAUSE, NULL); - } - } else + RARCH_LOG("[Translate] %s\n", intl); #endif - /* Can't use display widget overlays, so try writing to video buffer */ - { - size_t pitch; - /* Write to video buffer directly (software cores only) */ - - /* This is a BMP file coming back. */ - if ( raw_image_file_data[0] == 'B' - && raw_image_file_data[1] == 'M') - { - /* Get image data (24 bit), and convert to the emulated pixel format */ - image_width = - ((uint32_t) ((uint8_t)raw_image_file_data[21]) << 24) + - ((uint32_t) ((uint8_t)raw_image_file_data[20]) << 16) + - ((uint32_t) ((uint8_t)raw_image_file_data[19]) << 8) + - ((uint32_t) ((uint8_t)raw_image_file_data[18]) << 0); - - image_height = - ((uint32_t) ((uint8_t)raw_image_file_data[25]) << 24) + - ((uint32_t) ((uint8_t)raw_image_file_data[24]) << 16) + - ((uint32_t) ((uint8_t)raw_image_file_data[23]) << 8) + - ((uint32_t) ((uint8_t)raw_image_file_data[22]) << 0); - raw_image_data = (void*)malloc(image_width * image_height * 3 * sizeof(uint8_t)); - memcpy(raw_image_data, - raw_image_file_data + 54 * sizeof(uint8_t), - image_width * image_height * 3 * sizeof(uint8_t)); - } - /* PNG coming back from the url */ - else if (raw_image_file_data[1] == 'P' - && raw_image_file_data[2] == 'N' - && raw_image_file_data[3] == 'G') - { - int retval = 0; - rpng_t *rpng = NULL; - image_width = - ((uint32_t) ((uint8_t)raw_image_file_data[16]) << 24)+ - ((uint32_t) ((uint8_t)raw_image_file_data[17]) << 16)+ - ((uint32_t) ((uint8_t)raw_image_file_data[18]) << 8)+ - ((uint32_t) ((uint8_t)raw_image_file_data[19]) << 0); - image_height = - ((uint32_t) ((uint8_t)raw_image_file_data[20]) << 24)+ - ((uint32_t) ((uint8_t)raw_image_file_data[21]) << 16)+ - ((uint32_t) ((uint8_t)raw_image_file_data[22]) << 8)+ - ((uint32_t) ((uint8_t)raw_image_file_data[23]) << 0); - - if (!(rpng = rpng_alloc())) - { - error = "Can't allocate memory."; - goto finish; - } - - rpng_set_buf_ptr(rpng, raw_image_file_data, (size_t)new_image_size); - rpng_start(rpng); - while (rpng_iterate_image(rpng)); - - do - { - retval = rpng_process_image(rpng, &raw_image_data_alpha, - (size_t)new_image_size, &image_width, &image_height); - } while (retval == IMAGE_PROCESS_NEXT); - - /* Returned output from the png processor is an upside down RGBA - * image, so we have to change that to RGB first. This should - * probably be replaced with a scaler call.*/ - { - unsigned ui; - int tw, th, tc; - int d = 0; - raw_image_data = (void*)malloc(image_width*image_height*3*sizeof(uint8_t)); - for (ui = 0; ui < image_width * image_height * 4; ui++) - { - if (ui % 4 != 3) - { - tc = d % 3; - th = image_height-d / (image_width * 3) - 1; - tw = (d % (image_width * 3)) / 3; - ((uint8_t*) raw_image_data)[tw * 3 + th * 3 * image_width + tc] = ((uint8_t *)raw_image_data_alpha)[ui]; - d += 1; - } - } - } - rpng_free(rpng); - } - else - { - RARCH_LOG("Output from URL not a valid file type, or is not supported.\n"); - goto finish; - } - - if (!(scaler = (struct scaler_ctx*)calloc(1, sizeof(struct scaler_ctx)))) - goto finish; - - if (dummy_data == RETRO_HW_FRAME_BUFFER_VALID) - { - /* - In this case, we used the viewport to grab the image - and translate it, and we have the translated image in - the raw_image_data buffer. - */ - RARCH_LOG("Hardware frame buffer core, but selected video driver isn't supported.\n"); - goto finish; - } - - /* The assigned pitch may not be reliable. The width of - the video frame can change during run-time, but the - pitch may not, so we just assign it as the width - times the byte depth. - */ - - if (video_driver_pix_fmt == RETRO_PIXEL_FORMAT_XRGB8888) - { - raw_output_data = (uint8_t*)malloc(width * height * 4 * sizeof(uint8_t)); - scaler->out_fmt = SCALER_FMT_ARGB8888; - pitch = width * 4; - scaler->out_stride = (int)pitch; - } - else - { - raw_output_data = (uint8_t*)malloc(width * height * 2 * sizeof(uint8_t)); - scaler->out_fmt = SCALER_FMT_RGB565; - pitch = width * 2; - scaler->out_stride = width; - } - - if (!raw_output_data) - goto finish; - - scaler->in_fmt = SCALER_FMT_BGR24; - scaler->in_width = image_width; - scaler->in_height = image_height; - scaler->out_width = width; - scaler->out_height = height; - scaler->scaler_type = SCALER_TYPE_POINT; - scaler_ctx_gen_filter(scaler); - scaler->in_stride = -1 * width * 3; - - scaler_ctx_scale_direct(scaler, raw_output_data, - (uint8_t*)raw_image_data + (image_height - 1) * width * 3); - video_driver_frame(raw_output_data, image_width, image_height, pitch); - } + return true; } + return false; +} -#ifdef HAVE_AUDIOMIXER - if (raw_sound_data) - { - audio_mixer_stream_params_t params; +/** + * Displays the given message on screen and returns true. Returns false if no + * {message} is provided (i.e. it is NULL). The message will be displayed as + * an error and it will be logged. The message will also be played by the + * accessibility narrator if the user enabled it. + */ +static INLINE bool translation_user_error(const char *message) +{ + return translation_user_message(message, true); +} - params.volume = 1.0f; - params.slot_selection_type = AUDIO_MIXER_SLOT_SELECTION_MANUAL; /* user->slot_selection_type; */ - params.slot_selection_idx = 10; - params.stream_type = AUDIO_STREAM_TYPE_SYSTEM; /* user->stream_type; */ - params.type = AUDIO_MIXER_TYPE_WAV; - params.state = AUDIO_STREAM_STATE_PLAYING; - params.buf = raw_sound_data; - params.bufsize = new_sound_size; - params.cb = NULL; - params.basename = NULL; +/** + * Displays the given message on screen and returns true. Returns false if no + * {message} is provided (i.e. it is NULL). The message will be displayed as + * information and will only be logged if this is a debug build. The message + * will also be played by the accessibility narrator if the user enabled it. + */ +static INLINE bool translation_user_info(const char *message) +{ + return translation_user_message(message, false); +} - audio_driver_mixer_add_stream(¶ms); +/** + * Displays the given hash on screen and returns true. Returns false if no + * {hash} is provided (i.e. it is NULL). The message will be displayed as + * an error and it will be logged. The message will also be played by the + * accessibility narrator if the user enabled it. + */ +static INLINE bool translation_hash_error(enum msg_hash_enums hash) +{ + return translation_hash_message(hash, true); +} - if (raw_sound_data) - { - free(raw_sound_data); - raw_sound_data = NULL; - } - } +/** + * Displays the given hash on screen and returns true. Returns false if no + * {hash} is provided (i.e. it is NULL). The message will be displayed as + * information and will only be logged if this is a debug build. The message + * will also be played by the accessibility narrator if the user enabled it. + */ +static INLINE bool translation_hash_info(enum msg_hash_enums hash) +{ + return translation_hash_message(hash, false); +} + +/** + * Releases all data held by the service and stops it as soon as possible. + * If {inform} is true, a message will be displayed to the user if the service + * was running in automatic mode to warn them that it is now stopping. + */ +void translation_release(bool inform) +{ +#ifdef HAVE_GFX_WIDGETS + dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); +#endif + access_state_t *access_st = access_state_get_ptr(); + unsigned service_auto_prev = access_st->ai_service_auto; + access_st->ai_service_auto = 0; + +#ifdef DEBUG + RARCH_LOG("[Translate]: AI Service is now stopping.\n"); #endif - if (key_str) + if (access_st->request_task) + task_set_cancelled(access_st->request_task, true); + if (access_st->response_task) + task_set_cancelled(access_st->response_task, true); + +#ifdef HAVE_THREADS + if (access_st->image_lock) { - size_t i; - char key[8]; - size_t length = strlen(key_str); - size_t start = 0; - - for (i = 1; i < length; i++) - { - char t = key_str[i]; - if (i == length - 1 || t == ' ' || t == ',') - { - if (i == length - 1 && t != ' ' && t!= ',') - i++; - - if (i-start > 7) - { - start = i; - continue; - } - - strncpy(key, key_str + start, i-start); - key[i-start] = '\0'; - -#ifdef HAVE_ACCESSIBILITY - if (string_is_equal(key, "b")) - input_st->ai_gamepad_state[0] = 2; - if (string_is_equal(key, "y")) - input_st->ai_gamepad_state[1] = 2; - if (string_is_equal(key, "select")) - input_st->ai_gamepad_state[2] = 2; - if (string_is_equal(key, "start")) - input_st->ai_gamepad_state[3] = 2; - - if (string_is_equal(key, "up")) - input_st->ai_gamepad_state[4] = 2; - if (string_is_equal(key, "down")) - input_st->ai_gamepad_state[5] = 2; - if (string_is_equal(key, "left")) - input_st->ai_gamepad_state[6] = 2; - if (string_is_equal(key, "right")) - input_st->ai_gamepad_state[7] = 2; - - if (string_is_equal(key, "a")) - input_st->ai_gamepad_state[8] = 2; - if (string_is_equal(key, "x")) - input_st->ai_gamepad_state[9] = 2; - if (string_is_equal(key, "l")) - input_st->ai_gamepad_state[10] = 2; - if (string_is_equal(key, "r")) - input_st->ai_gamepad_state[11] = 2; - - if (string_is_equal(key, "l2")) - input_st->ai_gamepad_state[12] = 2; - if (string_is_equal(key, "r2")) - input_st->ai_gamepad_state[13] = 2; - if (string_is_equal(key, "l3")) - input_st->ai_gamepad_state[14] = 2; - if (string_is_equal(key, "r3")) - input_st->ai_gamepad_state[15] = 2; + slock_lock(access_st->image_lock); #endif - - if (string_is_equal(key, "pause")) - command_event(CMD_EVENT_PAUSE, NULL); - if (string_is_equal(key, "unpause")) - command_event(CMD_EVENT_UNPAUSE, NULL); - - start = i+1; - } - } + if (access_st->last_image) + free(access_st->last_image); + + access_st->last_image = NULL; + access_st->last_image_size = 0; + +#ifdef HAVE_THREADS + slock_unlock(access_st->image_lock); } - -#ifdef HAVE_ACCESSIBILITY - if ( txt_str - && is_accessibility_enabled( - accessibility_enable, - access_st->enabled)) - accessibility_speak_priority( - accessibility_enable, - accessibility_narrator_speech_speed, - txt_str, 10); #endif -finish: - if (error) - RARCH_ERR("%s: %s\n", msg_hash_to_str(MSG_DOWNLOAD_FAILED), error); - - if (user_data) - free(user_data); - - if (json) - rjson_free(json); - if (raw_image_file_data) - free(raw_image_file_data); - if (raw_image_data_alpha) - free(raw_image_data_alpha); - if (raw_image_data) - free(raw_image_data); - if (scaler) - free(scaler); - if (err_str) - free(err_str); - if (txt_str) - free(txt_str); - if (raw_output_data) - free(raw_output_data); +#ifdef HAVE_GFX_WIDGETS + if (p_dispwidget->ai_service_overlay_state != 0) + gfx_widgets_ai_service_overlay_unload(); +#endif - if (auto_str) - { - if (string_is_equal(auto_str, "auto")) - { - bool was_paused = (runloop_flags & RUNLOOP_FLAG_PAUSED) ? true : false; - if ( (access_st->ai_service_auto != 0) - && !settings->bools.ai_service_pause) - call_auto_translate_task(settings, &was_paused); - } - free(auto_str); - } - if (key_str) - free(key_str); + if (inform && service_auto_prev != 0) + translation_hash_info(MSG_AI_AUTO_MODE_DISABLED); } -static const char *ai_service_get_str(enum translation_lang id) +/** + * Returns the string representation of the translation language enum value. + */ +static const char* ai_service_get_str(enum translation_lang id) { switch (id) { @@ -752,6 +456,8 @@ static const char *ai_service_get_str(enum translation_lang id) return "tr"; case TRANSLATION_LANG_UK: return "uk"; + case TRANSLATION_LANG_BE: + return "be"; case TRANSLATION_LANG_UR: return "ur"; case TRANSLATION_LANG_VI: @@ -768,120 +474,790 @@ static const char *ai_service_get_str(enum translation_lang id) return ""; } -bool run_translation_service(settings_t *settings, bool paused) -{ - struct video_viewport vp; - uint8_t header[54]; - size_t pitch; - unsigned width, height; - const void *data = NULL; - uint8_t *bit24_image = NULL; - uint8_t *bit24_image_prev = NULL; - struct scaler_ctx *scaler = (struct scaler_ctx*) - calloc(1, sizeof(struct scaler_ctx)); - bool error = false; - - uint8_t *bmp_buffer = NULL; - uint64_t buffer_bytes = 0; - char *bmp64_buffer = NULL; - rjsonwriter_t *jsonwriter = NULL; - const char *json_buffer = NULL; - int bmp64_length = 0; - bool TRANSLATE_USE_BMP = false; - char *sys_lbl = NULL; - core_info_t *core_info = NULL; - video_driver_state_t *video_st = video_state_get_ptr(); - access_state_t *access_st = access_state_get_ptr(); -#ifdef HAVE_ACCESSIBILITY - input_driver_state_t *input_st = input_state_get_ptr(); -#endif -#ifdef HAVE_GFX_WIDGETS - dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); - /* For the case when ai service pause is disabled. */ - if ( (p_dispwidget->ai_service_overlay_state != 0) - && (access_st->ai_service_auto == 1)) - { - gfx_widgets_ai_service_overlay_unload(); - goto finish; - } -#endif +/* AUTOMATION --------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/** + * Handler invoking the next automatic request. This method simply waits for + * any previous request to terminate before re-invoking the translation service. + * By delegating this to a task handler we can safely do so in the task thread + * instead of hogging the main thread. + */ +static void call_auto_translate_hndl(retro_task_t *task) +{ + int *mode_ptr = (int*)task->user_data; + uint32_t runloop_flags = runloop_get_flags(); + access_state_t *access_st = access_state_get_ptr(); + settings_t *settings = config_get_ptr(); - /* get the core info here so we can pass long the game name */ - core_info_get_current_core(&core_info); + if (task_get_cancelled(task)) + goto finish; - if (core_info) + switch (*mode_ptr) { - size_t lbl_len; - const char *lbl = NULL; - const char *sys_id = core_info->system_id - ? core_info->system_id : "core"; - size_t sys_id_len = strlen(sys_id); - const struct playlist_entry *entry = NULL; - playlist_t *current_playlist = playlist_get_cached(); + case 1: /* Speech Mode */ +#ifdef HAVE_AUDIOMIXER + if (!audio_driver_is_ai_service_speech_running()) + goto finish; +#endif + break; + case 2: /* Narrator Mode */ + case 3: /* Text Mode */ + case 4: /* Text + Narrator */ + case 5: /* Image + Narrator */ +#ifdef HAVE_ACCESSIBILITY + if (!is_narrator_running(settings->bools.accessibility_enable)) + goto finish; +#endif + break; + default: + goto finish; + } + return; - if (current_playlist) +finish: + task_set_finished(task, true); + + if (task->user_data) + free(task->user_data); + + /* Final check to see if the user did not disable the service altogether */ + if (access_st->ai_service_auto != 0) + { + bool was_paused = runloop_flags & RUNLOOP_FLAG_PAUSED; + command_event(CMD_EVENT_AI_SERVICE_CALL, &was_paused); + } +} + +/** + * Invokes the next automatic request. This method delegates the invokation to + * a task to allow for threading. The task will only execute after the polling + * delay configured by the user has been honored since the last request. + */ +static void call_auto_translate_task(settings_t *settings) +{ + int* mode = NULL; + access_state_t *access_st = access_state_get_ptr(); + int ai_service_mode = settings->uints.ai_service_mode; + unsigned delay = settings->uints.ai_service_poll_delay; + retro_task_t *task = task_init(); + if (!task) + return; + + mode = (int*)malloc(sizeof(int)); + *mode = ai_service_mode; + + task->handler = call_auto_translate_hndl; + task->user_data = mode; + task->mute = true; + task->when = access_st->last_call + (delay * 1000); + task_queue_push(task); +} + +/* RESPONSE ----------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ + +/** + * Parses the JSON returned by the translation server and returns structured + * data. May return NULL if the parsing cannot be completed or the JSON is + * malformed. If unsupported keys are provided in the JSON, they will simply + * be ignored. Only the available data will be populated in the returned object + * and everything else will be zero-initialized. + */ +static access_response_t* parse_response_json(http_transfer_data_t *data) +{ + int key = -1; + rjson_t* json = NULL; + char* image_data = NULL; + int image_size = 0; +#ifdef HAVE_AUDIOMIXER + void *sound_data = NULL; + int sound_size = 0; +#endif + access_response_t *response = NULL; + bool empty = true; + enum rjson_type type; + + if (!data || !data->data) + goto finish; + if (!(json = rjson_open_buffer(data->data, data->len))) + goto finish; + if (!(response = (access_response_t*)calloc(1, sizeof(access_response_t)))) + goto finish; + + for (;;) + { + size_t length = 0; + const char *string = NULL; + type = rjson_next(json); + + if (type == RJSON_DONE || type == RJSON_ERROR) + break; + if (rjson_get_context_type(json) != RJSON_OBJECT) + continue; + + if (type == RJSON_STRING && (rjson_get_context_count(json) & 1) == 1) { - playlist_get_index_by_path( - current_playlist, path_get(RARCH_PATH_CONTENT), &entry); + int i; + string = rjson_get_string(json, &length); + for (i = 0; i < ARRAY_SIZE(ACCESS_RESPONSE_KEYS) && key == -1; i++) + { + if (string_is_equal(string, ACCESS_RESPONSE_KEYS[i])) + key = i; + } + } + else + { + if (type != RJSON_STRING && key < 6) + continue; + else + string = rjson_get_string(json, &length); + + switch (key) + { + case 0: /* image */ + response->image = (length == 0) ? NULL : (char*)unbase64( + string, (int)length, &response->image_size); + break; +#ifdef HAVE_AUDIOMIXER + case 1: /* sound */ + response->sound = (length == 0) ? NULL : (void*)unbase64( + string, (int)length, &response->sound_size); + break; +#endif + case 2: /* text */ + response->text = strdup(string); + break; + case 3: /* error */ + response->error = strdup(string); + break; + case 4: /* auto */ + response->recall = strdup(string); + break; + case 5: /* press */ + response->input = strdup(string); + break; + case 6: /* text_position */ + if (type == RJSON_NUMBER) + response->text_position = rjson_get_int(json); + break; + } + key = -1; + } + } + + if (type == RJSON_ERROR) + { + RARCH_LOG("[Translate] JSON error: %s\n", rjson_get_error(json)); + translation_user_error("Service returned a malformed JSON"); + free(response); + response = NULL; + } + +finish: + if (json) + rjson_free(json); + else + translation_user_error("Internal error parsing returned JSON."); + + return response; +} - if (entry && !string_is_empty(entry->label)) - lbl = entry->label; +/** + * Parses the image data of given type and displays it using widgets. If the + * image widget is already shown, it will be unloaded first automatically. + * This method will disable automatic translation if the widget could not be + * loaded to prevent further errors. + */ +#ifdef HAVE_GFX_WIDGETS +static void translation_response_image_widget( + char *image, int image_length, enum image_type_enum *image_type) +{ + video_driver_state_t *video_st = video_state_get_ptr(); + dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); + access_state_t *access_st = access_state_get_ptr(); + + bool ai_res; + bool gfx_widgets_paused = video_st->flags & VIDEO_FLAG_WIDGETS_PAUSED; + + if (p_dispwidget->ai_service_overlay_state != 0) + gfx_widgets_ai_service_overlay_unload(); + + ai_res = gfx_widgets_ai_service_overlay_load( + image, (unsigned)image_length, (*image_type)); + + if (!ai_res) + { + translation_hash_error(MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED); + translation_release(true); + } + else if (gfx_widgets_paused) + { + /* Unpause for a frame otherwise widgets won't be displayed */ + p_dispwidget->ai_service_overlay_state = 2; + command_event(CMD_EVENT_UNPAUSE, NULL); + } +} +#endif + +/** + * Parses the image buffer, converting the data to the raw image format we need + * to display the image within RetroArch. Writes the raw image data in {body} + * as well as its {width} and {height} as determined by the image header. + * Returns true if the process was successful. + */ +static bool translation_get_image_body( + char *image, int image_size, enum image_type_enum *image_type, + void *body, unsigned *width, unsigned *height) +{ +#ifdef HAVE_RPNG + rpng_t *rpng = NULL; + void *rpng_alpha = NULL; + int rpng_ret = 0; +#endif + + if ((*image_type) == IMAGE_TYPE_BMP) + { + if (image_size < 55) + return false; + + *width = ((uint32_t) ((uint8_t)image[21]) << 24) + + ((uint32_t) ((uint8_t)image[20]) << 16) + + ((uint32_t) ((uint8_t)image[19]) << 8) + + ((uint32_t) ((uint8_t)image[18]) << 0); + *height = ((uint32_t) ((uint8_t)image[25]) << 24) + + ((uint32_t) ((uint8_t)image[24]) << 16) + + ((uint32_t) ((uint8_t)image[23]) << 8) + + ((uint32_t) ((uint8_t)image[22]) << 0); + + image_size = (*width) * (*height) * 3 * sizeof(uint8_t); + body = (void*)malloc(image_size); + if (!body) + return false; + + memcpy(body, image + 54 * sizeof(uint8_t), image_size); + return true; + } + +#ifdef HAVE_RPNG + else if ((*image_type) == IMAGE_TYPE_PNG) + { + if (image_size < 24) + return false; + if (!(rpng = rpng_alloc())) + return false; + + *width = ((uint32_t) ((uint8_t)image[16]) << 24) + + ((uint32_t) ((uint8_t)image[17]) << 16) + + ((uint32_t) ((uint8_t)image[18]) << 8) + + ((uint32_t) ((uint8_t)image[19]) << 0); + *height = ((uint32_t) ((uint8_t)image[20]) << 24) + + ((uint32_t) ((uint8_t)image[21]) << 16) + + ((uint32_t) ((uint8_t)image[22]) << 8) + + ((uint32_t) ((uint8_t)image[23]) << 0); + + rpng_set_buf_ptr(rpng, image, (size_t)image_size); + rpng_start(rpng); + while (rpng_iterate_image(rpng)); + + do + { + rpng_ret = rpng_process_image( + rpng, &rpng_alpha, (size_t)image_size, width, height); + } while (rpng_ret == IMAGE_PROCESS_NEXT); + + /* + * Returned output from the png processor is an upside down RGBA + * image, so we have to change that to RGB first. This should + * probably be replaced with a scaler call. + */ + { + int d = 0; + int tw, th, tc; + unsigned ui; + image_size = (*width) * (*height) * 3 * sizeof(uint8_t); + body = (void*)malloc(image_size); + if (!body) + { + free(rpng_alpha); + rpng_free(rpng); + return false; + } + + for (ui = 0; ui < (*width) * (*height) * 4; ui++) + { + if (ui % 4 != 3) + { + tc = d % 3; + th = (*height) - d / (3 * (*width)) - 1; + tw = (d % ((*width) * 3)) / 3; + ((uint8_t*) body)[tw * 3 + th * 3 * (*width) + tc] + = ((uint8_t*)rpng_alpha)[ui]; + d++; + } + } } + free(rpng_alpha); + rpng_free(rpng); + return true; + } +#endif + + return false; +} + +/** + * Displays the raw image on screen by directly writing to the frame buffer. + * This method may fail depending on the current video driver. + */ + /* TODO/FIXME: Does nothing with Vulkan apparently? */ +static void translation_response_image_direct( + char *image, int image_size, enum image_type_enum *image_type) +{ + size_t pitch; + unsigned width; + unsigned height; + unsigned vp_width; + unsigned vp_height; + + void *image_body = NULL; + uint8_t *raw_output_data = NULL; + size_t raw_output_size = 0; + const void *dummy_data = NULL; + struct scaler_ctx *scaler = NULL; + video_driver_state_t *video_st = video_state_get_ptr(); + const enum retro_pixel_format video_driver_pix_fmt = video_st->pix_fmt; + + if (!(translation_get_image_body( + image, image_size, image_type, image_body, &width, &height))) + goto finish; + + if (!(scaler = (struct scaler_ctx*)calloc(1, sizeof(struct scaler_ctx)))) + goto finish; + + dummy_data = video_st->frame_cache_data; + vp_width = video_st->frame_cache_width; + vp_height = video_st->frame_cache_height; + pitch = video_st->frame_cache_pitch; - if (!lbl) - lbl = path_basename(path_get(RARCH_PATH_BASENAME)); - lbl_len = strlen(lbl); - sys_lbl = (char*)malloc(lbl_len + sys_id_len + 3); - memcpy(sys_lbl, sys_id, sys_id_len); - memcpy(sys_lbl + sys_id_len, "__", 2); - memcpy(sys_lbl + 2 + sys_id_len, lbl, lbl_len); - sys_lbl[sys_id_len + 2 + lbl_len] = '\0'; + if (!vp_width || !vp_height) + goto finish; + + if (dummy_data == RETRO_HW_FRAME_BUFFER_VALID) + { + /* In this case, we used the viewport to grab the image and translate it, + * and we have the translated image in the image_body buffer. */ + translation_user_error("Video driver unsupported for hardware frame."); + translation_release(true); + goto finish; } - if (!scaler) + /* + * The assigned pitch may not be reliable. The width of the video frame can + * change during run-time, but the pitch may not, so we just assign it as + * the width times the byte depth. + */ + if (video_driver_pix_fmt == RETRO_PIXEL_FORMAT_XRGB8888) + { + raw_output_size = vp_width * vp_height * 4 * sizeof(uint8_t); + raw_output_data = (uint8_t*)malloc(raw_output_size); + scaler->out_fmt = SCALER_FMT_ARGB8888; + scaler->out_stride = vp_width * 4; + pitch = vp_width * 4; + } + else + { + raw_output_size = vp_width * vp_height * 2 * sizeof(uint8_t); + raw_output_data = (uint8_t*)malloc(raw_output_size); + scaler->out_fmt = SCALER_FMT_RGB565; + scaler->out_stride = vp_width * 1; + pitch = vp_width * 2; + } + + if (!raw_output_data) goto finish; - data = video_st->frame_cache_data; - width = video_st->frame_cache_width; - height = video_st->frame_cache_height; - pitch = video_st->frame_cache_pitch; + scaler->in_fmt = SCALER_FMT_BGR24; + scaler->in_width = width; + scaler->in_height = height; + scaler->out_width = vp_width; + scaler->out_height = vp_height; + scaler->scaler_type = SCALER_TYPE_POINT; + scaler_ctx_gen_filter(scaler); + + scaler->in_stride = -1 * vp_width * 3; + + scaler_ctx_scale_direct( + scaler, raw_output_data, + (uint8_t*)image_body + (height - 1) * width * 3); + video_driver_frame(raw_output_data, width, height, pitch); + +finish: + if (image_body) + free(image_body); + if (scaler) + free(scaler); + if (raw_output_data) + free(raw_output_data); +} + +/** + * Parses image data received by the server following a translation request. + * This method assumes that image data is present in the response, it cannot + * be null. If widgets are supported, this method will prefer using them to + * overlay the picture on top of the video, otherwise it will try to write the + * data directly into the frame buffer, which is much less reliable. + */ +static void translation_response_image_hndl(retro_task_t *task) +{ + /* + * TODO/FIXME: Moved processing to the callback to fix an issue with + * texture loading off the main thread in OpenGL. I'm leaving the original + * structure here so we can move back to the handler if it becomes possible + * in the future. + */ + task_set_finished(task, true); +} - if (!data) +/** + * Callback invoked once the image data received from the server has been + * processed and eventually displayed. This is necessary to ensure that the + * next automatic request will be invoked once the task is finished. + */ +static void translation_response_image_cb( + retro_task_t *task, void *task_data, void *user_data, const char *error) +{ + settings_t* settings = config_get_ptr(); + access_state_t *access_st = access_state_get_ptr(); + + enum image_type_enum image_type; + access_response_t *response = (access_response_t*)task->user_data; + video_driver_state_t *video_st = video_state_get_ptr(); + + if (task_get_cancelled(task) || response->image_size < 4) + goto finish; + + if ( response->image[0] == 'B' + && response->image[1] == 'M') + image_type = IMAGE_TYPE_BMP; +#ifdef HAVE_RPNG + else if (response->image[1] == 'P' + && response->image[2] == 'N' + && response->image[3] == 'G') + image_type = IMAGE_TYPE_PNG; +#endif + else + { + translation_user_error("Service returned an unsupported image type."); + translation_release(true); goto finish; + } + +#ifdef HAVE_GFX_WIDGETS + if ( video_st->poke + && video_st->poke->load_texture + && video_st->poke->unload_texture) + translation_response_image_widget( + response->image, response->image_size, &image_type); + else +#endif + translation_response_image_direct( + response->image, response->image_size, &image_type); + +finish: + free(response->image); + free(response); - if (data == RETRO_HW_FRAME_BUFFER_VALID) + if (access_st->ai_service_auto != 0) + call_auto_translate_task(settings); +} + +/** + * Processes text data received by the server following a translation request. + * Does nothing if the response does not contain any text data (NULL). Text + * is either forcibly read by the narrator, even if it is disabled in the + * front-end (Narrator Mode) or displayed on screen (in Text Mode). In the + * later, it will only be read if the front-end narrator is enabled. + */ +static void translation_response_text(access_response_t *response) +{ + settings_t *settings = config_get_ptr(); + unsigned service_mode = settings->uints.ai_service_mode; + access_state_t *access_st = access_state_get_ptr(); + + if ( (!response->text || string_is_empty(response->text)) + && (service_mode == 2 || service_mode == 3 || service_mode == 4) + && access_st->ai_service_auto == 0) { - /* - The direct frame capture didn't work, so try getting it - from the viewport instead. This isn't as good as the - raw frame buffer, since the viewport may us bilinear - filtering, or other shaders that will completely trash - the OCR, but it's better than nothing. - */ - vp.x = 0; - vp.y = 0; - vp.width = 0; - vp.height = 0; - vp.full_width = 0; - vp.full_height = 0; - - video_driver_get_viewport_info(&vp); - - if (!vp.width || !vp.height) - goto finish; + translation_hash_info(MSG_AI_NOTHING_TO_TRANSLATE); + return; + } + + if (response->text) + { + /* The text should be displayed on screen in Text or Text+Narrator mode */ + if (service_mode == 3 || service_mode == 4) + { +#ifdef HAVE_GFX_WIDGETS + if (settings->bools.menu_enable_widgets) + { + dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); + + if (p_dispwidget->ai_service_overlay_state == 1) + gfx_widgets_ai_service_overlay_unload(); + + strlcpy(p_dispwidget->ai_service_text, response->text, 255); + + if (response->text_position > 0) + p_dispwidget->ai_service_text_position + = (unsigned)response->text_position; + else + p_dispwidget->ai_service_text_position = 0; + + p_dispwidget->ai_service_overlay_state = 1; + } + else + { +#endif + /* + * TODO/FIXME: Obviously this will not be as good as using widgets, + * since messages run on a timer but it's an alternative at least. + * Maybe split the message here so it fits the viewport. + */ + runloop_msg_queue_push( + response->text, 2, 180, + true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, + MESSAGE_QUEUE_CATEGORY_INFO); + +#ifdef HAVE_GFX_WIDGETS + } +#endif + } + translation_speak(&response->text[0]); + free(response->text); + } +} + +/** + * Processes audio data received by the server following a translation request. + * Does nothing if the response does not contain any audio data (NULL). Audio + * data is simply played as soon as possible using the audio driver. + */ +static void translation_response_sound(access_response_t *response) +{ +#ifdef HAVE_AUDIOMIXER + if (response->sound) + { + audio_mixer_stream_params_t params; - bit24_image_prev = (uint8_t*)malloc(vp.width * vp.height * 3); - bit24_image = (uint8_t*)malloc(width * height * 3); + params.volume = 1.0f; + /* user->slot_selection_type; */ + params.slot_selection_type = AUDIO_MIXER_SLOT_SELECTION_MANUAL; + params.slot_selection_idx = 10; + /* user->stream_type; */ + params.stream_type = AUDIO_STREAM_TYPE_SYSTEM; + params.type = AUDIO_MIXER_TYPE_WAV; + params.state = AUDIO_STREAM_STATE_PLAYING; + params.buf = response->sound; + params.bufsize = response->sound_size; + params.cb = NULL; + params.basename = NULL; + + audio_driver_mixer_add_stream(¶ms); + free(response->sound); + } +#endif +} + +/** + * Processes input data received by the server following a translation request. + * Does nothing if the response does not contain any input data (NULL). This + * method will try to forcibly press all the retropad keys listed in the input + * string (comma-separated). + */ +static void translation_response_input(access_response_t *response) +{ + if (response->input) + { +#ifdef HAVE_ACCESSIBILITY + input_driver_state_t *input_st = input_state_get_ptr(); +#endif + int length = strlen(response->input); + char *token = strtok(response->input, ","); + + while (token) + { + if (string_is_equal(token, "pause")) + command_event(CMD_EVENT_PAUSE, NULL); + else if (string_is_equal(token, "unpause")) + command_event(CMD_EVENT_UNPAUSE, NULL); +#ifdef HAVE_ACCESSIBILITY + else + { + int i = 0; + bool found = false; + + for (; i < ARRAY_SIZE(ACCESS_INPUT_LABELS) && !found; i++) + found = string_is_equal(ACCESS_INPUT_LABELS[i], response->input); + + if (found) + input_st->ai_gamepad_state[i] = 2; + } +#endif + token = strtok(NULL, ","); + } + free(response->input); + } +} + +/** + * Callback invoked when the server responds to our translation request. If the + * service is still running by then, this method will parse the JSON payload + * and process the data, eventually re-invoking the translation service for + * a new request if the server allowed automatic translation. + */ +static void translation_response_cb( + retro_task_t *task, void *task_data, void *user_data, const char *error) +{ + http_transfer_data_t *data = (http_transfer_data_t*)task_data; + access_state_t *access_st = access_state_get_ptr(); + settings_t *settings = config_get_ptr(); + access_response_t *response = NULL; + bool auto_mode_prev = access_st->ai_service_auto; + unsigned service_mode = settings->uints.ai_service_mode; + + /* We asked the service to stop by calling translation_release, so bail */ + if (!access_st->last_image) + goto finish; + if (translation_user_error(error)) + goto abort; + if (!(response = parse_response_json(data))) + goto abort; + if (translation_user_error(response->error)) + goto abort; + + access_st->ai_service_auto = (response->recall == NULL) ? 0 : 1; + if (auto_mode_prev != access_st->ai_service_auto) + translation_hash_info(auto_mode_prev + ? MSG_AI_AUTO_MODE_DISABLED : MSG_AI_AUTO_MODE_ENABLED); + + /* + * We want to skip the data on auto=continue, unless automatic translation + * has just been enabled, meaning data must be displayed again to the user. + */ + if ( !string_is_equal(response->recall, "continue") + || (auto_mode_prev == 0 && access_st->ai_service_auto == 1)) + { +#ifdef HAVE_GFX_WIDGETS + dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr(); + if (p_dispwidget->ai_service_overlay_state != 0) + gfx_widgets_ai_service_overlay_unload(); +#endif + translation_response_text(response); + translation_response_sound(response); + translation_response_input(response); + + if (response->image) + { + retro_task_t *task = task_init(); + if (!task) + goto finish; + + task->handler = translation_response_image_hndl; + task->callback = translation_response_image_cb; + task->user_data = response; + task->mute = true; + access_st->response_task = task; + task_queue_push(task); + + /* Leave memory clean-up and auto callback to the task itself */ + return; + } + else if (access_st->ai_service_auto == 0 + && (service_mode == 0 || service_mode == 5)) + translation_hash_info(MSG_AI_NOTHING_TO_TRANSLATE); + } + goto finish; + +abort: + translation_release(true); + if (response && response->error) + free(response->error); + +finish: + if (response) + { + if (response->image) + free(response->image); + if (response->recall) + free(response->recall); + free(response); + + if (access_st->ai_service_auto != 0) + call_auto_translate_task(settings); + } +} - if (!bit24_image_prev || !bit24_image) +/* REQUEST ------------------------------------------------------------------ */ +/* -------------------------------------------------------------------------- */ + +/** + * Grabs and returns a frame from the video driver. If the frame buffer cannot + * be accessed, this method will try to obtain a capture of the viewport as a + * fallback, although this frame may be altered by any filter or shader enabled + * by the user. Returns null if both methods fail. + */ +static access_frame_t* translation_grab_frame() +{ + size_t pitch; + struct video_viewport vp = {0}; + const void *data = NULL; + uint8_t *bit24_image_prev = NULL; + struct scaler_ctx *scaler = NULL; + access_frame_t *frame = NULL; + video_driver_state_t *video_st = video_state_get_ptr(); + const enum retro_pixel_format pix_fmt = video_st->pix_fmt; + + if (!(scaler = (struct scaler_ctx*)calloc(1, sizeof(struct scaler_ctx)))) + goto finish; + if (!(frame = (access_frame_t*)malloc(sizeof(access_frame_t)))) + goto finish; + + data = video_st->frame_cache_data; + frame->width = video_st->frame_cache_width; + frame->height = video_st->frame_cache_height; + pitch = video_st->frame_cache_pitch; + + if (!data) + goto finish; + + video_driver_get_viewport_info(&vp); + if (!vp.width || !vp.height) + goto finish; + + frame->content_x = vp.x; + frame->content_y = vp.y; + frame->content_width = vp.width; + frame->content_height = vp.height; + frame->viewport_width = vp.full_width; + frame->viewport_height = vp.full_height; + frame->size = frame->width * frame->height * 3; + + if (!(frame->data = (uint8_t*)malloc(frame->size))) + goto finish; + + if (data == RETRO_HW_FRAME_BUFFER_VALID) + { + /* Direct frame capture failed, fallback on viewport capture */ + if (!(bit24_image_prev = (uint8_t*)malloc(vp.width * vp.height * 3))) goto finish; if (!( video_st->current_video->read_viewport && video_st->current_video->read_viewport( video_st->data, bit24_image_prev, false))) { - RARCH_LOG("Could not read viewport for translation service...\n"); + translation_user_error("Could not read viewport."); + translation_release(true); goto finish; } @@ -891,275 +1267,535 @@ bool run_translation_service(settings_t *settings, bool paused) scaler->scaler_type = SCALER_TYPE_POINT; scaler->in_width = vp.width; scaler->in_height = vp.height; - scaler->out_width = width; - scaler->out_height = height; + scaler->out_width = frame->width; + scaler->out_height = frame->height; scaler_ctx_gen_filter(scaler); - scaler->in_stride = vp.width*3; - scaler->out_stride = width*3; - scaler_ctx_scale_direct(scaler, bit24_image, bit24_image_prev); + scaler->in_stride = vp.width * 3; + scaler->out_stride = frame->width * 3; + scaler_ctx_scale_direct(scaler, frame->data, bit24_image_prev); } else { - const enum retro_pixel_format - video_driver_pix_fmt = video_st->pix_fmt; - /* This is a software core, so just change the pixel format to 24-bit. */ - if (!(bit24_image = (uint8_t*)malloc(width * height * 3))) - goto finish; - - if (video_driver_pix_fmt == RETRO_PIXEL_FORMAT_XRGB8888) + /* This is a software core, so just change the pixel format to 24-bit */ + if (pix_fmt == RETRO_PIXEL_FORMAT_XRGB8888) scaler->in_fmt = SCALER_FMT_ARGB8888; else scaler->in_fmt = SCALER_FMT_RGB565; + video_frame_convert_to_bgr24( - scaler, - (uint8_t *)bit24_image, - (const uint8_t*)data + ((int)height - 1)*pitch, - width, height, - (int)-pitch); + scaler, frame->data, (const uint8_t*)data, + frame->width, frame->height, (int)pitch); } scaler_ctx_gen_reset(scaler); + +finish: + if (bit24_image_prev) + free(bit24_image_prev); + if (scaler) + free(scaler); - if (!bit24_image) + if (frame) { - error = true; - goto finish; + if (frame->data) + return frame; + else + free(frame); } + return NULL; +} - if (TRANSLATE_USE_BMP) +/** + * Returns true if the {frame} passed in parameter is a duplicate of the last + * frame the service was invoked on. This method effectively helps to prevent + * the service from spamming the server with the same request over and over + * again when running in automatic mode. This method will also save the image + * in the {frame} structure as the new last image for the service. + */ +static bool translation_dupe_fail(access_frame_t *frame) +{ + access_state_t *access_st = access_state_get_ptr(); + bool size_equal = (frame->size == access_st->last_image_size); + bool has_failed = false; + +#ifdef HAVE_THREADS + slock_lock(access_st->image_lock); +#endif + if (access_st->last_image && access_st->ai_service_auto != 0) { - /* - At this point, we should have a screenshot in the buffer, - so allocate an array to contain the BMP image along with - the BMP header as bytes, and then covert that to a - b64 encoded array for transport in JSON. - */ - form_bmp_header(header, width, height, false); - if (!(bmp_buffer = (uint8_t*)malloc(width * height * 3 + 54))) - goto finish; - - memcpy(bmp_buffer, header, 54 * sizeof(uint8_t)); - memcpy(bmp_buffer + 54, - bit24_image, - width * height * 3 * sizeof(uint8_t)); - buffer_bytes = sizeof(uint8_t) * (width * height * 3 + 54); + if ( size_equal + && u8_array_equal(frame->data, access_st->last_image, frame->size)) + has_failed = true; } - else + + /* Init last image or reset buffer size if image size changed */ + if (!has_failed && (!access_st->last_image || !size_equal)) { - pitch = width * 3; - bmp_buffer = rpng_save_image_bgr24_string( - bit24_image + width * (height-1) * 3, - width, height, (signed)-pitch, &buffer_bytes); + if (access_st->last_image) + free(access_st->last_image); + + access_st->last_image_size = frame->size; + if (!(access_st->last_image = (uint8_t*)malloc(frame->size))) + has_failed = true; } + + if (!has_failed) + memcpy(access_st->last_image, frame->data, frame->size); - if (!(bmp64_buffer = base64((void *)bmp_buffer, - (int)(sizeof(uint8_t) * buffer_bytes), - &bmp64_length))) - goto finish; +#ifdef HAVE_THREADS + slock_unlock(access_st->image_lock); +#endif + return has_failed; +} - if (!(jsonwriter = rjsonwriter_open_memory())) +/** + * Converts and returns the {frame} as a base64 encoded PNG or BMP. The + * selected image type will be available in the returned object, and will + * favor PNG if possible. Returns NULL on failure. + */ +static access_base64_t* translation_frame_encode(access_frame_t *frame) +{ + uint8_t header[54]; + uint8_t *buffer = NULL; + uint64_t bytes = 0; + access_base64_t *encode = NULL; + + if (!(encode = (access_base64_t*)malloc(sizeof(access_base64_t)))) + goto finish; + +#ifdef HAVE_RPNG + strcpy(encode->format, "png"); + buffer = rpng_save_image_bgr24_string( + frame->data, frame->width, frame->height, + frame->width * 3, &bytes); +#else + strcpy(encode->format, "bmp"); + form_bmp_header(header, frame->width, frame->height, false); + if (!(buffer = (uint8_t*)malloc(frame->size + 54))) goto finish; - rjsonwriter_raw(jsonwriter, "{", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string(jsonwriter, "image"); - rjsonwriter_raw(jsonwriter, ":", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string_len(jsonwriter, bmp64_buffer, bmp64_length); + memcpy(buffer, header, 54 * sizeof(uint8_t)); + memcpy(buffer + 54, frame->data, frame->size * sizeof(uint8_t)); + bytes = sizeof(uint8_t) * (frame->size + 54); +#endif - /* Form request... */ - if (sys_lbl) - { - rjsonwriter_raw(jsonwriter, ",", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string(jsonwriter, "label"); - rjsonwriter_raw(jsonwriter, ":", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string(jsonwriter, sys_lbl); - } + encode->data = base64( + (void*)buffer, (int)(bytes * sizeof(uint8_t)), &encode->length); + +finish: + if (buffer) + free(buffer); + + if (encode->data) + return encode; + else + free(encode); - rjsonwriter_raw(jsonwriter, ",", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string(jsonwriter, "state"); - rjsonwriter_raw(jsonwriter, ":", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_raw(jsonwriter, "{", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string(jsonwriter, "paused"); - rjsonwriter_raw(jsonwriter, ":", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_rawf(jsonwriter, "%u", (paused ? 1 : 0)); + return NULL; +} + +/** + * Returns a newly allocated string describing the content and core currently + * running. The string will contains the name of the core (or 'core') followed + * by a double underscore (_) and the name of the content. Returns NULL on + * failure. + */ +static char* translation_get_content_label() +{ + const char *label = NULL; + char* system_label = NULL; + core_info_t *core_info = NULL; + + core_info_get_current_core(&core_info); + if (core_info) { - static const char* state_labels[] = { "b", "y", "select", "start", "up", "down", "left", "right", "a", "x", "l", "r", "l2", "r2", "l3", "r3" }; - int i; - for (i = 0; i < (int)ARRAY_SIZE(state_labels); i++) + const struct playlist_entry *entry = NULL; + playlist_t *current_playlist = playlist_get_cached(); + const char *system_id; + size_t system_id_len; + size_t label_len; + + system_id = (core_info->system_id) ? core_info->system_id : "core"; + system_id_len = strlen(system_id); + + if (current_playlist) { - rjsonwriter_raw(jsonwriter, ",", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_add_string(jsonwriter, state_labels[i]); - rjsonwriter_raw(jsonwriter, ":", 1); - rjsonwriter_raw(jsonwriter, " ", 1); -#ifdef HAVE_ACCESSIBILITY - rjsonwriter_rawf(jsonwriter, "%u", - (input_st->ai_gamepad_state[i] ? 1 : 0)); -#else - rjsonwriter_rawf(jsonwriter, "%u", 0); -#endif + playlist_get_index_by_path( + current_playlist, path_get(RARCH_PATH_CONTENT), &entry); + + if (entry && !string_is_empty(entry->label)) + label = entry->label; } + + if (!label) + label = path_basename(path_get(RARCH_PATH_BASENAME)); + + label_len = strlen(label); + if (!(system_label = (char*)malloc(label_len + system_id_len + 3))) + return NULL; + + memcpy(system_label, system_id, system_id_len); + memcpy(system_label + system_id_len, "__", 2); + memcpy(system_label + 2 + system_id_len, label, label_len); + system_label[system_id_len + 2 + label_len] = '\0'; } - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_raw(jsonwriter, "}", 1); - rjsonwriter_raw(jsonwriter, " ", 1); - rjsonwriter_raw(jsonwriter, "}", 1); + + return system_label; +} - if (!(json_buffer = rjsonwriter_get_memory_buffer(jsonwriter, NULL))) - goto finish; /* ran out of memory */ +/** + * Creates and returns a JSON writer containing the payload to send alongside + * the translation request. {label} may be NULL, in which case no label will + * be supplied in the JSON. Returns NULL if the writer cannot be initialized. + */ +static rjsonwriter_t* build_request_json( + access_base64_t *image, access_request_t *request, + access_frame_t *frame, char *label) +{ + unsigned i; + rjsonwriter_t* writer = NULL; + + if (!(writer = rjsonwriter_open_memory())) + return NULL; -#ifdef DEBUG - if (access_st->ai_service_auto != 2) - RARCH_LOG("Request size: %d\n", bmp64_length); -#endif + rjsonwriter_add_start_object(writer); { - char new_ai_service_url[PATH_MAX_LENGTH]; - char separator = '?'; - unsigned ai_service_source_lang = settings->uints.ai_service_source_lang; - unsigned ai_service_target_lang = settings->uints.ai_service_target_lang; - const char *ai_service_url = settings->arrays.ai_service_url; - size_t _len = strlcpy(new_ai_service_url, - ai_service_url, sizeof(new_ai_service_url)); - - /* if query already exists in url, then use &'s instead */ - if (strrchr(new_ai_service_url, '?')) - separator = '&'; - - /* source lang */ - if (ai_service_source_lang != TRANSLATION_LANG_DONT_CARE) + rjsonwriter_add_string(writer, "image"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_string_len(writer, image->data, image->length); + + rjsonwriter_add_comma(writer); + rjsonwriter_add_string(writer, "format"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_string(writer, image->format); + + rjsonwriter_add_comma(writer); + rjsonwriter_add_string(writer, "coords"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_start_array(writer); { - const char *lang_source = ai_service_get_str( - (enum translation_lang)ai_service_source_lang); - - if (!string_is_empty(lang_source)) - { - new_ai_service_url[ _len] = separator; - new_ai_service_url[++_len] = '\0'; - _len += strlcpy(new_ai_service_url + _len, - "source_lang=", - sizeof(new_ai_service_url) - _len); - _len += strlcpy(new_ai_service_url + _len, - lang_source, - sizeof(new_ai_service_url) - _len); - separator = '&'; - } + rjsonwriter_add_unsigned(writer, frame->content_x); + rjsonwriter_add_comma(writer); + rjsonwriter_add_unsigned(writer, frame->content_y); + rjsonwriter_add_comma(writer); + rjsonwriter_add_unsigned(writer, frame->content_width); + rjsonwriter_add_comma(writer); + rjsonwriter_add_unsigned(writer, frame->content_height); + } + rjsonwriter_add_end_array(writer); + + rjsonwriter_add_comma(writer); + rjsonwriter_add_string(writer, "viewport"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_start_array(writer); + { + rjsonwriter_add_unsigned(writer, frame->viewport_width); + rjsonwriter_add_comma(writer); + rjsonwriter_add_unsigned(writer, frame->viewport_height); } + rjsonwriter_add_end_array(writer); - /* target lang */ - if (ai_service_target_lang != TRANSLATION_LANG_DONT_CARE) + if (label) { - const char *lang_target = ai_service_get_str( - (enum translation_lang)ai_service_target_lang); + rjsonwriter_add_comma(writer); + rjsonwriter_add_string(writer, "label"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_string(writer, label); + } - if (!string_is_empty(lang_target)) + rjsonwriter_add_comma(writer); + rjsonwriter_add_string(writer, "state"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_start_object(writer); + { + rjsonwriter_add_string(writer, "paused"); + rjsonwriter_add_colon(writer); + rjsonwriter_add_unsigned(writer, (request->paused ? 1 : 0)); + + for (i = 0; i < ARRAY_SIZE(ACCESS_INPUT_LABELS); i++) { - new_ai_service_url[ _len] = separator; - new_ai_service_url[++_len] = '\0'; - _len += strlcpy(new_ai_service_url + _len, - "target_lang=", - sizeof(new_ai_service_url) - _len); - _len += strlcpy(new_ai_service_url + _len, - lang_target, - sizeof(new_ai_service_url) - _len); - separator = '&'; - } + rjsonwriter_add_comma(writer); + rjsonwriter_add_string(writer, ACCESS_INPUT_LABELS[i]); + rjsonwriter_add_colon(writer); + rjsonwriter_add_unsigned(writer, request->inputs[i]); + } + rjsonwriter_add_end_object(writer); } + rjsonwriter_add_end_object(writer); + } + + return writer; +} + +/** + * Writes in the provided {buffer} the URL for the translation request. The + * buffer is guaranteed to contain the server URL as well as an 'output' param + * specifying the accepted data types for this service. + */ +static void build_request_url(char *buffer, size_t length, settings_t *settings) +{ + char token[2]; + size_t _len; + bool poke_supported = false; + unsigned service_source_lang = settings->uints.ai_service_source_lang; + unsigned service_target_lang = settings->uints.ai_service_target_lang; + const char *service_url = settings->arrays.ai_service_url; + unsigned ai_service_mode = settings->uints.ai_service_mode; +#ifdef HAVE_GFX_WIDGETS + video_driver_state_t *video_st = video_state_get_ptr(); + poke_supported = video_st->poke + && video_st->poke->load_texture + && video_st->poke->unload_texture; +#endif + + _len = strlcpy(buffer, service_url, length); + buffer += _len; + length -= _len; + + token[1] = '\0'; + if (strrchr(buffer, '?')) + token[0] = '&'; + else + token[0] = '?'; + + if (service_source_lang != TRANSLATION_LANG_DONT_CARE) + { + const char *lang_source + = ai_service_get_str((enum translation_lang)service_source_lang); - /* mode */ + if (!string_is_empty(lang_source)) { - unsigned ai_service_mode = settings->uints.ai_service_mode; - /*"image" is included for backwards compatability with - * vgtranslate < 1.04 */ + _len = strlcpy(buffer, token, length); + buffer += _len; + length -= _len; + + _len = strlcpy(buffer + _len, "source_lang=", length - _len); + buffer += _len; + length -= _len; + + _len = strlcpy(buffer, lang_source, length); + buffer += _len; + length -= _len; + token[0] = '&'; + } + } + + if (service_target_lang != TRANSLATION_LANG_DONT_CARE) + { + const char *lang_target + = ai_service_get_str((enum translation_lang)service_target_lang); + + if (!string_is_empty(lang_target)) + { + _len = strlcpy(buffer, token, length); + buffer += _len; + length -= _len; + + _len = strlcpy(buffer, "target_lang=", length); + buffer += _len; + length -= _len; + + _len = strlcpy(buffer, lang_target, length); + buffer += _len; + length -= _len; + token[0] = '&'; + } + } + + _len = strlcpy(buffer, token, length); + buffer += _len; + length -= _len; - new_ai_service_url[ _len] = separator; - new_ai_service_url[++_len] = '\0'; - _len += strlcpy(new_ai_service_url + _len, - "output=", - sizeof(new_ai_service_url) - _len); + _len = strlcpy(buffer, "output=", length); + buffer += _len; + length -= _len; - switch (ai_service_mode) + switch (ai_service_mode) + { + case 0: /* Image Mode */ + _len = strlcpy(buffer, "image,bmp", length); + buffer += _len; + length -= _len; +#ifdef HAVE_RPNG + _len = strlcpy(buffer, ",png", length); + buffer += _len; + length -= _len; + if (poke_supported) { - case 2: - strlcpy(new_ai_service_url + _len, - "text", - sizeof(new_ai_service_url) - _len); - break; - case 1: - case 3: - _len += strlcpy(new_ai_service_url + _len, - "sound,wav", - sizeof(new_ai_service_url) - _len); - if (ai_service_mode == 1) - break; - /* fall-through intentional for ai_service_mode == 3 */ - case 0: - _len += strlcpy(new_ai_service_url + _len, - "image,png", - sizeof(new_ai_service_url) - _len); -#ifdef HAVE_GFX_WIDGETS - if ( video_st->poke - && video_st->poke->load_texture - && video_st->poke->unload_texture) - strlcpy(new_ai_service_url + _len, - ",png-a", - sizeof(new_ai_service_url) - _len); + strlcpy(buffer, ",png-a", length); + buffer += _len; + length -= _len; + } #endif - break; - default: - break; + break; + + case 1: /* Speech Mode */ + _len = strlcpy(buffer, "sound,wav", length); + buffer += _len; + length -= _len; + break; + + case 2: /* Narrator Mode */ + _len = strlcpy(buffer, "text", length); + buffer += _len; + length -= _len; + break; + + case 3: /* Text Mode */ + case 4: /* Text + Narrator */ + _len = strlcpy(buffer, "text,subs", length); + buffer += _len; + length -= _len; + break; + + case 5: /* Image + Narrator */ + _len = strlcpy(buffer, "text,image,bmp", length); + buffer += _len; + length -= _len; +#ifdef HAVE_RPNG + _len = strlcpy(buffer, ",png", length); + buffer += _len; + length -= _len; + if (poke_supported) + { + _len = strlcpy(buffer, ",png-a", length); + buffer += _len; + length -= _len; } - - } -#ifdef DEBUG - if (access_st->ai_service_auto != 2) - RARCH_LOG("SENDING... %s\n", new_ai_service_url); #endif - task_push_http_post_transfer(new_ai_service_url, - json_buffer, true, NULL, handle_translation_cb, NULL); + break; } +} - error = false; +/** + * Captures a frame from the currently running core and sends a request to the + * translation server. Processing and encoding this data comes with a cost, so + * it is offloaded to the task thread. + */ +static void translation_request_hndl(retro_task_t *task) +{ + access_request_t *request = (access_request_t*)task->user_data; + settings_t *settings = config_get_ptr(); + access_state_t *access_st = access_state_get_ptr(); + access_frame_t *frame = NULL; + access_base64_t *encode = NULL; + char *label = NULL; + rjsonwriter_t *writer = NULL; + const char *json = NULL; + bool sent = false; + char url[PATH_MAX_LENGTH]; + + if (task_get_cancelled(task)) + goto finish; + + access_st->last_call = cpu_features_get_time_usec(); + + frame = translation_grab_frame(); + if (task_get_cancelled(task) || !frame) + goto finish; + + if (translation_dupe_fail(frame)) + goto finish; + + encode = translation_frame_encode(frame); + if (task_get_cancelled(task) || !encode) + goto finish; + + label = translation_get_content_label(); + writer = build_request_json(encode, request, frame, label); + if (task_get_cancelled(task) || !writer) + goto finish; + + json = rjsonwriter_get_memory_buffer(writer, NULL); + build_request_url(url, PATH_MAX_LENGTH, settings); + if (task_get_cancelled(task) || !json) + goto finish; + +#ifdef DEBUG + if (access_st->ai_service_auto == 0) + RARCH_LOG("[Translate]: Sending request to: %s\n", url); +#endif + sent = true; + task_push_http_post_transfer( + url, json, true, NULL, translation_response_cb, NULL); + finish: - if (bit24_image_prev) - free(bit24_image_prev); - if (bit24_image) - free(bit24_image); - - if (scaler) - free(scaler); - - if (bmp_buffer) - free(bmp_buffer); + task_set_finished(task, true); - if (bmp64_buffer) - free(bmp64_buffer); - if (sys_lbl) - free(sys_lbl); - if (jsonwriter) - rjsonwriter_free(jsonwriter); - return !error; + if (frame && frame->data) + free(frame->data); + if (frame) + free(frame); + if (encode && encode->data) + free(encode->data); + if (encode) + free(encode); + if (label) + free(label); + if (writer) + rjsonwriter_free(writer); + if (request && request->inputs) + free(request->inputs); + if (request) + free(request); + + /* Plan next auto-request if this one was skipped */ + if (!sent && access_st->ai_service_auto != 0) + call_auto_translate_task(settings); } -#ifdef HAVE_ACCESSIBILITY -bool is_narrator_running(bool accessibility_enable) +/** + * Invokes the translation service. Captures a frame from the current content + * core and sends it over HTTP to the translation server. Once the server + * responds, the translation data is displayed accordingly to the preferences + * of the user. Returns true if the request could be built and sent. + */ +bool run_translation_service(settings_t *settings, bool paused) { - access_state_t *access_st = access_state_get_ptr(); - if (is_accessibility_enabled( - accessibility_enable, - access_st->enabled)) + unsigned i; + retro_task_t *task = NULL; + access_request_t *request = NULL; + access_state_t *access_st = access_state_get_ptr(); +#ifdef HAVE_ACCESSIBILITY + input_driver_state_t *input_st = input_state_get_ptr(); +#endif + + if (!(request = (access_request_t*)malloc(sizeof(access_request_t)))) + goto failure; + +#ifdef HAVE_THREADS + if (!access_st->image_lock) { - frontend_ctx_driver_t *frontend = - frontend_state_get_ptr()->current_frontend_ctx; - if (frontend && frontend->is_narrator_running) - return frontend->is_narrator_running(); + if (!(access_st->image_lock = slock_new())) + goto failure; } +#endif + + task = task_init(); + if (!task) + goto failure; + + /* Freeze frontend state while we're still running on the main thread */ + request->paused = paused; + request->inputs = (char*)malloc( + sizeof(char) * ARRAY_SIZE(ACCESS_INPUT_LABELS)); + +#ifdef HAVE_ACCESSIBILITY + for (i = 0; i < ARRAY_SIZE(ACCESS_INPUT_LABELS); i++) + request->inputs[i] = input_st->ai_gamepad_state[i] ? 1 : 0; +#endif + + task->handler = translation_request_hndl; + task->user_data = request; + task->mute = true; + access_st->request_task = task; + task_queue_push(task); + return true; + +failure: + if (request) + free(request); + + return false; } -#endif diff --git a/translation_defines.h b/translation_defines.h index 353825eb24d8..09c4565407d4 100644 --- a/translation_defines.h +++ b/translation_defines.h @@ -64,6 +64,7 @@ enum translation_lang TRANSLATION_LANG_TH, /* Thai */ TRANSLATION_LANG_TR, /* Turkish */ TRANSLATION_LANG_UK, /* Ukrainian */ + TRANSLATION_LANG_BE, /* Belarusian */ TRANSLATION_LANG_UR, /* Urdu */ TRANSLATION_LANG_VI, /* Vietnamese */ TRANSLATION_LANG_CY, /* Welsh */ diff --git a/ui/drivers/ui_cocoatouch.m b/ui/drivers/ui_cocoatouch.m index 395f37788289..ff19359238ec 100644 --- a/ui/drivers/ui_cocoatouch.m +++ b/ui/drivers/ui_cocoatouch.m @@ -69,6 +69,7 @@ static void rarch_draw_observer(CFRunLoopObserverRef observer, ui_companion_cocoatouch_event_command( NULL, CMD_EVENT_MENU_SAVE_CURRENT_CONFIG); main_exit(NULL); + exit(0); return; } diff --git a/verbosity.h b/verbosity.h index 39fe0743e366..731de2451099 100644 --- a/verbosity.h +++ b/verbosity.h @@ -32,10 +32,10 @@ RETRO_BEGIN_DECLS -#define FILE_PATH_LOG_DBG "[DEBUG]" -#define FILE_PATH_LOG_INFO "[INFO]" -#define FILE_PATH_LOG_ERROR "[ERROR]" -#define FILE_PATH_LOG_WARN "[WARN]" +#define FILE_PATH_LOG_DBG "[DBG]" +#define FILE_PATH_LOG_INFO "[NFO]" +#define FILE_PATH_LOG_ERROR "[ERR]" +#define FILE_PATH_LOG_WARN "[WRN]" bool verbosity_is_enabled(void);