diff --git a/android/app/build.gradle b/android/app/build.gradle index 78a6e78..6f91496 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -171,7 +171,7 @@ android { "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \ "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \ - "-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c" + "-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c" abiFilters 'arm64-v8a' } } diff --git a/android/app/src/main/cpp/xr_input.c b/android/app/src/main/cpp/xr_input.c index 5b61df8..086a0ea 100644 --- a/android/app/src/main/cpp/xr_input.c +++ b/android/app/src/main/cpp/xr_input.c @@ -7,6 +7,7 @@ #include +#include "xr_mouse.h" #include "xr_overlay.h" #include "xr_session.h" #include "xr_swapchain.h" @@ -53,6 +54,7 @@ static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE}; static bool g_prev_select[2] = {false, false}; static bool g_prev_menu = false; static bool g_game_prev_hit[2] = {false, false}; +static bool g_game_touch_active[2] = {false, false}; static bool g_menu_prev_hit[2] = {false, false}; static bool g_menu_touch_active[2] = {false, false}; static bool g_keyboard_prev_hit[2] = {false, false}; @@ -524,12 +526,17 @@ static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float // Hit-tests this hand's ray against the game quad (xr_get_game_quad_extent()) // - the lowest-priority target, tried only once neither the keyboard nor -// menu overlay claimed the ray this frame. There's no touch/drag state -// for the game quad yet (see the file comment above) - this purely drives -// the laser-beam visual, symmetric with xr_input_try_overlay()'s `hit` -// term but without a touch/drag component. Returns plain `hit`. +// menu overlay claimed the ray this frame (or, symmetrically, once this +// hand already has a pending mouse-down on the game quad - see +// g_game_touch_active and the keyboard/menu call sites in +// xr_input_sync_and_draw()). outU/outV (only meaningful when the +// returned hit is true) let the caller drive the game's own mouse cursor +// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual +// clicks is the caller's responsibility (xr_mouse_click()), not this +// function's, unlike xr_input_try_overlay(). static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy, - float fz, bool *outPlaneHit, float *outPlaneDistance) { + float fz, bool *outPlaneHit, float *outPlaneDistance, + float *outU, float *outV) { float distance, halfWidth, halfHeight; xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight); @@ -548,6 +555,8 @@ static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, fl } *outPlaneHit = planeHit; *outPlaneDistance = t; + *outU = u; + *outV = v; if (hit != g_game_prev_hit[hand]) { LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand], @@ -671,10 +680,18 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { // xr_input_try_overlay()'s doc comment) - the game quad itself. // Whichever target claims the ray gets a laser-beam quad built for // it (if a head pose is available and the ray actually crosses - // that target's plane) - see xr_input_build_beam(). + // that target's plane) - see xr_input_build_beam(). Keyboard/menu + // are additionally gated on !g_game_touch_active[hand]: a pending + // mouse-down on the game quad (trigger still held since a + // down-edge dispatched there) must keep claiming the ray even if + // it strays onto another panel before release, the cross-target + // analogue of xr_input_try_overlay()'s own hadTouch/hadDrag - the + // eventual mouse-up has to reach the game quad, not whatever the + // ray happens to be over on the release frame. bool keyboardPlaneHit = false; float keyboardPlaneDistance = 0.0f; - if (xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz, + if (!g_game_touch_active[hand] && + xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz, selectDownEdge, selectUpEdge, &g_keyboard_touch_active[hand], &g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU, &keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit, @@ -686,7 +703,8 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { } bool menuPlaneHit = false; float menuPlaneDistance = 0.0f; - if (xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge, + if (!g_game_touch_active[hand] && + xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge, selectUpEdge, &g_menu_touch_active[hand], &g_menu_prev_hit[hand], "menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit, &menuPlaneDistance)) { @@ -696,13 +714,36 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { continue; } - // Lowest priority: the game quad itself - purely visual (beam - // only, same as the other two targets), no click/touch dispatch - // here yet (see the file comment above). + // Lowest priority (for a fresh hit): the game quad itself - drives + // the engine's own mouse cursor/clicks via xr_mouse.h, exactly + // like a desktop mouse (absolute position + left button), plus the + // same laser-beam visual the other two targets get. bool gamePlaneHit = false; - float gamePlaneDistance = 0.0f; - bool gameHit = - xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit, &gamePlaneDistance); + float gamePlaneDistance = 0.0f, gameU = 0.0f, gameV = 0.0f; + bool gameHit = xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit, + &gamePlaneDistance, &gameU, &gameV); + if (gameHit) { + int gameWidth, gameHeight; + xr_get_game_resolution(&gameWidth, &gameHeight); + int px = (int)(gameU * (float)gameWidth); + int py = (int)(gameV * (float)gameHeight); + if (px < 0) + px = 0; + else if (px >= gameWidth) + px = gameWidth - 1; + if (py < 0) + py = 0; + else if (py >= gameHeight) + py = gameHeight - 1; + xr_mouse_move(px, py); + } + if (selectDownEdge && gameHit) { + xr_mouse_click(true); + g_game_touch_active[hand] = true; + } else if (selectUpEdge && g_game_touch_active[hand]) { + xr_mouse_click(false); + g_game_touch_active[hand] = false; + } // gameHit implies gamePlaneHit (both only ever set together above), // kept as a separate out-param for symmetry with // xr_input_try_overlay()'s outPlaneHit/outPlaneDistance pair. @@ -746,6 +787,7 @@ void xr_input_shutdown(void) { g_session = XR_NULL_HANDLE; memset(g_prev_select, 0, sizeof(g_prev_select)); memset(g_game_prev_hit, 0, sizeof(g_game_prev_hit)); + memset(g_game_touch_active, 0, sizeof(g_game_touch_active)); memset(g_menu_prev_hit, 0, sizeof(g_menu_prev_hit)); memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active)); memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit)); diff --git a/android/app/src/main/cpp/xr_input.h b/android/app/src/main/cpp/xr_input.h index d404e0e..3044f35 100644 --- a/android/app/src/main/cpp/xr_input.h +++ b/android/app/src/main/cpp/xr_input.h @@ -6,13 +6,18 @@ // this frame (landing on it now, or continuing a touch/drag begun on a // previous frame - not merely because that overlay happens to be visible) // - the game quad xr_session.c submits, forwarding trigger edges to -// whichever overlay claims the ray as synthetic touches. Since none of -// the three targets' own feedback (the overlays' on-quad cursor, drawn by -// their Java views; nothing at all for the game quad, which has no -// click/touch dispatch here yet) gives any indication while the ray is -// short of actually landing on something, every claimed target also -// builds a thin, billboarded laser-beam quad per hand from the controller -// to wherever the ray currently crosses that target's plane (see +// whichever overlay claims the ray as synthetic touches. The game quad +// itself acts as a plain desktop-style mouse pointer into the engine's +// own UI (see xr_mouse.h) - absolute cursor position while the ray hits +// it, plus a left click on the trigger edge; a pending click there +// likewise keeps claiming the ray ahead of the keyboard/menu until +// released, so the eventual mouse-up isn't lost if the ray strays. Since +// none of the three targets' own feedback (the overlays' on-quad cursor, +// drawn by their Java views; the engine's own mouse cursor sprite for the +// game quad) gives any indication while the ray is short of actually +// landing on something, every claimed target also builds a thin, +// billboarded laser-beam quad per hand from the controller to wherever +// the ray currently crosses that target's plane (see // xr_input_get_beam_layer()) - so aiming looks and behaves the same // whether the target is an overlay or the game quad itself. #ifndef QUESTSHOCK_XR_INPUT_H diff --git a/android/app/src/main/cpp/xr_mouse.c b/android/app/src/main/cpp/xr_mouse.c new file mode 100644 index 0000000..af77268 --- /dev/null +++ b/android/app/src/main/cpp/xr_mouse.c @@ -0,0 +1,20 @@ +#include "xr_mouse.h" + +#include + +#include "mouse.h" // engine/src/Libraries/INPUT/Source/mouse.h - mouse_put_xy() + +void xr_mouse_move(int x, int y) { mouse_put_xy((short)x, (short)y); } + +void xr_mouse_click(bool down) { + SDL_Event event; + SDL_zero(event); + event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; + event.button.timestamp = SDL_GetTicks(); + event.button.windowID = 0; + event.button.which = 0; + event.button.button = SDL_BUTTON_LEFT; + event.button.state = down ? SDL_PRESSED : SDL_RELEASED; + event.button.clicks = 1; + SDL_PushEvent(&event); +} diff --git a/android/app/src/main/cpp/xr_mouse.h b/android/app/src/main/cpp/xr_mouse.h new file mode 100644 index 0000000..263f217 --- /dev/null +++ b/android/app/src/main/cpp/xr_mouse.h @@ -0,0 +1,35 @@ +// Bridges VR aim-ray hit-testing (xr_input.c) to the vendored engine's +// mouse input (engine/src/Libraries/INPUT/Source/mouse.h) - lets the +// controller's laser pointer drive the game's own UI (inventory, menus, +// dialogs) exactly like a desktop mouse: absolute cursor position plus a +// single left button, no relative/mouselook mode, no right button. +#ifndef QUESTSHOCK_XR_MOUSE_H +#define QUESTSHOCK_XR_MOUSE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Sets the engine's absolute mouse position (mouse_put_xy()) - call every +// frame the aim ray is actually hitting the game quad; x/y are pixel +// coordinates in the game's own logical resolution (see +// xr_get_game_resolution() in xr_session.h), already clamped by the +// caller. Do not call on frames the ray isn't hitting the game quad - the +// cursor should hold its last position, not snap elsewhere. +void xr_mouse_move(int x, int y); + +// Pushes a synthetic SDL_MOUSEBUTTONDOWN/UP (SDL_BUTTON_LEFT) - matches +// KeyboardOverlay's nativeSendPrintableChar() in questshock_native.c: a +// plain SDL_Event built by hand and handed to the public SDL_PushEvent(), +// no engine patch needed. pump_events() (sdl_events.c) reads whatever +// mouse_put_xy() last set, not any x/y carried on the event itself - call +// xr_mouse_move() first if position needs updating this frame. +void xr_mouse_click(bool down); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/android/app/src/main/cpp/xr_session.c b/android/app/src/main/cpp/xr_session.c index 805ee9f..d785a7b 100644 --- a/android/app/src/main/cpp/xr_session.c +++ b/android/app/src/main/cpp/xr_session.c @@ -469,3 +469,8 @@ void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half *half_height_m = QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width; } + +void xr_get_game_resolution(int *width, int *height) { + *width = g_game_swapchain.width; + *height = g_game_swapchain.height; +} diff --git a/android/app/src/main/cpp/xr_session.h b/android/app/src/main/cpp/xr_session.h index b4ea2df..943c213 100644 --- a/android/app/src/main/cpp/xr_session.h +++ b/android/app/src/main/cpp/xr_session.h @@ -74,6 +74,13 @@ void xr_shutdown(void); // laser pointer always matches whatever's actually visible. void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m); +// Pixel resolution of the game quad's own swapchain - i.e. game_width/ +// game_height as passed to xr_init() (grd_cap->w/h, see Shock.c's +// InitSDL()) - the logical coordinate space xr_input.c needs to convert a +// game-quad ray hit's normalized u,v into engine mouse coordinates (see +// xr_mouse.h). +void xr_get_game_resolution(int *width, int *height); + #ifdef __cplusplus } #endif