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) 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/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/input.c b/src/input.c index e3db35a..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,30 @@ 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. +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) { @@ -224,6 +249,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 { @@ -475,8 +515,47 @@ void clear_editing_handler(void) editing_handler = NULL; } +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 < 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 < 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) { @@ -509,18 +588,78 @@ 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: - mouse_event(&e.button); + if (e.button.which == SDL_TOUCH_MOUSEID) { + if (deferred_synthetic_mouse_event.timestamp) + synthetic_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; 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); @@ -533,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) 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; diff --git a/src/video.c b/src/video.c index c8ef84f..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; } @@ -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()); 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