diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt index b8f3e86..76c6a0b 100644 --- a/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt @@ -61,9 +61,13 @@ class GameActivity : SDLActivity() { } override fun getArguments(): Array { - return arrayOf( + val args = mutableListOf( "-gamedir", intent.getStringExtra(EXTRA_GAME_ROOT)!!, "-savedir", intent.getStringExtra(EXTRA_SAVE_DIRECTORY)!!) + val prefs = getSharedPreferences(Launcher.PREFS_NAME, MODE_PRIVATE) + if (prefs.getBoolean(Launcher.PREF_VIRTUAL_POINTER, false)) + args.add("-virtualpointer") + return args.toTypedArray() } override fun setTitle(title: CharSequence?) { diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt index adecc2c..23f90a3 100644 --- a/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt +++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt @@ -42,6 +42,8 @@ interface LauncherObserver { class Launcher private constructor(private val rootDir: File) { companion object { const val SAVE_DIR = "save" + const val PREFS_NAME = "settings" + const val PREF_VIRTUAL_POINTER = "virtual_pointer" const val TITLE_FILE = "title.txt" const val GAMEDIR_FILE = "game_directory.txt" const val PLAYLIST_FILE = "playlist.txt" diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt index 675870f..defe852 100644 --- a/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt @@ -92,6 +92,9 @@ class LauncherActivity : Activity(), LauncherObserver { override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.launcher_menu, menu) + val prefs = getSharedPreferences(Launcher.PREFS_NAME, MODE_PRIVATE) + menu.findItem(R.id.virtual_pointer).isChecked = + prefs.getBoolean(Launcher.PREF_VIRTUAL_POINTER, false) return true } @@ -143,6 +146,13 @@ class LauncherActivity : Activity(), LauncherObserver { startActivity(intent) true } + R.id.virtual_pointer -> { + val enabled = !item.isChecked + item.isChecked = enabled + getSharedPreferences(Launcher.PREFS_NAME, MODE_PRIVATE).edit() + .putBoolean(Launcher.PREF_VIRTUAL_POINTER, enabled).apply() + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/android/app/src/main/res/menu/launcher_menu.xml b/android/app/src/main/res/menu/launcher_menu.xml index 772b388..e97f6b5 100644 --- a/android/app/src/main/res/menu/launcher_menu.xml +++ b/android/app/src/main/res/menu/launcher_menu.xml @@ -12,6 +12,10 @@ + diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml index 9958a9f..87c3851 100644 --- a/android/app/src/main/res/values-ja/strings.xml +++ b/android/app/src/main/res/values-ja/strings.xml @@ -7,6 +7,7 @@ ZIPからインストール オープンソースライセンス セーブデータをクリア + 仮想マウスポインタ キャンセル System 3.x のファイル (*.ald) が見つかりません。 ファイルを選択 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 5913347..f87529c 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Import Save Files Install from ZIP Open source licenses + Virtual mouse pointer Cancel Cannot find System 3.x game files (*.ald). Choose a file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a22d20..481611c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -91,6 +91,7 @@ target_sources(xsystem35 PRIVATE system.c texthook.c variable.c + virtual_pointer.c vsp.c xsystem35.c ) diff --git a/src/ags.h b/src/ags.h index abac5d4..5810625 100644 --- a/src/ags.h +++ b/src/ags.h @@ -131,6 +131,7 @@ struct _ags { bool enable_zb; bool noantialias; /* antialias を使用しない */ bool noimagecursor; /* リソースファイルのカーソルを読みこまない */ + bool virtualpointer; /* enable the virtual mouse pointer (trackpad-style touch) */ }; typedef struct _ags ags_t; diff --git a/src/bitmaps/virtual_cursor.xpm b/src/bitmaps/virtual_cursor.xpm new file mode 100644 index 0000000..b207679 --- /dev/null +++ b/src/bitmaps/virtual_cursor.xpm @@ -0,0 +1,29 @@ +/* XPM */ +static const char *virtual_cursor[] = { + /* width height num_colors chars_per_pixel */ + "16 19 3 1", + /* colors */ + "X c #000000", + ". c #ffffff", + " c None", + /* pixels */ + "X ", + "XX ", + "X.X ", + "X..X ", + "X...X ", + "X....X ", + "X.....X ", + "X......X ", + "X.......X ", + "X........X ", + "X.....XXXXX ", + "X..X..X ", + "X.X X..X ", + "XX X..X ", + "X X..X ", + " X..X ", + " X..X ", + " X..X ", + " XX " +}; diff --git a/src/event.c b/src/event.c index 335542f..6953011 100644 --- a/src/event.c +++ b/src/event.c @@ -20,6 +20,7 @@ #include "config.h" #include +#include #include #include #include @@ -40,6 +41,7 @@ #include "input.h" #include "msgskip.h" #include "hacks.h" +#include "virtual_pointer.h" static void get_event(void); static void keyEventProsess(SDL_KeyboardEvent *e, bool pressed); @@ -277,6 +279,13 @@ void send_agsevent(enum agsevent_type type, int code) { } void event_set_mouse_location(int x, int y) { + if (vp_is_enabled()) { + // In virtual pointer mode there is no visible OS cursor to warp; move + // the internal location instead so the on-screen virtual cursor follows + // (including each step of the eased path in ags_setCursorLocation()). + event_set_mouse_internal_location(x, y); + return; + } // scale mouse x and y float scalex, scaley; SDL_RenderGetScale(gfx_renderer, &scalex, &scaley); @@ -306,9 +315,16 @@ void event_set_mouse_location(int x, int y) { void event_set_mouse_internal_location(int x, int y) { mousex = x; mousey = y; + if (vp_is_enabled()) + gfx_dirty = true; // repaint so the virtual cursor follows send_agsevent(AGSEVENT_MOUSE_MOTION, 0); } +void event_get_pointer_pos(int *x, int *y) { + *x = mousex; + *y = mousey; +} + // Stores a deferred touch event (valid if .timestamp != 0) to add a delay // between mouse pointer movement and mouse button state change caused by a // touch event. This prevents the game from processing a button down event @@ -352,6 +368,127 @@ static void rance4v2_hack(void) { cancel_yield(); } +/* + * Virtual mouse pointer (trackpad-style touch input). + * Active only when nact->ags.virtualpointer is set. Reuses the pointer state + * (mousex/mousey/mouseb, RawKeyInfo) so all game queries are unaffected. + */ +#define VP_SENSITIVITY 1.5f // finger-to-cursor movement multiplier +#define VP_TAP_MAX_MOVE 0.02f // max normalized finger travel for a tap +#define VP_TAP_MAX_TIME 300 // max touch duration (ms) for a tap +#define VP_LONGPRESS_TIME 500 // hold time (ms) to start a drag +#define VP_CLICK_HOLD_TIME 100 // how long (ms) a tap holds the left button + +static bool vp_primary_down; // a finger is tracked as the pointer +static SDL_FingerID vp_primary_id; +static uint32_t vp_down_time; +static float vp_moved_dist; // accumulated normalized finger travel +static bool vp_moved; // travel exceeded the tap threshold +static bool vp_longpress; // left button held due to a long press (drag) +static bool vp_two_finger; // right button held due to a 2nd finger +static bool vp_click_release_pending; +static uint32_t vp_click_release_time; +static bool vp_centered; // cursor has been placed at the center + +static void vp_press(int button) { + mouseb |= 1 << button; + RawKeyInfo[mouse_to_rawkey(button)] = true; + send_agsevent(AGSEVENT_BUTTON_PRESS, mouse_to_agsevent(button)); +} + +static void vp_release(int button) { + mouseb &= ~(1 << button); + RawKeyInfo[mouse_to_rawkey(button)] = false; + send_agsevent(AGSEVENT_BUTTON_RELEASE, mouse_to_agsevent(button)); +} + +static void vp_move(float dnx, float dny) { + mousex += (int)lroundf(dnx * view_w * VP_SENSITIVITY); + mousey += (int)lroundf(dny * view_h * VP_SENSITIVITY); + // Keep the cursor inside the view. + if (mousex < 0) mousex = 0; + else if (mousex >= view_w) mousex = view_w - 1; + if (mousey < 0) mousey = 0; + else if (mousey >= view_h) mousey = view_h - 1; + send_agsevent(AGSEVENT_MOUSE_MOTION, 0); + gfx_dirty = true; +} + +static void vp_finger_down(SDL_TouchFingerEvent *t) { + if (SDL_GetNumTouchFingers(t->touchId) >= 2) { + // A second finger means a right click at the current position. + if (!vp_two_finger) { + vp_two_finger = true; + vp_press(SDL_BUTTON_RIGHT); + } + return; + } + // First finger: start tracking. The cursor moves only on drag. + vp_primary_down = true; + vp_primary_id = t->fingerId; + vp_down_time = SDL_GetTicks(); + vp_moved_dist = 0.0f; + vp_moved = false; +} + +static void vp_finger_motion(SDL_TouchFingerEvent *t) { + if (vp_two_finger || !vp_primary_down || t->fingerId != vp_primary_id) + return; + vp_moved_dist += hypotf(t->dx, t->dy); + if (vp_moved_dist > VP_TAP_MAX_MOVE) + vp_moved = true; + vp_move(t->dx, t->dy); +} + +static void vp_finger_up(SDL_TouchFingerEvent *t) { + if (vp_two_finger) { + // Release the right button once every finger is lifted. + if (SDL_GetNumTouchFingers(t->touchId) == 0) { + vp_two_finger = false; + vp_primary_down = false; + vp_release(SDL_BUTTON_RIGHT); + } + return; + } + if (!vp_primary_down || t->fingerId != vp_primary_id) + return; + vp_primary_down = false; + if (vp_longpress) { + vp_longpress = false; + vp_release(SDL_BUTTON_LEFT); // end drag + } else if (!vp_moved && SDL_GetTicks() - vp_down_time < VP_TAP_MAX_TIME) { + // Tap: left click. Hold the button briefly so polling games detect it. + vp_press(SDL_BUTTON_LEFT); + vp_click_release_pending = true; + vp_click_release_time = SDL_GetTicks() + VP_CLICK_HOLD_TIME; + } +} + +// Time-driven virtual pointer transitions, polled each event pump. +static void vp_tick(void) { + if (!vp_is_enabled()) + return; + uint32_t now = SDL_GetTicks(); + // Place the cursor at the center once the view size is known. + if (!vp_centered && view_w > 0 && view_h > 0) { + mousex = view_w / 2; + mousey = view_h / 2; + vp_centered = true; + gfx_dirty = true; + } + // A stationary long press starts a left-button drag. + if (vp_primary_down && !vp_moved && !vp_longpress && !vp_two_finger && + now - vp_down_time >= VP_LONGPRESS_TIME) { + vp_longpress = true; + vp_press(SDL_BUTTON_LEFT); + } + // Release the briefly-held tap click. + if (vp_click_release_pending && now >= vp_click_release_time) { + vp_click_release_pending = false; + vp_release(SDL_BUTTON_LEFT); + } +} + void event_handle_event(SDL_Event *e) { if (event_custom_handler && event_custom_handler(e)) return; @@ -396,6 +533,10 @@ void event_handle_event(SDL_Event *e) { #endif break; case SDL_MOUSEMOTION: + // Ignore real mouse motion while the virtual pointer is active. This + // also discards the spurious (0,0) motion seen right after startup. + if (vp_is_enabled()) + break; event_set_mouse_internal_location(e->motion.x, e->motion.y); #ifdef _WIN32 win_menu_onMouseMotion(e->motion.x, e->motion.y); @@ -429,6 +570,10 @@ void event_handle_event(SDL_Event *e) { break; case SDL_FINGERDOWN: + if (vp_is_enabled()) { + vp_finger_down(&e->tfinger); + break; + } if (SDL_GetNumTouchFingers(e->tfinger.touchId) >= 2) { mouseb &= ~(1 << SDL_BUTTON_LEFT); mouseb |= 1 << SDL_BUTTON_RIGHT; @@ -455,6 +600,10 @@ void event_handle_event(SDL_Event *e) { break; case SDL_FINGERUP: + if (vp_is_enabled()) { + vp_finger_up(&e->tfinger); + break; + } if (SDL_GetNumTouchFingers(e->tfinger.touchId) == 0) { int ags_button = (mouseb & 1 << SDL_BUTTON_LEFT) ? AGSEVENT_BUTTON_LEFT : AGSEVENT_BUTTON_RIGHT; mousex = e->tfinger.x * view_w; @@ -465,6 +614,10 @@ void event_handle_event(SDL_Event *e) { break; case SDL_FINGERMOTION: + if (vp_is_enabled()) { + vp_finger_motion(&e->tfinger); + break; + } mousex = e->tfinger.x * view_w; mousey = e->tfinger.y * view_h; send_agsevent(AGSEVENT_MOUSE_MOTION, 0); @@ -554,6 +707,7 @@ static void get_event(void) { enum scheduler_event scheduler_event = SCHEDULER_EVENT_INPUT_CHECK_MISS; fire_deferred_touch_event(); + vp_tick(); SDL_Event e; while (SDL_PollEvent(&e)) { diff --git a/src/event.h b/src/event.h index 2634996..76598b5 100644 --- a/src/event.h +++ b/src/event.h @@ -31,6 +31,7 @@ void event_remove(void); void event_set_joy_device_index(int index); void event_set_mouse_location(int x, int y); void event_set_mouse_internal_location(int x, int y); +void event_get_pointer_pos(int *x, int *y); int event_get_key(void); int event_get_mouse(SDL_Point *p); void event_get_wheel(int *forward, int *back); diff --git a/src/gfx_draw.c b/src/gfx_draw.c index 298320d..553312b 100644 --- a/src/gfx_draw.c +++ b/src/gfx_draw.c @@ -41,6 +41,7 @@ #include "image.h" #include "nact.h" #include "debugger.h" +#include "virtual_pointer.h" static void gfx_pal_check(void) { if (nact->ags.pal_changed) { @@ -60,6 +61,7 @@ void gfx_updateScreen(void) { return; SDL_RenderClear(gfx_renderer); SDL_RenderCopy(gfx_renderer, gfx_texture, NULL, NULL); + vp_draw(gfx_renderer); SDL_RenderPresent(gfx_renderer); gfx_dirty = false; } diff --git a/src/virtual_pointer.c b/src/virtual_pointer.c new file mode 100644 index 0000000..a270a48 --- /dev/null +++ b/src/virtual_pointer.c @@ -0,0 +1,105 @@ +/* + * virtual_pointer.c Trackpad-style virtual mouse pointer for touch devices + * + * Copyright (C) 2026 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +*/ + +#include "config.h" + +#include +#include +#include +#include + +#include "portab.h" +#include "system.h" +#include "nact.h" +#include "event.h" +#include "virtual_pointer.h" + +#include "bitmaps/virtual_cursor.xpm" + +bool vp_is_enabled(void) { + return nact->ags.virtualpointer; +} + +// Builds an ARGB surface from the (simple, 1 char/pixel) XPM array above. +static SDL_Surface *create_cursor_surface(void) { + int w, h, ncolors, cpp; + if (sscanf(virtual_cursor[0], "%d %d %d %d", &w, &h, &ncolors, &cpp) != 4 || cpp != 1) { + WARNING("virtual_pointer: unexpected cursor bitmap header"); + return NULL; + } + + // Map each character to an ARGB8888 value. + Uint32 colormap[256] = {0}; + for (int i = 0; i < ncolors; i++) { + const char *line = virtual_cursor[1 + i]; + unsigned char ch = (unsigned char)line[0]; + // Format: " c ", value is "#rrggbb" or "None". + const char *val = line + 4; + if (val[0] == '#') { + unsigned int rgb = (unsigned int)strtoul(val + 1, NULL, 16); + colormap[ch] = 0xff000000u | rgb; // opaque + } else { + colormap[ch] = 0; // transparent + } + } + + SDL_Surface *s = SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, SDL_PIXELFORMAT_ARGB8888); + if (!s) + return NULL; + const char **pixels = &virtual_cursor[1 + ncolors]; + for (int y = 0; y < h; y++) { + Uint32 *row = (Uint32 *)((Uint8 *)s->pixels + y * s->pitch); + const char *src = pixels[y]; + for (int x = 0; x < w; x++) + row[x] = colormap[(unsigned char)src[x]]; + } + return s; +} + +// Lazily-created cursor texture. The hotspot (arrow tip) is at its top-left +// corner, so it is drawn with its origin at the pointer position. +static SDL_Texture *cursor_texture; +static int cursor_w, cursor_h; + +void vp_draw(SDL_Renderer *renderer) { + if (!vp_is_enabled()) + return; + + if (!cursor_texture) { + SDL_Surface *s = create_cursor_surface(); + if (!s) + return; + cursor_w = s->w; + cursor_h = s->h; + cursor_texture = SDL_CreateTextureFromSurface(renderer, s); + SDL_FreeSurface(s); + if (!cursor_texture) + return; + SDL_SetTextureBlendMode(cursor_texture, SDL_BLENDMODE_BLEND); + } + + int x, y; + event_get_pointer_pos(&x, &y); + // The renderer's logical size is set to the game view, so drawing in view + // coordinates positions the cursor exactly at the game's mouse coordinates. + SDL_Rect dst = { x, y, cursor_w, cursor_h }; + SDL_RenderCopy(renderer, cursor_texture, NULL, &dst); +} diff --git a/src/virtual_pointer.h b/src/virtual_pointer.h new file mode 100644 index 0000000..de69c03 --- /dev/null +++ b/src/virtual_pointer.h @@ -0,0 +1,37 @@ +/* + * virtual_pointer.h Trackpad-style virtual mouse pointer for touch devices + * + * Copyright (C) 2026 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +*/ +#ifndef __VIRTUAL_POINTER_H__ +#define __VIRTUAL_POINTER_H__ + +#include +#include + +/* Returns true if the virtual mouse pointer feature is enabled. */ +bool vp_is_enabled(void); + +/* + * Draws the virtual cursor on top of the presented frame. No-op when the + * feature is disabled. Called from gfx_updateScreen() between the game image + * RenderCopy and RenderPresent. + */ +void vp_draw(SDL_Renderer *renderer); + +#endif /* __VIRTUAL_POINTER_H__ */ diff --git a/src/xsystem35.c b/src/xsystem35.c index d6a6cb9..5d44caa 100644 --- a/src/xsystem35.c +++ b/src/xsystem35.c @@ -145,6 +145,7 @@ static void sys35_usage(bool verbose) { puts(" -fullscreen : start with fullscreen"); puts(" -integerscale : use integer scaling when resizing"); puts(" -noimagecursor : disable image cursor"); + puts(" -virtualpointer : enable the virtual mouse pointer (for touch)"); puts(" -version : show version"); puts(" -h : show this message"); puts(" --help : show this message"); @@ -286,6 +287,8 @@ static void sys35_ParseOption(int *argc, char **argv) { } } else if (0 == strcmp(argv[i], "-noimagecursor")) { nact->ags.noimagecursor = true; + } else if (0 == strcmp(argv[i], "-virtualpointer")) { + nact->ags.virtualpointer = true; } else if (0 == strcmp(argv[i], "-debuglv")) { if (argv[i + 1] != NULL) { sys_set_debug_level(argv[i + 1][0] - '0'); @@ -380,6 +383,9 @@ static void check_profile() { /* disable image cursor */ get_boolean_profile("no_imagecursor", &nact->ags.noimagecursor); + /* enable the virtual mouse pointer */ + get_boolean_profile("virtualpointer", &nact->ags.virtualpointer); + /* enable integer scaling */ get_boolean_profile("integerscale", &integer_scaling);