From 3e9f975213eef40469038385228d24892326eabc Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 12:43:55 +0900 Subject: [PATCH 1/8] Update libsys4 --- subprojects/libsys4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subprojects/libsys4 b/subprojects/libsys4 index 0409f62..9906c3c 160000 --- a/subprojects/libsys4 +++ b/subprojects/libsys4 @@ -1 +1 @@ -Subproject commit 0409f62789bd2f9b4364d4e9a755aaff7725cfba +Subproject commit 9906c3c2b0725fb309abac414bbbf5668bd325bc From bc522b27e064fe0b1e2a54f546e940a5f4eac9d6 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 12:45:07 +0900 Subject: [PATCH 2/8] Android: Report ERROR() with a message box --- include/vm.h | 5 +++++ src/system4.c | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/include/vm.h b/include/vm.h index 3849142..80682b5 100644 --- a/include/vm.h +++ b/include/vm.h @@ -129,8 +129,13 @@ void vm_stack_trace(void); _Noreturn void _vm_error(const char *fmt, ...); _Noreturn void vm_exit(int code); +#ifdef __ANDROID__ +// Report the error with a message box and exit. +#define VM_ERROR ERROR +#else #define VM_ERROR(fmt, ...) \ _vm_error("*ERROR*(%s:%s:%d): " fmt "\n", __FILE__, __func__, __LINE__, ##__VA_ARGS__) +#endif #ifdef VM_PRIVATE #include "little_endian.h" diff --git a/src/system4.c b/src/system4.c index 6f6fd0c..cd9570f 100644 --- a/src/system4.c +++ b/src/system4.c @@ -401,11 +401,22 @@ static void windows_error_handler(const char *msg) } #endif +#ifdef __ANDROID__ +static void android_error_handler(const char *msg) +{ + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "xsystem4", msg, NULL); +} +#endif + int main(int argc, char *argv[]) { #ifdef _WIN32 sys_error_handler = windows_error_handler; #endif +#ifdef __ANDROID__ + sys_error_handler = android_error_handler; +#endif + initialize_instructions(); char *ainfile; int err = AIN_SUCCESS; From b7ab1ed476a62c36f21eac8d412db213a0f1ae8a Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 12:49:52 +0900 Subject: [PATCH 3/8] Android: Enforce landscape fullscreen mode --- src/video.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/video.c b/src/video.c index c8ef84f..1f15151 100644 --- a/src/video.c +++ b/src/video.c @@ -200,12 +200,19 @@ int gfx_init(void) #endif sdl.format = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA32); + uint32_t window_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN; +#ifdef __ANDROID__ + window_flags |= SDL_WINDOW_FULLSCREEN; + SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); +#else + window_flags |= SDL_WINDOW_RESIZABLE; +#endif sdl.window = SDL_CreateWindow("XSystem4", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, config.view_width, config.view_height, - SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + window_flags); if (!sdl.window) ERROR("SDL_CreateWindow failed: %s", SDL_GetError()); From b7304f747b908fe9592fd2c15924651246855b7a Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 12:53:56 +0900 Subject: [PATCH 4/8] Android: Load fonts and shaders from apk assets --- src/font_freetype.c | 21 +++++++++++++++++++++ src/video.c | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/font_freetype.c b/src/font_freetype.c index 3c4a766..2c17aaa 100644 --- a/src/font_freetype.c +++ b/src/font_freetype.c @@ -18,6 +18,7 @@ #include #include FT_FREETYPE_H #include FT_BITMAP_H +#include #include "system4.h" #include "system4/hashtable.h" @@ -188,13 +189,33 @@ static float ft_font_size_char(struct font_size *size, uint32_t code) struct font *ft_font_load(const char *path) { struct font_ft *font = xcalloc(1, sizeof(struct font_ft)); + +#ifdef __ANDROID__ + // On Android, path may be an asset name which FT_New_Face cannot read + // directly, so use SDL_LoadFile to load the content into memory. + size_t size; + void *buf = SDL_LoadFile(path, &size); + if (!buf) { + free(font); + return NULL; + } + if (FT_New_Memory_Face(ft_lib, buf, size, 0, &font->font)) { + free(buf); + free(font); + return NULL; + } +#else if (FT_New_Face(ft_lib, path, 0, &font->font)) { free(font); return NULL; } +#endif if (!font->font->charmap) { WARNING("Font '%s' doesn't contain unicode charmap", path); +#ifdef __ANDROID__ + free(buf); +#endif free(font); return NULL; } diff --git a/src/video.c b/src/video.c index 1f15151..619e799 100644 --- a/src/video.c +++ b/src/video.c @@ -68,11 +68,11 @@ struct texture main_surface; static GLchar *read_shader_file(const char *path) { - GLchar *source = file_read(path, NULL); + GLchar *source = SDL_LoadFile(path, NULL); if (!source) { char full_path[PATH_MAX]; snprintf(full_path, PATH_MAX, XSYS4_DATA_DIR "/%s", path); - source = file_read(full_path, NULL); + source = SDL_LoadFile(full_path, NULL); if (!source) ERROR("Failed to load shader file %s", full_path, strerror(errno)); } @@ -98,7 +98,7 @@ GLuint gfx_load_shader_file(const char *path, GLenum type) glGetShaderInfoLog(shader, len, NULL, infolog); ERROR("Failed to compile shader %s: %s", path, infolog); } - free((char*)source[1]); + SDL_free((char*)source[1]); return shader; } From 9ec354c198814956ee4da147526e38bf05dfcabc Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 12:54:54 +0900 Subject: [PATCH 5/8] Android: Add CMakeLists.txt --- CMakeLists.txt | 183 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..43d183a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,183 @@ +# CMakeLists.txt for Android build + +cmake_minimum_required(VERSION 3.14) +project(xsystem4 LANGUAGES C) +set(CMAKE_C_STANDARD 11) + +include(FetchContent) + +FetchContent_Declare( + cglm + URL https://github.com/recp/cglm/archive/refs/tags/v0.9.2.tar.gz + URL_HASH SHA1=cb8472aa8c2ab67b66378dbaf10c2c7368d4e4c3 +) +FetchContent_Declare( + SDL + URL https://github.com/libsdl-org/SDL/releases/download/release-2.30.1/SDL2-2.30.1.tar.gz + URL_HASH SHA1=9d502c495f3aa2d15446376e835a5e561ac32897 +) +FetchContent_Declare( + freetype + GIT_REPOSITORY https://github.com/freetype/freetype.git + GIT_TAG VER-2-13-2 +) +FetchContent_MakeAvailable(cglm SDL freetype) + +add_library(ffi STATIC IMPORTED) +set_target_properties(ffi PROPERTIES + IMPORTED_LOCATION ${CMAKE_STAGING_PREFIX}/lib/libffi.a + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_STAGING_PREFIX}/include) + +find_package(SndFile REQUIRED) + +add_subdirectory(subprojects/libsys4) + +add_library(xsystem4 SHARED) + +target_compile_definitions(xsystem4 PRIVATE + _DEFAULT_SOURCE + USE_GLES + ) + +target_include_directories(xsystem4 PRIVATE include) + +execute_process( + COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE VCS_TAG + OUTPUT_STRIP_TRAILING_WHITESPACE) +configure_file(src/version.h.in version.h) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +target_sources(xsystem4 PRIVATE + src/audio.c + src/audio_meta.c + src/audio_mixer.c + src/asset_manager.c + src/base64.c + src/cJSON.c + src/draw.c + src/effect.c + src/ffi.c + src/font_freetype.c + src/font_fnl.c + src/format.c + src/hacks.c + src/heap.c + src/id_pool.c + src/input.c + src/json.c + src/movie_plmpeg.c + src/msgqueue.c + src/page.c + src/resume.c + src/savedata.c + src/scene.c + src/sprite.c + src/swf.c + src/system4.c + src/text.c + src/util.c + src/video.c + src/vm.c + + src/3d/debug.c + src/3d/model.c + src/3d/parser.c + src/3d/particle.c + src/3d/reign.c + src/3d/renderer.c + + src/dungeon/dgn.c + src/dungeon/dtx.c + src/dungeon/dungeon.c + src/dungeon/map.c + src/dungeon/polyobj.c + src/dungeon/renderer.c + src/dungeon/skybox.c + src/dungeon/tes.c + + src/parts/construction.c + src/parts/debug.c + src/parts/flash.c + src/parts/input.c + src/parts/motion.c + src/parts/parts.c + src/parts/render.c + src/parts/text.c + src/parts/save.c + + src/hll/ACXLoader.c + src/hll/ADVSYS.c + src/hll/AliceLogo.c + src/hll/AliceLogo2.c + src/hll/AliceLogo3.c + src/hll/AliceLogo4.c + src/hll/AliceLogo5.c + src/hll/AnteaterADVEngine.c + src/hll/BanMisc.c + src/hll/Bitarray.c + src/hll/CGManager.c + src/hll/CharSpriteManager.c + src/hll/ChipmunkSpriteEngine.c + src/hll/ChrLoader.c + src/hll/CommonSystemData.c + src/hll/Confirm.c + src/hll/Confirm2.c + src/hll/Confirm3.c + src/hll/CrayfishLogViewer.c + src/hll/Cursor.c + src/hll/Data.c + src/hll/DataFile.c + src/hll/DrawDungeon.c + src/hll/DrawGraph.c + src/hll/DrawMovie.c + src/hll/DrawPluginManager.c + src/hll/DrawSimpleText.c + src/hll/File.c + src/hll/File2.c + src/hll/FileOperation.c + src/hll/FillAngle.c + src/hll/GoatGUIEngine.c + src/hll/Gpx2Plus.c + src/hll/GUIEngine.c + src/hll/iarray.c + src/hll/IbisInputEngine.c + src/hll/InputDevice.c + src/hll/InputString.c + src/hll/KiwiSoundEngine.c + src/hll/LoadCG.c + src/hll/MainEXFile.c + src/hll/MainSurface.c + src/hll/MarmotModelEngine.c + src/hll/Math.c + src/hll/MapLoader.c + src/hll/MenuMsg.c + src/hll/MonsterInfo.c + src/hll/MsgLogManager.c + src/hll/MsgLogViewer.c + src/hll/MsgSkip.c + src/hll/OutputLog.c + src/hll/PassRegister.c + src/hll/PartsEngine.c + src/hll/PlayDemo.c + src/hll/PlayMovie.c + src/hll/ReignEngine.c + src/hll/SACT2.c + src/hll/SengokuRanceFont.c + src/hll/SoundFilePlayer.c + src/hll/StoatSpriteEngine.c + src/hll/StretchHelper.c + src/hll/SystemService.c + src/hll/SystemServiceEx.c + src/hll/Timer.c + src/hll/Toushin3Loader.c + src/hll/VSFile.c + ) + +target_link_libraries(xsystem4 PRIVATE + m z SDL2 freetype ffi GLESv3 cglm SndFile::sndfile sys4) + +target_compile_options(xsystem4 PRIVATE -Wno-unused-parameter) + +install(TARGETS xsystem4 SDL2 freetype) From 5e80c9658787b118920f850ae4cbc883e6fceb1e Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 12:57:04 +0900 Subject: [PATCH 6/8] Defer mouse button events synthesized from touch events A typical event loop in System4 game looks like this: while (true) { SACT2.Mouse_GetPos(x, y); // Update the screen according to the mouse position (x, y) ... SACT2.Update(); if (SACT2.Key_IsDown(VK_LBUTTON)) { // A button was pressed at (x, y) ... } } This works poorly with touch devices. Assume that a touch event occured during SACT2.Update(). It updates internal mouse position and button status. After SACT2.Update(), the game calls SACT2.Key_IsDown(VK_LBUTTON). It returns true, but the game has not yet called SACT2.Mouse_GetPos() after the touch event, so it assumes that the button was pressed where the mouse pointer was before the touch. In order to avoid this situation, this defers mouse button events synthesized (by SDL) from touch. Such deferred events are processed when SACT2.Mouse_GetPos() is called or after 50ms. --- src/input.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/input.c b/src/input.c index e3db35a..6cb3fa8 100644 --- a/src/input.c +++ b/src/input.c @@ -145,6 +145,12 @@ bool keyboard_focus = true; #define MAX_CONTROLLERS 4 static SDL_GameController *controllers[MAX_CONTROLLERS]; +// Stores a mouse button event synthesized from a touch event (valid if +// .timestamp != 0). We defer such events to prevent games from handling +// button down events before reading the pointer position. +static SDL_MouseButtonEvent deferred_synthetic_mouse_event; +#define SYNTHETIC_MOUSE_EVENT_DELAY 50 + static enum sact_keycode sdl_to_sact_button(int button) { switch (button) { @@ -477,6 +483,12 @@ void clear_editing_handler(void) void handle_events(void) { + // Flush the deferred mouse button event if it's older than 50ms. + if (deferred_synthetic_mouse_event.timestamp && deferred_synthetic_mouse_event.timestamp + SYNTHETIC_MOUSE_EVENT_DELAY < SDL_GetTicks()) { + mouse_event(&deferred_synthetic_mouse_event); + deferred_synthetic_mouse_event.timestamp = 0; + } + SDL_Event e; while (SDL_PollEvent(&e)) { switch (e.type) { @@ -516,7 +528,13 @@ void handle_events(void) break; case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONDOWN: - mouse_event(&e.button); + if (e.button.which == SDL_TOUCH_MOUSEID) { + if (deferred_synthetic_mouse_event.timestamp) + mouse_event(&deferred_synthetic_mouse_event); + deferred_synthetic_mouse_event = e.button; + } else { + mouse_event(&e.button); + } break; case SDL_MOUSEWHEEL: wheel_dir = e.wheel.y; From 18a71960e8e6832e9a9ce11d60012fa29d9fd092 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 13:21:32 +0900 Subject: [PATCH 7/8] Add touch gestures This adds gestures that allow touch device users to emulate the following mouse/keyboard actions: - Right-click: touch outside the game's viewport - Ctrl key: one-finger touch and hold (for one second) - Mouse wheel: Swipe up / down with two fingers --- src/input.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/src/input.c b/src/input.c index 6cb3fa8..f527a30 100644 --- a/src/input.c +++ b/src/input.c @@ -151,6 +151,13 @@ static SDL_GameController *controllers[MAX_CONTROLLERS]; static SDL_MouseButtonEvent deferred_synthetic_mouse_event; #define SYNTHETIC_MOUSE_EVENT_DELAY 50 +static uint32_t long_touch_start_timestamp; +static SDL_FRect long_touch_finger_rect; +#define LONG_TOUCH_DURATION 1000 + +static float scroll_gesture_y; +#define SCROLL_GESTURE_SENSITIVITY 0.02f // 2% of screen height + static enum sact_keycode sdl_to_sact_button(int button) { switch (button) { @@ -230,6 +237,21 @@ static void mouse_event(SDL_MouseButtonEvent *e) #endif } +static void synthetic_mouse_event(SDL_MouseButtonEvent *e) +{ + if (e->state == SDL_PRESSED) { + // Touch outside the viewport is treated as right-click. + SDL_Point p = { .x = e->x, .y = e->y }; + if (SDL_PointInRect(&p, &sdl.viewport)) + key_state[VK_LBUTTON] = true; + else + key_state[VK_RBUTTON] = true; + } else { + key_state[VK_LBUTTON] = false; + key_state[VK_RBUTTON] = false; + } +} + #define JOYAXIS_DEADZONE 13500 enum joyaxis_axis { @@ -485,9 +507,14 @@ void handle_events(void) { // Flush the deferred mouse button event if it's older than 50ms. if (deferred_synthetic_mouse_event.timestamp && deferred_synthetic_mouse_event.timestamp + SYNTHETIC_MOUSE_EVENT_DELAY < SDL_GetTicks()) { - mouse_event(&deferred_synthetic_mouse_event); + synthetic_mouse_event(&deferred_synthetic_mouse_event); deferred_synthetic_mouse_event.timestamp = 0; } + // Long touch emulates pressing the Ctrl key. + if (long_touch_start_timestamp && long_touch_start_timestamp + LONG_TOUCH_DURATION < SDL_GetTicks()) { + key_state[VK_LBUTTON] = false; + key_state[VK_CONTROL] = true; + } SDL_Event e; while (SDL_PollEvent(&e)) { @@ -530,7 +557,7 @@ void handle_events(void) case SDL_MOUSEBUTTONDOWN: if (e.button.which == SDL_TOUCH_MOUSEID) { if (deferred_synthetic_mouse_event.timestamp) - mouse_event(&deferred_synthetic_mouse_event); + synthetic_mouse_event(&deferred_synthetic_mouse_event); deferred_synthetic_mouse_event = e.button; } else { mouse_event(&e.button); @@ -539,6 +566,47 @@ void handle_events(void) case SDL_MOUSEWHEEL: wheel_dir = e.wheel.y; break; + case SDL_FINGERDOWN: + if (SDL_GetNumTouchFingers(e.tfinger.touchId) >= 2) { + // The user is about to start a multi-touch gesture. + key_state[VK_LBUTTON] = false; + key_state[VK_RBUTTON] = false; + deferred_synthetic_mouse_event.timestamp = 0; + long_touch_start_timestamp = 0; + scroll_gesture_y = -1.0f; + break; + } + long_touch_start_timestamp = e.tfinger.timestamp; + // Movement within this rect (1% of the screen size from the touch + // start position) will be ignored. + long_touch_finger_rect = (SDL_FRect) { + .x = e.tfinger.x - 0.01, + .y = e.tfinger.y - 0.01, + .w = 0.02, + .h = 0.02 + }; + break; + case SDL_FINGERMOTION: + if (SDL_PointInFRect(&(SDL_FPoint){ e.tfinger.x, e.tfinger.y }, &long_touch_finger_rect)) + break; + // Cancel the timer only if Ctrl emulation has not already started. + if (!key_state[VK_CONTROL]) + long_touch_start_timestamp = 0; + break; + case SDL_FINGERUP: + long_touch_start_timestamp = 0; + key_state[VK_CONTROL] = false; + break; + case SDL_MULTIGESTURE: + if (e.mgesture.numFingers == 2) { + if (scroll_gesture_y < 0.0f) + scroll_gesture_y = e.mgesture.y; + float dy = scroll_gesture_y - e.mgesture.y; + if (dy * dy > SCROLL_GESTURE_SENSITIVITY * SCROLL_GESTURE_SENSITIVITY) { + wheel_dir = dy < 0 ? 1 : -1; // Swipe up to scroll down. + scroll_gesture_y = e.mgesture.y; + } + } case SDL_CONTROLLERDEVICEADDED: if (e.cdevice.which < MAX_CONTROLLERS) controllers[e.cdevice.which] = SDL_GameControllerOpen(e.cdevice.which); From f0f84a05cec6882eeb0c0343fe610430da7b885f Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 16 Mar 2024 13:00:51 +0900 Subject: [PATCH 8/8] Android: Fix text input issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Android, when the user enters "ら", "ん", "す" with the virtual keyboard and converts it to "ランス", xsystem4 receives the following events: SDL_TEXTINPUT("ら") SDL_TEXTINPUT("ん") SDL_TEXTINPUT("す") SDL_KEYDOWN(backspace) SDL_KEYUP(backspace) SDL_KEYDOWN(backspace) SDL_KEYUP(backspace) SDL_KEYDOWN(backspace) SDL_KEYUP(backspace) SDL_TEXTINPUT("ランス") The backspace key events are generated by SDL to delete the unconverted text. However, System4 games process the backspace key by polling, so if the SDL_KEYDOWN and SDL_KEYUP events occur in close succession, the game cannot detect that the backspace key has been pressed. To solve this problem, this patch queues these consecutive input events and processes them with an interval of at least 10ms. Fixes https://github.com/kichikuou/xsystem4-android/issues/2. --- src/input.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/input.c b/src/input.c index f527a30..62cbb56 100644 --- a/src/input.c +++ b/src/input.c @@ -18,6 +18,7 @@ #include #include #include "system4.h" +#include "queue.h" #include "gfx/gfx.h" #include "gfx/private.h" #include "input.h" @@ -145,6 +146,17 @@ bool keyboard_focus = true; #define MAX_CONTROLLERS 4 static SDL_GameController *controllers[MAX_CONTROLLERS]; +#ifdef __ANDROID__ +struct deferred_keyevent { + STAILQ_ENTRY(deferred_keyevent) entry; + SDL_Event e; +}; +static STAILQ_HEAD(deferred_keyevent_queue, deferred_keyevent) deferred_keyevent_queue = + STAILQ_HEAD_INITIALIZER(deferred_keyevent_queue); +static uint32_t last_keyevent_timestamp; +#define DEFERRED_KEY_DELAY 10 +#endif + // Stores a mouse button event synthesized from a touch event (valid if // .timestamp != 0). We defer such events to prevent games from handling // button down events before reading the pointer position. @@ -503,19 +515,47 @@ void clear_editing_handler(void) editing_handler = NULL; } -void handle_events(void) +static void fire_deferred_events(void) { + uint32_t now = SDL_GetTicks(); + // Flush the deferred mouse button event if it's older than 50ms. - if (deferred_synthetic_mouse_event.timestamp && deferred_synthetic_mouse_event.timestamp + SYNTHETIC_MOUSE_EVENT_DELAY < SDL_GetTicks()) { + if (deferred_synthetic_mouse_event.timestamp && + deferred_synthetic_mouse_event.timestamp + SYNTHETIC_MOUSE_EVENT_DELAY < now) { synthetic_mouse_event(&deferred_synthetic_mouse_event); deferred_synthetic_mouse_event.timestamp = 0; } // Long touch emulates pressing the Ctrl key. - if (long_touch_start_timestamp && long_touch_start_timestamp + LONG_TOUCH_DURATION < SDL_GetTicks()) { + if (long_touch_start_timestamp && long_touch_start_timestamp + LONG_TOUCH_DURATION < now) { key_state[VK_LBUTTON] = false; key_state[VK_CONTROL] = true; } +#ifdef __ANDROID__ + // Fire the deferred keyboard and text input events. + while (!STAILQ_EMPTY(&deferred_keyevent_queue) && now >= last_keyevent_timestamp + DEFERRED_KEY_DELAY) { + struct deferred_keyevent *ev = STAILQ_FIRST(&deferred_keyevent_queue); + switch (ev->e.type) { + case SDL_TEXTINPUT: + if (input_handler) + input_handler(ev->e.text.text); + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + last_keyevent_timestamp = now; + key_event(&ev->e.key, ev->e.type == SDL_KEYDOWN); + break; + } + STAILQ_REMOVE_HEAD(&deferred_keyevent_queue, entry); + free(ev); + } +#endif +} + +void handle_events(void) +{ + fire_deferred_events(); + SDL_Event e; while (SDL_PollEvent(&e)) { switch (e.type) { @@ -548,10 +588,23 @@ void handle_events(void) case SDL_KEYDOWN: if (e.key.keysym.scancode == SDL_SCANCODE_F9) vm_stack_trace(); - key_event(&e.key, true); - break; + // fallthrough case SDL_KEYUP: - key_event(&e.key, false); +#ifdef __ANDROID__ + if (input_handler && e.key.timestamp < last_keyevent_timestamp + DEFERRED_KEY_DELAY) { + // Input from virtual keyboard is sent as consecutive + // SDL_KEYDOWN and SDL_KEYUP events. To give the game a chance + // to see the previous key event, delay the event. + struct deferred_keyevent *ev = xmalloc(sizeof(struct deferred_keyevent)); + ev->e = e; + STAILQ_INSERT_TAIL(&deferred_keyevent_queue, ev, entry); + } else { + last_keyevent_timestamp = e.key.timestamp; + key_event(&e.key, e.type == SDL_KEYDOWN); + } +#else + key_event(&e.key, e.type == SDL_KEYDOWN); +#endif break; case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONDOWN: @@ -619,8 +672,18 @@ void handle_events(void) controller_button_event(&e.cbutton); break; case SDL_TEXTINPUT: - if (input_handler) + if (!input_handler) { + break; +#ifdef __ANDROID__ + } else if (!STAILQ_EMPTY(&deferred_keyevent_queue)) { + // The order of key events and text events must be preserved. + struct deferred_keyevent *ev = xmalloc(sizeof(struct deferred_keyevent)); + ev->e = e; + STAILQ_INSERT_TAIL(&deferred_keyevent_queue, ev, entry); +#endif + } else { input_handler(e.text.text); + } break; case SDL_TEXTEDITING: if (editing_handler)