From 9a2b8b8178c5ebf399d68f66853ee931d7ca491c Mon Sep 17 00:00:00 2001 From: bigmak94 <196841780+bigmak94@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:42:32 +0200 Subject: [PATCH] The touchpad's gestures on buttons, and a stick's finger the game can follow The first worn try of the Quest app with the headset's own controllers ended at the catapult of the first level: the right stick pulled back and let go did not shoot it. And nothing told the player that the pad is pressed (the water cannon) by pressing that stick in. Why the catapult failed: the game reads the pad once for every frame it draws (the newest of the states it is handed), and takes the place it first finds the finger at for the start of a pull. A stick is at its end within a few hundredths of a second, so the finger that followed it there had, for the game, begun where it ended. By a model of the game's rule (tools/tests/touch_game_model.h: read at 0xc04cc3, y = 1 - raw / 950, the catapult of 0x436fc0 wants more than 0.25 s and more than 0.4 of the pad) one fast pull in four failed at 60 frames a second and two in three at 30, which is what the reports of Issue #2 say and what the headset draws. - Input::MovedFinger: a finger that stays where it lands until the game has seen it there, goes no faster than a finger flicks and counts as arrived once it was seen at its end. - Input::StickFinger is built on it: the finger comes down behind the pad's middle, on the side the stick comes from, and is dragged to where the stick points. - Input::PadGestures: what the game asks of the pad, done by buttons it has no use for in play (it only asks about them in a few menus, and is still told of them): the pad pressed while a button is held, a swipe forward for each press of another, a pull back that is held and let go with a third. On the headset's own controllers the trigger and the grip of the hand that holds the controller in the game and the other hand's trigger; on a gamepad without a touchpad R2, R1 and L2. In the Quest app's router, the PC host's code for the headset's controllers and the PC's gamepads. - The Quest app shows a card over the game that names those buttons, when the game starts and whenever OPTIONS is pressed (the game's pause). - SHADPS4_PAD_TOUCH_TRACE=1 has the log say what the game was given of the touchpad at each of its reads; input scripts can press the gesture buttons (gesture=press|swipe|pull); the Quest app's dry start pulls the right stick and presses R1. Tried: the tests (stick_finger_test and pad_gestures_test against the model at 26 to 60 frames a second, pad_router_test); with the trace, in the game on the PC (62 reads a second) and on a Quest 3 without wearing it (48 in its sandbox, 36 through the whole app): a pull is given as 0.74 to 0.76 of the pad over 0.34 s and more, a swipe as 0.67 over a dozen reads, a press for as long as it is held. Not tried: the catapult itself going off (a script did not get through the level to it), and all of it worn. Co-Authored-By: Claude Opus 5.5 --- README-PC-VR.md | 44 ++-- README-QUEST-VR.md | 70 ++++-- README.md | 23 +- pc-vr/launch.ps1 | 8 +- quest-host/cpp/main.cpp | 28 ++- quest-host/cpp/pad_router.h | 219 ++++++++++++------ quest-host/cpp/xr_host.cpp | 13 +- quest-host/cpp/xr_host.h | 2 + .../astrobotquest/vrhost/MainActivity.java | 86 ++++++- .../astrobotquest/vrhost/SandboxShell.java | 26 ++- .../src/core/libraries/pad/pad.cpp | 24 ++ .../src/core/vr/openxr_host.cpp | 52 +++-- shadps4-arm64-main/src/input/controller.cpp | 49 +++- shadps4-arm64-main/src/input/controller.h | 4 + shadps4-arm64-main/src/input/moved_finger.h | 86 +++++++ shadps4-arm64-main/src/input/pad_gestures.h | 131 +++++++++++ .../src/input/scripted_input.cpp | 28 ++- shadps4-arm64-main/src/input/scripted_input.h | 5 +- shadps4-arm64-main/src/input/stick_finger.h | 59 +++-- tools/quest-drystart.sh | 2 +- tools/tests/pad_gestures_test.cpp | 155 +++++++++++++ tools/tests/pad_router_test.cpp | 166 ++++++++++++- tools/tests/stick_finger_test.cpp | 73 +++++- tools/tests/touch_game_model.h | 106 +++++++++ 24 files changed, 1260 insertions(+), 199 deletions(-) create mode 100644 shadps4-arm64-main/src/input/moved_finger.h create mode 100644 shadps4-arm64-main/src/input/pad_gestures.h create mode 100644 tools/tests/pad_gestures_test.cpp create mode 100644 tools/tests/touch_game_model.h diff --git a/README-PC-VR.md b/README-PC-VR.md index 8021c81..f1ff7d5 100644 --- a/README-PC-VR.md +++ b/README-PC-VR.md @@ -120,13 +120,22 @@ In the game: face is straight ahead. The emulator does it by itself when ✕ is pressed for the first time after the headset came up, and when the headset's own "reset view" is used (hold the Meta button). -- A controller without a touchpad (an Xbox pad): the right stick stands in for the finger, - and the Back / View button for the click. Push the stick the way you would swipe; for the - catapult at the end of a level (and the slingshot later on), **pull the stick towards you - and let go**: the finger lifts where the stick was pulled to, as a finger would, and the - shot goes off. The right stick does that with a DualSense as well while nothing touches - its pad; the game has no other use for it. (A stick that drifts, never resting at its - centre, is left out of this: it would hold a finger on the pad for good.) +- **A controller without a touchpad** (an Xbox pad, the headset's own controllers) has + buttons for what the game wants done on one: + - **right trigger (R2): the pad pressed**, for as long as the trigger is pulled. That is + how the water cannon and the machine gun fire. + - **right shoulder button (R1; the right grip of VR controllers): a swipe forward**, once + for each press. That shoots the hook, throws the stars, opens the chests. + - **left trigger (L2): a pull back** that is held while the trigger is, and let go when it + is. That is the catapult at the end of every level: point the controller at the goal, + pull, let go. It also pulls on the hook's rope. + + The game has no use for these buttons while it is played, and is still told of them (a few + of its menus have). Besides them, the right stick is a finger on the pad, for anything + else: it comes down behind the pad's middle, is dragged the way the stick is pushed, and + lifts where the stick was let go; the Back / View button presses the pad. The right stick + does that with a DualSense as well while nothing touches its pad. (A stick that drifts, + never resting at its centre, is left out: it would hold a finger on the pad for good.) - **Where the game has you blow** into the controller's microphone: blow at the headset's microphone, or hold the **PS button and □** together, which blows for as long as they are held. The console window says how loud the game heard the last ten seconds whenever it @@ -172,11 +181,13 @@ While no gamepad is connected to the PC, the two Touch controllers are the gamep | B (right hand) | □ (punch) | | X (left hand) | ○ (back) | | Y (left hand) | △ | -| right stick | the finger on the touchpad: push it the way you would swipe; pull it towards you and let go for the catapult at the end of a level | +| right trigger | the touchpad pressed, for as long as it is pulled (water, guns); also R2 | +| right grip | a swipe forward on the touchpad, once (hook, stars, chests); also R1 | +| left trigger | a pull back on the touchpad, let go when the trigger is (the catapult at the end of a level); also L2 | +| right stick | a finger on the touchpad, dragged the way the stick is pushed | | right stick pressed in | the touchpad pressed | | X and Y together (left hand) | blowing into the microphone, for as long as they are held | -| triggers | L2, R2 | -| grips | L1, R1 | +| left grip | L1 | | menu button (left hand) | OPTIONS | | left stick pressed in | L3 | | both sticks pressed in | resets the view | @@ -462,9 +473,16 @@ Things that had to be right, for whoever works on this again: memory. The console's memory is now set aside before the headset is looked for (found by evertec82). And a session that is asked for no pictures is only made anew for Virtual Desktop's runtime, which needs it; SteamVR's are left alone. -- **The catapult at the end of a level with a stick** (#2): see "In the game". It took luck - before: the game shoots by where the finger is when it lifts, and a stick let go took the - finger back to the middle first. +- **The catapult at the end of a level, and the rest of the touchpad, without a touchpad** + (#2): see "In the game". The catapult took luck with a stick, for two reasons. The game + shoots by where the finger is when it lifts, and a stick let go took the finger back to the + middle first. And the game looks at the pad once for every frame it draws: a stick is at + its end within a few hundredths of a second, so the pull began, for the game, where it + ended, the more often the fewer frames it drew (by a model of the game's own rule, one + fast pull in four failed at 60 frames a second and two in three at 30). The stick's finger + now stays where it comes down until the game has seen it there and moves no faster than a + finger does; and three buttons do the three things the game asks of the pad outright: + press it, swipe forward, pull back and let go. - **The right stick no longer holds a finger on a real touchpad** (#12): a stick that drifts touched the pad for good, and a real finger's swipes then counted for nothing. If swipes on a DualSense's touchpad still do nothing, the console window tells what Windows hands diff --git a/README-QUEST-VR.md b/README-QUEST-VR.md index 9d9b506..455e94e 100644 --- a/README-QUEST-VR.md +++ b/README-QUEST-VR.md @@ -103,9 +103,18 @@ it has the field-of-view menu and the fixes of 0.14 to 0.17 above, and - **a gamepad without motion sensors is turned and tilted in the game by the hands that hold it.** Before, it followed the hands to where they were and kept pointing straight ahead, which left the game's gadgets nothing to aim with. - - **the right stick as the touchpad's finger lifts where the stick is let go**, as on the - PC, which is what the catapult at the end of a level needs: pull the stick towards you - and let go. A button that presses the touchpad touches it as well. + - **what the game wants done on the touchpad is on buttons**, on the headset's + controllers and on every gamepad that has no touchpad: the right trigger (R2) presses + it, the right grip (R1) swipes forward, the left trigger (L2) pulls back and lets go, + which is the catapult at the end of every level. A card in the headset says so when the + game starts and whenever OPTIONS is pressed. (The first worn try of this version ended + at that catapult: the stick alone did not shoot it. The game looks at the pad once for + every frame it draws, thirty times a second here, and a stick is at its end before it + has looked twice: the pull began, for the game, where it ended.) + - **the right stick as the touchpad's finger** is one the game can follow now: it stays + where it comes down until the game has seen it there, moves no faster than a finger + does, and lifts where the stick was let go. A button that presses the touchpad touches + it as well. - smaller things for gamepads that are not PlayStation's: a right stick that reports on other axes, triggers that are only buttons, a Back button the system would take for its own "back" and close the app with. @@ -269,11 +278,29 @@ light bar. Where it is in the game is where the headset sees the hands that hold 8BitDo, Logitech, Razer, PDP, Hori and others; nothing but a DualSense has been tried) lack two things the game is built around, which are made up for: -- *the touchpad.* The right stick, for which this game has no use, moves a finger over it: the - finger comes down in the middle of the pad, goes where the stick goes, and lifts where the - stick is let go (not back in the middle, where a stick flies to). That is what the catapult - at the end of a level needs: pull the stick towards you and let go. The Create / Back / View - button presses the pad. (`stick_touchpad=0` leaves the stick a stick.) +- *the touchpad.* The game asks three things of it, and each is on a button the game has no + use for while it is played (it is still told of them: a few of its menus have): + - **R2, the right trigger: the pad pressed**, for as long as the trigger is pulled. The + water cannon and the machine gun fire that way. + - **R1: a swipe forward**, once for each press. That shoots the hook, throws the stars, + opens the chests. + - **L2, the left trigger: a pull back**, held while the trigger is and let go when it is. + That is the catapult at the end of every level (point the controller at the goal: the + game only shoots within 25 degrees of it), and it pulls on the hook's rope. + + A card in the headset names them when the game starts and again whenever OPTIONS is + pressed, which is the game's pause. Besides them the right stick, for which this game has + no use either, moves a finger over the pad for anything else: the finger comes down behind + the pad's middle, is dragged the way the stick is pushed, and lifts where the stick is let + go (not back in the middle, where a stick flies to). The Create / Back / View button + presses the pad. (`stick_touchpad=0` leaves the stick a stick.) + + Why buttons, and why the finger is slow: the game looks at the pad once for every frame it + draws, thirty times a second in a level here, and makes a pull or a swipe of where it saw + the finger first and last. A stick is at its end within a few hundredths of a second, and + a finger that follows it there has not moved at all as far as the game can tell. So every + finger the app moves stays where it lands until the game has seen it, goes no faster than + a real one flicks, and lifts only when the game has seen it at its end. - *the motion sensors*, by which the game knows how the controller is turned: its gadgets aim with it. The hands holding the gamepad stand in: the line from one palm to the other is the gamepad's sideways axis, which gives how it is turned and how far it is rolled, and the way @@ -296,15 +323,18 @@ system makes of them for other apps says neither, and is ignored): | A, B (right) | ✕, □ | | X, Y (left) | ○, △ | | X and Y together | blowing into the microphone, for as long as they are held | -| grips | L1, R1 | -| triggers | L2, R2 | -| right stick | the finger on the touchpad, as on a gamepad without one | +| right trigger | the touchpad pressed, while it is pulled (water, guns); also R2 | +| right grip | a swipe forward on the touchpad (hook, stars, chests); also R1 | +| left trigger | a pull back on the touchpad, let go when the trigger is (the catapult); also L2 | +| left grip | L1 | +| right stick | a finger on the touchpad, as on a gamepad without one | | right stick pressed in | the touchpad pressed | | menu button (left) | OPTIONS; held for a second, it resets the view | | both sticks pressed in | resets the view | -Both controllers shake with the game's rumble, the left one more with the heavy motor and -the right one more with the light one. +(With `pad_hand=left` the trigger and the grip that press and swipe are the left ones, and +the right trigger pulls.) Both controllers shake with the game's rumble, the left one more +with the heavy motor and the right one more with the light one. **With a gamepad and the headset's controllers both at hand, the game is played with the one that was used last**: a button on the other takes over, and `host.log` says so. From a @@ -724,9 +754,17 @@ session itself: frames on the display, head and hand tracking, the controller. `tools/quest-drystart.sh [settings file]` starts the app's own activity without showing it: everything the app does when it is launched runs (settings, log files, OpenXR up to the idle session, unpacking the runtime, the emulator with the game, sound), a controller that is not -there presses ✕ now and then (through the same code a real one's events go through), and the -`host.log` and `core.log` the app writes are pulled to `build/quest/drystart/` together with -pictures of what its status panel showed and of the start-up menu. +there presses ✕ now and then (through the same code a real one's events go through), pulls +its right stick back once and presses R1 once, and the `host.log` and `core.log` the app +writes are pulled to `build/quest/drystart/` together with pictures of what its status panel +showed, of the start-up menu and of the controls cards. + +`SHADPS4_PAD_TOUCH_TRACE=1` (as `env=` in a settings file for the dry start, or as a setting of +`quest-sandbox-test.sh`, whose input scripts can press the gesture buttons with +`gesture=press`, `gesture=swipe` and `gesture=pull` and move the stick's finger with +`finger=x,y`) has core.log say what the game was given of the touchpad at each of its reads, +for as long as a finger was on it: where the game saw a touch begin and end, and over how +many reads, is what it makes its swipes and pulls of. `quest-host/build.sh --test-package` builds the same app under another name (`com.astrobotquest.vrhost.test`, "Astro VR Host (test)", `build/quest/astro-vr-host-test.apk`). diff --git a/README.md b/README.md index 85fc845..2e7ade6 100644 --- a/README.md +++ b/README.md @@ -121,16 +121,21 @@ Playing: - The app begins with a menu for the field of view: a stick to a side chooses, ✕ / A or a trigger starts the game. - **With a gamepad**: put the Touch controllers aside and hold it. The headset tracks your - hands around it, and that is where the controller is in the game. A gamepad without a - touchpad has its right stick for the finger (for the catapult at the end of a level, pull - the stick towards you and let go) and its Create / Back / View button to press the pad. A - gamepad without motion sensors, which is every one that is not a PlayStation's, is turned - and tilted in the game by your hands, as far as the headset sees them. + hands around it, and that is where the controller is in the game. A gamepad without motion + sensors, which is every one that is not a PlayStation's, is turned and tilted in the game + by your hands, as far as the headset sees them. - **With the Touch controllers**: the right one is the controller in the game, where it is - and the way it points. Left stick to move; A is ✕, B is □, X is ○, Y is △; the grips are L1 - and R1, the triggers L2 and R2; the right stick is the finger on the touchpad, and pressed - in it presses the pad; the left controller's menu button is OPTIONS. X and Y together - blow, where the game wants you to blow into the microphone. + and the way it points. Left stick to move; A is ✕, B is □, X is ○, Y is △; the left + controller's menu button is OPTIONS. X and Y together blow, where the game wants you to + blow into the microphone. +- **The touchpad, on the Touch controllers and on a gamepad that has none**, is on buttons + (a card in the headset says so when the game starts, and again whenever you press + OPTIONS): + - **right trigger (R2): press it and hold**: the water cannon, the machine gun + - **right grip (R1): swipe forward**: the hook, the throwing stars, the chests + - **left trigger (L2): pull back, let go to shoot**: the catapult at the end of every + level (point the controller at the goal) + - the right stick is a finger on it, for anything else. - With both at hand, the game is played with the one you used last: a button on the other changes over. - The first screen asks to move the controller into a floating outline: hold it up in front diff --git a/pc-vr/launch.ps1 b/pc-vr/launch.ps1 index 6607e0f..c901dcb 100644 --- a/pc-vr/launch.ps1 +++ b/pc-vr/launch.ps1 @@ -878,8 +878,12 @@ if ($env:SHADPS4_OPENXR -ne "0" -and [int]$env:SHADPS4_XR_WAIT -gt 0) { Say "Hold OPTIONS for a second (or press the PS button) to reset the view." Say "Where the game wants you to blow: into the headset's microphone, or hold the PS button and square" Say "(X and Y together on VR controllers)." -Say "With VR controllers: the right stick is the touchpad (pull it back and let go to shoot at the end of a level)," -Say "both sticks pressed in reset the view." +Say "With VR controllers, or a gamepad that has no touchpad, the touchpad is on buttons:" +Say " right trigger (R2): press it and hold (water, guns)" +Say " right grip (R1): swipe forward (hook, throwing stars, chests)" +Say " left trigger (L2): pull back, let go to shoot (the catapult at the end of a level)" +Say " right stick: a finger on it, for anything else" +Say "Both sticks of VR controllers pressed in reset the view." Say "Close the game's window to quit." Say "" # The emulator asks Windows for about 14 GB at once (the console's memory, and what the larger diff --git a/quest-host/cpp/main.cpp b/quest-host/cpp/main.cpp index 02f0d08..b88cae7 100644 --- a/quest-host/cpp/main.cpp +++ b/quest-host/cpp/main.cpp @@ -76,6 +76,7 @@ JNIEXPORT void JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeStartXr( env->GetJavaVM(&g_app->vm); g_app->activity = env->NewGlobalRef(activity); g_app->pads.SetStickTouchpad(stick_touchpad == JNI_TRUE); + g_app->pads.SetPadHand(pad_hand); g_app->pads.SetNotice([](const char* text) { LOGI("%s", text); }); g_app->pads.SetSink([app = g_app.get()](const PadRouter::Output& output) { app->core.SetPad(output.pad); @@ -253,10 +254,27 @@ JNIEXPORT void JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeMotion( } /// A gamepad is there to play with (again, or another one than before), or none is any more. +/// `touchpad`: it has a touchpad of its own. JNIEXPORT void JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeSetGamepad( - JNIEnv*, jobject, jboolean connected) { + JNIEnv*, jobject, jboolean connected, jboolean touchpad) { if (g_app) { - g_app->pads.SetGamepadConnected(connected == JNI_TRUE, PadRouter::Now()); + g_app->pads.SetGamepadConnected(connected == JNI_TRUE, touchpad == JNI_TRUE, + PadRouter::Now()); + } +} + +/// How often OPTIONS was pressed in the game so far. +JNIEXPORT jint JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeOptionsPresses(JNIEnv*, + jobject) { + return g_app ? static_cast(g_app->pads.Look().options_presses) : 0; +} + +/// Shows the panel over the game although the game has a picture (for what the player +/// should read while they play), or no longer. +JNIEXPORT void JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeShowPanel(JNIEnv*, jobject, + jboolean show) { + if (g_app) { + g_app->xr_status.show_panel = show == JNI_TRUE; } } @@ -269,14 +287,16 @@ JNIEXPORT void JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeSetPlayi } /// What the game is played with: 0 nothing, 1 the gamepad, 2 the headset's own controllers. -/// 4 is added while those are in the player's hands. +/// 4 is added while those are in the player's hands, 8 where what the game is played with +/// has no touchpad and buttons do its gestures. JNIEXPORT jint JNICALL Java_com_astrobotquest_vrhost_MainActivity_nativeController(JNIEnv*, jobject) { if (!g_app) { return 0; } const PadRouter::View view = g_app->pads.Look(); - return static_cast(view.source) | (view.touch_present ? 4 : 0); + return static_cast(view.source) | (view.touch_present ? 4 : 0) | + (view.gesture_buttons ? 8 : 0); } /// The player chose with it in the host's menu: 1 the gamepad, 2 the headset's controllers. diff --git a/quest-host/cpp/pad_router.h b/quest-host/cpp/pad_router.h index f773aa8..aab78e7 100644 --- a/quest-host/cpp/pad_router.h +++ b/quest-host/cpp/pad_router.h @@ -10,7 +10,9 @@ #include #include "pad_state.h" -// From the emulator (src/input): the right stick as a finger on the touchpad. +// From the emulator (src/input): the right stick as a finger on the touchpad, and buttons +// that do what a finger does there. +#include "pad_gestures.h" #include "stick_finger.h" /// PS4 pad button bits (ORBIS_PAD_BUTTON_*). @@ -55,6 +57,13 @@ struct TouchState { /// and R2, the grips L1 and R1, the left controller's menu button is OPTIONS. Both sticks /// pressed in reset the view. /// +/// What the game wants done on the touchpad is also on buttons, for the headset's +/// controllers and for every gamepad that has no touchpad (see Input::PadGestures): the +/// trigger of the hand that holds the controller in the game (R2 on a gamepad) presses the +/// pad for as long as it is pulled, the grip of that hand (R1) swipes forward once, and the +/// other hand's trigger (L2) pulls back, holds, and lets go when it is let go. The game has +/// no use for those buttons in play; it is still told of them, for the menus that have. +/// /// With both a gamepad and the headset's controllers at hand, the game is played with the /// one that was used last: a button on the other takes over. A gamepad is taken over from /// only by what a hand cannot do without holding a controller (a button under a thumb, a @@ -91,6 +100,10 @@ public: bool gamepad_motion{}; float gamepad_accel[3]{}; bool touch_present{}; + /// What the game is played with has no touchpad: buttons do its gestures. + bool gesture_buttons{}; + /// How often OPTIONS was pressed in the game so far. + uint32_t options_presses{}; }; /// The time everything here is told, in seconds: the same clock for every caller. @@ -116,24 +129,31 @@ public: stick_touchpad = enabled; } + /// Which of the headset's own controllers is the controller in the game: 1 the right + /// one, 0 the left. Its trigger and grip are the ones that press and swipe. + void SetPadHand(int hand) { + std::scoped_lock lock{mutex}; + pad_hand = hand == 0 ? 0 : 1; + } + /// The game runs, and what the player presses is meant for it. Until then the controls /// belong to the host's own menu, and nothing of them reaches the game: neither does /// what is still held down from there, until it is let go. void SetPlaying(bool playing_, double now) { std::scoped_lock lock{mutex}; if (playing_ && !playing) { - playing = true; - held_back = 0; - held_back = Compose(now, false).pad.buttons; + held_back = Read().buttons; } playing = playing_; Publish(now); } /// A gamepad is there (again, or another one than before), or the last one is gone. - void SetGamepadConnected(bool connected, double now) { + /// `touchpad`: it has a touchpad of its own, and needs no buttons to stand in for one. + void SetGamepadConnected(bool connected, bool touchpad, double now) { std::scoped_lock lock{mutex}; gamepad_connected = connected; + gamepad_touchpad = connected && touchpad; // Its motion sensors, if it has any, speak up within milliseconds. gamepad_motion = false; if (!connected) { @@ -204,6 +224,9 @@ public: view.gamepad_motion = gamepad_motion; std::copy(gamepad_accel, gamepad_accel + 3, view.gamepad_accel); view.touch_present = touch.present; + view.gesture_buttons = + source == Source::Touch || (source == Source::Gamepad && !gamepad_touchpad); + view.options_presses = options_presses; return view; } @@ -265,8 +288,9 @@ private: return; } source = wanted; - // What the other one's stick was doing on the touchpad is over. + // What the other one was doing on the touchpad is over. stick_finger.Reset(); + gestures.Reset(); options_since = -1.0; options_fired = false; view_reset_held = false; @@ -286,93 +310,136 @@ private: std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f)); } - /// What the game is given now. With `act`, what the buttons mean besides (resets of the - /// view) is acted on as well. - Output Compose(double now, bool act) { - Output out; - if (!playing) { - return out; - } - uint32_t buttons = 0; - float left_x = 0.0f, left_y = 0.0f, right_x = 0.0f, right_y = 0.0f; - float left_trigger = 0.0f, right_trigger = 0.0f; - bool real_finger = false; - float touch_x = 0.5f, touch_y = 0.5f; - bool view_reset = false; - bool blow = false; + /// What the controller the game is played with says right now, as a gamepad would say it. + struct Reading { + uint32_t buttons{}; + float left_x{}, left_y{}, right_x{}, right_y{}; + float left_trigger{}, right_trigger{}; + /// A real finger on a real touchpad. + bool finger{}; + float finger_x{0.5f}, finger_y{0.5f}; + bool view_reset{}; + bool blow{}; + /// The buttons that do the touchpad's gestures, and which button each of them is. + Input::PadGestures::Controls gestures; + uint32_t press_button{}, swipe_button{}, pull_button{}; + }; + Reading Read() const { + Reading read; if (source == Source::Touch) { const TouchState& t = touch; const auto add = [&](bool down, uint32_t button) { if (down) { - buttons |= button; + read.buttons |= button; } }; add(t.a, PadButton::Cross); add(t.b, PadButton::Square); // The left controller's two buttons together are not circle and triangle: they // blow into the microphone, for as long as they are held. - blow = t.x && t.y; - add(t.x && !blow, PadButton::Circle); - add(t.y && !blow, PadButton::Triangle); + read.blow = t.x && t.y; + add(t.x && !read.blow, PadButton::Circle); + add(t.y && !read.blow, PadButton::Triangle); add(t.menu, PadButton::Options); add(t.left_grip > 0.5f, PadButton::L1); add(t.right_grip > 0.5f, PadButton::R1); // Both sticks pressed in is not something a game is played with: it resets the // view, for which a gamepad has its PS button. - view_reset = t.left_stick_in && t.right_stick_in; - add(t.left_stick_in && !view_reset, PadButton::L3); - add(t.right_stick_in && !view_reset, PadButton::TouchPad); + read.view_reset = t.left_stick_in && t.right_stick_in; + add(t.left_stick_in && !read.view_reset, PadButton::L3); + add(t.right_stick_in && !read.view_reset, PadButton::TouchPad); // (A trigger that is not quite at rest is not pulled: any pull counts as the // button.) - left_trigger = t.left_trigger < 0.12f ? 0.0f : t.left_trigger; - right_trigger = t.right_trigger < 0.12f ? 0.0f : t.right_trigger; - add(left_trigger > 0.0f, PadButton::L2); - add(right_trigger > 0.0f, PadButton::R2); - left_x = t.left_x; - left_y = -t.left_y; - right_x = t.right_x; - right_y = -t.right_y; + read.left_trigger = t.left_trigger < 0.12f ? 0.0f : t.left_trigger; + read.right_trigger = t.right_trigger < 0.12f ? 0.0f : t.right_trigger; + add(read.left_trigger > 0.0f, PadButton::L2); + add(read.right_trigger > 0.0f, PadButton::R2); + read.left_x = t.left_x; + read.left_y = -t.left_y; + read.right_x = t.right_x; + read.right_y = -t.right_y; + // The hand that holds the controller in the game presses and swipes, the other + // one pulls. + const bool right = pad_hand != 0; + read.gestures.press = (right ? t.right_trigger : t.left_trigger) > 0.5f; + read.gestures.swipe = (right ? t.right_grip : t.left_grip) > 0.5f; + read.gestures.pull = (right ? t.left_trigger : t.right_trigger) > 0.5f; + read.press_button = right ? PadButton::R2 : PadButton::L2; + read.swipe_button = right ? PadButton::R1 : PadButton::L1; + read.pull_button = right ? PadButton::L2 : PadButton::R2; } else { const GamepadState& g = gamepad; - buttons = g.buttons; - left_x = g.left_x; - left_y = g.left_y; - right_x = g.right_x; - right_y = g.right_y; - left_trigger = g.left_trigger; - right_trigger = g.right_trigger; - real_finger = g.touch_down; - touch_x = g.touch_x; - touch_y = g.touch_y; - if (gamepad_motion) { - out.pad.has_motion = true; - std::copy(gamepad_gyro, gamepad_gyro + 3, out.pad.gyro); - std::copy(gamepad_accel, gamepad_accel + 3, out.pad.accel); + read.buttons = g.buttons; + read.left_x = g.left_x; + read.left_y = g.left_y; + read.right_x = g.right_x; + read.right_y = g.right_y; + read.left_trigger = g.left_trigger; + read.right_trigger = g.right_trigger; + read.finger = g.touch_down; + read.finger_x = g.touch_x; + read.finger_y = g.touch_y; + if (!gamepad_touchpad) { + read.gestures.press = g.right_trigger > 0.5f; + read.gestures.swipe = (g.buttons & PadButton::R1) != 0; + read.gestures.pull = g.left_trigger > 0.5f; + read.press_button = PadButton::R2; + read.swipe_button = PadButton::R1; + read.pull_button = PadButton::L2; } } + return read; + } + + /// What the game is given now, and what the buttons mean besides (resets of the view). + Output Compose(double now) { + Output out; + if (!playing) { + return out; + } + Reading read = Read(); + uint32_t buttons = read.buttons; + if (source != Source::Touch && gamepad_motion) { + out.pad.has_motion = true; + std::copy(gamepad_gyro, gamepad_gyro + 3, out.pad.gyro); + std::copy(gamepad_accel, gamepad_accel + 3, out.pad.accel); + } // What was held down when the game got the controls is not the game's. held_back &= buttons; buttons &= ~held_back; if ((held_back & PadButton::L2) != 0) { - left_trigger = 0.0f; + read.left_trigger = 0.0f; } if ((held_back & PadButton::R2) != 0) { - right_trigger = 0.0f; + read.right_trigger = 0.0f; } + read.gestures.press = read.gestures.press && (held_back & read.press_button) == 0; + read.gestures.swipe = read.gestures.swipe && (held_back & read.swipe_button) == 0; + read.gestures.pull = read.gestures.pull && (held_back & read.pull_button) == 0; - // This title has no use for the right stick, while several of its gadgets want - // swipes: the stick moves a finger over the touchpad (see Input::StickFinger: a - // stick let go lifts the finger where it was, which is what lets a pull on the stick - // shoot the game's catapults). - bool touching = real_finger; - if (real_finger) { - // A real finger is on the pad: the stick's has nothing to say, and is not back - // on the pad the moment the real one lifts either. + // The touchpad. A real finger on a real one comes first; then what buttons do on a + // controller that has none (the pad pressed, a swipe forward, a pull back that is + // let go); then the right stick, for which this title has no use, as a finger. + bool touching = read.finger; + float touch_x = read.finger_x; + float touch_y = read.finger_y; + const Input::PadGestures::Touch made = gestures.Update(now, read.gestures); + if (read.finger) { + // (The stick's finger is not back on the pad the moment the real one lifts.) + stick_finger.Reset(); + } else if (made.down) { + touching = true; + touch_x = made.x; + touch_y = made.y; + if (made.pressed) { + buttons |= PadButton::TouchPad; + } stick_finger.Reset(); } else if (stick_touchpad) { - const Input::StickFinger::Touch dragged = stick_finger.Update(now, right_x, right_y); + const Input::StickFinger::Touch dragged = + stick_finger.Update(now, read.right_x, read.right_y); if (dragged.down) { touching = true; touch_x = dragged.x; @@ -380,8 +447,8 @@ private: } } if (source == Source::Touch && stick_touchpad) { - right_x = 0.0f; - right_y = 0.0f; + read.right_x = 0.0f; + read.right_y = 0.0f; } if (!touching && (buttons & PadButton::TouchPad) != 0) { // A touchpad cannot be pressed without touching it: in its middle, where a @@ -400,21 +467,18 @@ private: } out.pad.buttons = buttons; - out.pad.left_x = Stick(left_x); - out.pad.left_y = Stick(left_y); - out.pad.right_x = Stick(right_x); - out.pad.right_y = Stick(right_y); - out.pad.left_trigger = Trigger(left_trigger); - out.pad.right_trigger = Trigger(right_trigger); + out.pad.left_x = Stick(read.left_x); + out.pad.left_y = Stick(read.left_y); + out.pad.right_x = Stick(read.right_x); + out.pad.right_y = Stick(read.right_y); + out.pad.left_trigger = Trigger(read.left_trigger); + out.pad.right_trigger = Trigger(read.right_trigger); out.pad.touch_down = touching; // (A finger that lifts does so from where it was.) out.pad.touch_x = sent_touch_x; out.pad.touch_y = sent_touch_y; - out.blowing = blow; + out.blowing = read.blow; - if (!act) { - return out; - } if ((buttons & PadButton::Cross) != 0 && !seat_taken) { // The first press of X is the player settled in, controller in hand, looking at // the game: that, and not where they were when the app started, is their seat. @@ -427,6 +491,7 @@ private: options_fired = false; } else if (options_since < 0.0) { options_since = now; + ++options_presses; } else if (!options_fired && now - options_since >= OptionsHold) { // What a PlayStation VR does when OPTIONS is held, and what the game tells its // players to do when the view is off. The game still sees the button. @@ -434,16 +499,16 @@ private: recenter |= RecenterPad | RecenterSeat; Note("OPTIONS held: view reset"); } - if (view_reset && !view_reset_held) { + if (read.view_reset && !view_reset_held) { recenter |= RecenterPad | RecenterSeat; Note("both sticks pressed in: view reset"); } - view_reset_held = view_reset; + view_reset_held = read.view_reset; return out; } void Publish(double now) { - const Output out = Compose(now, true); + const Output out = Compose(now); if (published && out == last) { return; } @@ -462,10 +527,12 @@ private: std::function sink; std::function notice; bool stick_touchpad{true}; + int pad_hand{1}; bool playing{}; Source source{Source::None}; bool gamepad_connected{}; + bool gamepad_touchpad{}; GamepadState gamepad; bool gamepad_motion{}; float gamepad_gyro[3]{}; @@ -473,6 +540,8 @@ private: TouchState touch; Input::StickFinger stick_finger; + Input::PadGestures gestures; + uint32_t options_presses{}; uint32_t held_back{}; uint16_t sent_touch_x{TouchWidth / 2}; uint16_t sent_touch_y{TouchHeight / 2}; diff --git a/quest-host/cpp/xr_host.cpp b/quest-host/cpp/xr_host.cpp index c1c51df..9286a58 100644 --- a/quest-host/cpp/xr_host.cpp +++ b/quest-host/cpp/xr_host.cpp @@ -543,7 +543,10 @@ private: "B = square, X = circle, Y = triangle (X and Y together blow), right stick = " "finger on the touchpad, pressed in = touchpad pressed, grips = L1 and R1, " "triggers = L2 and R2, left menu button = OPTIONS (held for a second: view " - "reset), both sticks pressed in = view reset"); + "reset), both sticks pressed in = view reset; on the touchpad, the %s trigger " + "presses, the %s grip swipes forward, the %s trigger pulls back and lets go", + options.pad_hand == 0 ? "left" : "right", options.pad_hand == 0 ? "left" : "right", + options.pad_hand == 0 ? "right" : "left"); } controller_located = false; if (controllers_used && touch.present) { @@ -1237,7 +1240,8 @@ private: layers.push_back( reinterpret_cast(&projection)); } - if ((stale || options.show_stats) && status_ready) { + const bool card = host_status.show_panel; + if ((stale || options.show_stats || card) && status_ready) { quad.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; quad.space = view_space; quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH; @@ -1247,6 +1251,11 @@ private: if (stale) { quad.pose.position = {0.0f, 0.0f, -1.6f}; quad.size = {1.4f, 0.7f}; + } else if (card) { + // To be read while playing: large enough for that, below the + // middle of the view. + quad.pose.position = {0.0f, -0.40f, -1.3f}; + quad.size = {0.9f, 0.45f}; } else { // Over the game: out of the way of what is being played. quad.pose.position = {0.0f, -0.55f, -1.6f}; diff --git a/quest-host/cpp/xr_host.h b/quest-host/cpp/xr_host.h index ce3549e..b379a8f 100644 --- a/quest-host/cpp/xr_host.h +++ b/quest-host/cpp/xr_host.h @@ -93,6 +93,8 @@ struct XrHostStatus { std::atomic hands_tracked{}; /// The one of the headset's own controllers that is the controller in the game is seen. std::atomic controller_tracked{}; + /// The panel is to be shown over the game: it has something to say to who is playing. + std::atomic show_panel{}; std::atomic refresh_rate{}; /// Set when the startup choice is confirmed, before the game can deliver any frames. std::atomic reduced_fov{}; diff --git a/quest-host/java/com/astrobotquest/vrhost/MainActivity.java b/quest-host/java/com/astrobotquest/vrhost/MainActivity.java index a1e8150..c61683e 100644 --- a/quest-host/java/com/astrobotquest/vrhost/MainActivity.java +++ b/quest-host/java/com/astrobotquest/vrhost/MainActivity.java @@ -121,8 +121,17 @@ public class MainActivity extends Activity private native long nativePadFeedback(); - /** A gamepad is there to play with (again, or another one), or none is any more. */ - private native void nativeSetGamepad(boolean connected); + /** + * A gamepad is there to play with (again, or another one), or none is any more. + * touchpad: it has a touchpad of its own, and needs no buttons to stand in for one. + */ + private native void nativeSetGamepad(boolean connected, boolean touchpad); + + /** How often OPTIONS was pressed in the game so far, on whichever controller. */ + private native int nativeOptionsPresses(); + + /** Shows the panel over the game's picture, for what is to be read while playing. */ + private native void nativeShowPanel(boolean show); /** The start-up menu is over: what the player presses is the game's. */ private native void nativeSetPlaying(boolean playing); @@ -139,6 +148,8 @@ public class MainActivity extends Activity private static final int CONTROLLER_WHICH = 3, CONTROLLER_GAMEPAD = 1, CONTROLLER_OWN = 2; /** Added to it while the headset's own controllers are in the player's hands. */ private static final int CONTROLLER_OWN_HELD = 4; + /** Added where what the game is played with has no touchpad: buttons do its gestures. */ + private static final int CONTROLLER_GESTURE_BUTTONS = 8; private static final int MENU_LEFT = 1, MENU_RIGHT = 2, MENU_CONFIRM = 4; private static final int VENDOR_SONY = 0x054c; @@ -220,6 +231,13 @@ public class MainActivity extends Activity private float[] headsetFov; private static final int FOV_MIN = 50, FOV_MAX = 100, FOV_STEP = 5; private long nextOwnFovMoveAt; + /** The card that says which buttons do the touchpad's gestures: shown until then. */ + private long cardUntil; + private boolean cardShowing; + /** Which controller it was last shown for, and the presses of OPTIONS counted then. */ + private int cardShownFor; + private int optionsSeen; + private static final long CARD_AT_START_MS = 15000, CARD_ON_PAUSE_MS = 10000; /** * What confirms the menu on the headset's own controllers has been seen let go while the * menu showed: the trigger that started the app from the headset's library is not it. @@ -288,6 +306,9 @@ public class MainActivity extends Activity headsetFov = new float[] {1.376f, 0.839f, 0.966f, 1.428f}; drawFovMenu(); headsetFov = null; + // Likewise the cards that say which buttons do the touchpad's gestures. + drawStatus("The touchpad, on this controller", controlsCard(true)); + drawStatus("The touchpad, on this controller", controlsCard(false)); startGame(); } else { handler.post(this::pollOwnControllers); @@ -796,11 +817,58 @@ public class MainActivity extends Activity + "was)."; } } + // On a controller without a touchpad, buttons do what the game wants done on one: + // which ones is said over the game once it shows, and again whenever OPTIONS is + // pressed (the game's pause), for as long as it takes to read. + String title = "Astro VR Host"; + int controller = nativeController(); + int presses = nativeOptionsPresses(); + long clock = SystemClock.elapsedRealtime(); + boolean playing = !setupFailed && nativeCoreState() == CORE_RUNNING + && nativeFrameCount() > 0; + if (playing && (controller & CONTROLLER_GESTURE_BUTTONS) != 0) { + if (cardShownFor != (controller & CONTROLLER_WHICH)) { + cardShownFor = controller & CONTROLLER_WHICH; + cardUntil = clock + CARD_AT_START_MS; + } else if (presses != optionsSeen) { + cardUntil = clock + CARD_ON_PAUSE_MS; + } + } + optionsSeen = presses; + boolean card = playing && (controller & CONTROLLER_GESTURE_BUTTONS) != 0 + && clock < cardUntil; + if (card != cardShowing) { + cardShowing = card; + nativeShowPanel(card); + } + if (card) { + title = "The touchpad, on this controller"; + status = controlsCard((controller & CONTROLLER_WHICH) == CONTROLLER_OWN); + } if (!status.equals(shownStatus)) { shownStatus = status; - drawStatus(status); + drawStatus(title, status); } - handler.postDelayed(this::refreshStatus, 500); + handler.postDelayed(this::refreshStatus, card || playing ? 250 : 500); + } + + /** Which buttons do what a finger does on a DualShock 4's touchpad. */ + private String controlsCard(boolean ownControllers) { + if (ownControllers) { + String pad = padHand == 0 ? "Left" : "Right"; + String other = padHand == 0 ? "Right" : "Left"; + return pad + " trigger: press the touchpad (water, guns)\n" + + pad + " grip: swipe forward (hook, stars, chests)\n" + + other + " trigger: pull back, aim at the goal, let go (catapult)\n" + + "Right stick: a finger on the touchpad\n" + + "X and Y together: blow\n" + + "The menu button brings this back."; + } + return "R2: press the touchpad (water, guns)\n" + + "R1: swipe forward (hook, stars, chests)\n" + + "L2: pull back, aim at the goal, let go (catapult)\n" + + "Right stick: a finger on the touchpad\n" + + "OPTIONS brings this back."; } /** What the game and the display are doing, for the panel that stats=1 keeps in view. */ @@ -941,7 +1009,7 @@ public class MainActivity extends Activity handler.postDelayed(this::pollOwnControllers, 50); } - private void drawStatus(String status) { + private void drawStatus(String title, String status) { Bitmap bitmap = Bitmap.createBitmap(STATUS_WIDTH, STATUS_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -951,7 +1019,7 @@ public class MainActivity extends Activity paint.setColor(Color.rgb(90, 170, 255)); paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); paint.setTextSize(52); - canvas.drawText("Astro VR Host", 48, 86, paint); + canvas.drawText(title, 48, 86, paint); paint.setColor(Color.WHITE); paint.setTypeface(Typeface.DEFAULT); @@ -979,7 +1047,7 @@ public class MainActivity extends Activity } private void publishStatusBitmap(Bitmap bitmap) { - if (dryRun && statusPictures < 8) { + if (dryRun && statusPictures < 12) { // Nobody sees the panel in a test: its pictures are kept for looking at, in the // folder the test has made for them. File picture = new File(new File(getFilesDir(), "drystart"), @@ -1361,7 +1429,7 @@ public class MainActivity extends Activity + Integer.toHexString(device.getSources()) + ")"); learnAxes(device); - nativeSetGamepad(true); + nativeSetGamepad(true, device.supportsSource(InputDevice.SOURCE_TOUCHPAD)); // Every part is optional: what a controller offers depends on it and on the system. try { @@ -1577,7 +1645,7 @@ public class MainActivity extends Activity findGamepad(); if (gamepad == null) { // The headset's own controllers play on, if the player picks them up. - nativeSetGamepad(false); + nativeSetGamepad(false, false); } } } diff --git a/quest-host/java/com/astrobotquest/vrhost/SandboxShell.java b/quest-host/java/com/astrobotquest/vrhost/SandboxShell.java index f543de3..32ef816 100644 --- a/quest-host/java/com/astrobotquest/vrhost/SandboxShell.java +++ b/quest-host/java/com/astrobotquest/vrhost/SandboxShell.java @@ -157,7 +157,15 @@ public class SandboxShell extends Instrumentation { : step == 110 ? -KeyEvent.KEYCODE_BUTTON_START : step == 114 ? KeyEvent.KEYCODE_BUTTON_MODE : step == 115 ? -KeyEvent.KEYCODE_BUTTON_MODE : 0; - if (reset == 0 && (step < 80 || (phase != 0 && phase != 1 && phase != 10))) { + // Once, what a gamepad without a touchpad does about the touchpad: the right + // stick pulled back for most of a second and let go (the finger it moves), + // and R1 (which swipes forward). With env=SHADPS4_PAD_TOUCH_TRACE=1 among the + // settings, core.log says what the game was given of both. + final int gesture = step == 122 ? 1 : step == 125 ? 2 + : step == 128 ? KeyEvent.KEYCODE_BUTTON_R1 + : step == 129 ? -KeyEvent.KEYCODE_BUTTON_R1 : 0; + if (reset == 0 && gesture == 0 + && (step < 80 || (phase != 0 && phase != 1 && phase != 10))) { continue; } runOnMainSync(() -> { @@ -166,8 +174,16 @@ public class SandboxShell extends Instrumentation { activity[0].dispatchKeyEvent(new KeyEvent( reset > 0 ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, Math.abs(reset))); + } else if (gesture == 1 || gesture == 2) { + activity[0].dispatchGenericMotionEvent( + stickEvent(0.0f, 0.0f, 0.0f, gesture == 1 ? 1.0f : 0.0f)); + } else if (gesture != 0) { + activity[0].dispatchKeyEvent(new KeyEvent( + gesture > 0 ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, + Math.abs(gesture))); } else if (phase == 10) { - activity[0].dispatchGenericMotionEvent(stickEvent(0.5f, -0.25f)); + activity[0].dispatchGenericMotionEvent( + stickEvent(0.5f, -0.25f, 0.0f, 0.0f)); } else { activity[0].dispatchKeyEvent(new KeyEvent( phase == 0 ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, @@ -207,13 +223,15 @@ public class SandboxShell extends Instrumentation { return report.toString(); } - /** What a gamepad reports when its left stick is moved. */ - private static MotionEvent stickEvent(float x, float y) { + /** What a gamepad reports when its sticks are moved: the left one, the right one. */ + private static MotionEvent stickEvent(float x, float y, float rightX, float rightY) { MotionEvent.PointerProperties[] properties = {new MotionEvent.PointerProperties()}; properties[0].id = 0; MotionEvent.PointerCoords[] coords = {new MotionEvent.PointerCoords()}; coords[0].setAxisValue(MotionEvent.AXIS_X, x); coords[0].setAxisValue(MotionEvent.AXIS_Y, y); + coords[0].setAxisValue(MotionEvent.AXIS_Z, rightX); + coords[0].setAxisValue(MotionEvent.AXIS_RZ, rightY); long now = SystemClock.uptimeMillis(); return MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, 1, properties, coords, 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_JOYSTICK, 0); diff --git a/shadps4-arm64-main/src/core/libraries/pad/pad.cpp b/shadps4-arm64-main/src/core/libraries/pad/pad.cpp index 993dfab..41a5135 100644 --- a/shadps4-arm64-main/src/core/libraries/pad/pad.cpp +++ b/shadps4-arm64-main/src/core/libraries/pad/pad.cpp @@ -535,6 +535,30 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::GameController& contro pData[i].deviceUniqueDataLen = 0; } + // SHADPS4_PAD_TOUCH_TRACE=1: what the title is given of the touchpad, read by read, for + // as long as a finger is on it or the pad is pressed. A title makes its swipes and pulls + // of what it finds at each of its reads (the newest of the states it is handed), which + // is not what the finger did but what was seen of it: this says what was. + static const bool touch_trace = [] { + const char* value = std::getenv("SHADPS4_PAD_TOUCH_TRACE"); + return value != nullptr && value[0] == '1'; + }(); + if (touch_trace && num > 0 && handle == 1) { + static bool was_busy = false; + static u64 reads = 0; + const OrbisPadData& last = pData[num - 1]; + const bool down = last.touchData.touchNum != 0; + const bool pressed = True(last.buttons & OrbisPadButtonDataOffset::TouchPad); + ++reads; + if (down || pressed || was_busy) { + LOG_INFO(Lib_Pad, "touch trace: read {} at {} ms, {} states: finger {} at {} {}, pad {}", + reads, last.timestamp / 1000, num, down ? "down" : "up", + last.touchData.touch[0].x, last.touchData.touch[0].y, + pressed ? "pressed" : "not pressed"); + } + was_busy = down || pressed; + } + return num; } diff --git a/shadps4-arm64-main/src/core/vr/openxr_host.cpp b/shadps4-arm64-main/src/core/vr/openxr_host.cpp index 02e008a..c2e4981 100644 --- a/shadps4-arm64-main/src/core/vr/openxr_host.cpp +++ b/shadps4-arm64-main/src/core/vr/openxr_host.cpp @@ -28,6 +28,7 @@ #include "core/vr/openxr_host.h" #include "input/controller.h" #include "input/scripted_input.h" +#include "input/pad_gestures.h" #include "input/stick_finger.h" #include "video_core/renderer_vulkan/vk_scheduler.h" @@ -394,6 +395,7 @@ struct OpenXrHost::Impl { float sent_touch_x{0.5f}; float sent_touch_y{0.5f}; Input::StickFinger stick_finger; + Input::PadGestures gestures; bool blowing{}; std::atomic rumble_wanted{}; u32 rumble_applied{}; @@ -930,6 +932,7 @@ struct OpenXrHost::Impl { sent_axes = {128, 128, 128, 128, 0, 0}; sent_touch = false; stick_finger.Reset(); + gestures.Reset(); if (blowing) { blowing = false; Input::SetBlowing(false); @@ -1026,8 +1029,10 @@ struct OpenXrHost::Impl { add(pressed(act_options), Buttons::Options); const bool left_stick_in = pressed(act_l3); const bool right_stick_in = pressed(act_finger_press); - add(pulled(act_l1) > 0.5f, Buttons::L1); - add(pulled(act_r1) > 0.5f, Buttons::R1); + const float left_grip = pulled(act_l1); + const float right_grip = pulled(act_r1); + add(left_grip > 0.5f, Buttons::L1); + add(right_grip > 0.5f, Buttons::R1); const float left_trigger = pulled(act_l2); const float right_trigger = pulled(act_r2); const XrVector2f move = stick(act_move); @@ -1046,7 +1051,8 @@ struct OpenXrHost::Impl { "circle, left Y/B = triangle, right stick = finger on the touchpad, " "pressed in = touchpad pressed, left menu/trackpad = OPTIONS, " "both sticks pressed in = reset the view; the {} " - "one is the controller in the game)", + "one is the controller in the game: its trigger presses the touchpad, its " + "grip swipes forward, the other trigger pulls back and lets go)", pad_hand == 0 ? "left" : "right"); } @@ -1080,18 +1086,36 @@ struct OpenXrHost::Impl { if (axes[5] > 0) { buttons |= Buttons::R2; } - // The right stick moves a finger over the touchpad (see Input::StickFinger: a stick - // let go lifts the finger where it was, which is what lets a pull on the stick shoot - // the game's catapults). A stick pushed away from the player is the pad's far edge. - // The finger also touches, in the middle of the pad, when the stick is only pressed - // in: a touchpad cannot be pressed without touching it. - const Input::StickFinger::Touch dragged = stick_finger.Update( - std::chrono::duration(Clock::now().time_since_epoch()).count(), finger.x, - -finger.y); - const bool swiping = dragged.down; + // What the title wants done on the touchpad is on buttons (see Input::PadGestures): + // the trigger of the hand that holds the controller in the game presses the pad for + // as long as it is pulled, the grip of that hand swipes forward once, the other + // hand's trigger pulls back and lets go when it is let go. (They stay L1, R1, L2 and + // R2 besides: the title has no use for those in play, but its menus have.) + const double seconds = + std::chrono::duration(Clock::now().time_since_epoch()).count(); + Input::PadGestures::Controls controls; + controls.press = (pad_hand == 0 ? left_trigger : right_trigger) > 0.5f; + controls.swipe = (pad_hand == 0 ? left_grip : right_grip) > 0.5f; + controls.pull = (pad_hand == 0 ? right_trigger : left_trigger) > 0.5f; + const Input::PadGestures::Touch made = gestures.Update(seconds, controls); + // Besides that, the right stick moves a finger over the touchpad (see + // Input::StickFinger: a stick let go lifts the finger where it was, which is what + // lets a pull on the stick shoot the game's catapults). A stick pushed away from the + // player is the pad's far edge. The finger also touches, in the middle of the pad, + // when the stick is only pressed in: a touchpad cannot be pressed without touching it. + Input::StickFinger::Touch dragged{false, 0.5f, 0.5f}; + if (made.down) { + stick_finger.Reset(); + if (made.pressed) { + buttons |= Buttons::TouchPad; + } + } else { + dragged = stick_finger.Update(seconds, finger.x, -finger.y); + } + const bool swiping = made.down || dragged.down; const bool touch = swiping || (right_stick_in && !view_reset); - const float touch_x = swiping ? dragged.x : 0.5f; - const float touch_y = swiping ? dragged.y : 0.5f; + const float touch_x = made.down ? made.x : dragged.down ? dragged.x : 0.5f; + const float touch_y = made.down ? made.y : dragged.down ? dragged.y : 0.5f; if (buttons != sent_buttons || axes != sent_axes || touch != sent_touch || (touch && (std::abs(touch_x - sent_touch_x) > 0.002f || std::abs(touch_y - sent_touch_y) > 0.002f))) { diff --git a/shadps4-arm64-main/src/input/controller.cpp b/shadps4-arm64-main/src/input/controller.cpp index 3055593..fbbc351 100644 --- a/shadps4-arm64-main/src/input/controller.cpp +++ b/shadps4-arm64-main/src/input/controller.cpp @@ -197,8 +197,7 @@ void GameController::UpdateStickTouch() { const char* value = std::getenv("SHADPS4_STICK_TOUCHPAD"); return value == nullptr || value[0] != '0'; }(); - if (!enabled || m_sdl_gamepad == nullptr || - !Core::Vr::Runtime::Instance().IsHeadsetConnected()) { + if (m_sdl_gamepad == nullptr || !Core::Vr::Runtime::Instance().IsHeadsetConnected()) { return; } std::scoped_lock lock{m_finger_mutex}; @@ -208,13 +207,42 @@ void GameController::UpdateStickTouch() { m_stick_finger.Reset(); return; } - const float x = - static_cast(SDL_GetGamepadAxis(m_sdl_gamepad, SDL_GAMEPAD_AXIS_RIGHTX)) / 32767.0f; - const float y = - static_cast(SDL_GetGamepadAxis(m_sdl_gamepad, SDL_GAMEPAD_AXIS_RIGHTY)) / 32767.0f; const double now = std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); - const StickFinger::Touch touch = m_stick_finger.Update(now, x, y); + // A gamepad that has no touchpad at all also has buttons for what such a title wants + // done on one (see PadGestures): the right trigger presses the pad for as long as it is + // pulled, the right shoulder button swipes forward once, the left trigger pulls back and + // lets go when it is let go. The title has no use for those buttons in play, and is + // still told of them. (Not while the PS button is held: it gives them other meanings.) + PadGestures::Touch made{}; + if (SDL_GetNumGamepadTouchpads(m_sdl_gamepad) == 0) { + PadGestures::Controls controls; + if (!SDL_GetGamepadButton(m_sdl_gamepad, SDL_GAMEPAD_BUTTON_GUIDE)) { + static constexpr s16 Pulled = 16384; + controls.press = + SDL_GetGamepadAxis(m_sdl_gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > Pulled; + controls.swipe = + SDL_GetGamepadButton(m_sdl_gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); + controls.pull = + SDL_GetGamepadAxis(m_sdl_gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > Pulled; + } + made = m_gestures.Update(now, controls); + if (made.pressed != m_gesture_press) { + m_gesture_press = made.pressed; + Button(OrbisPadButtonDataOffset::TouchPad, made.pressed); + } + } + StickFinger::Touch touch{false, m_stick_touch_x, m_stick_touch_y}; + if (made.down) { + m_stick_finger.Reset(); + touch = {true, made.x, made.y}; + } else if (enabled) { + const float x = static_cast(SDL_GetGamepadAxis(m_sdl_gamepad, SDL_GAMEPAD_AXIS_RIGHTX)) / + 32767.0f; + const float y = static_cast(SDL_GetGamepadAxis(m_sdl_gamepad, SDL_GAMEPAD_AXIS_RIGHTY)) / + 32767.0f; + touch = m_stick_finger.Update(now, x, y); + } if (touch.down) { if (!m_stick_touch || std::abs(touch.x - m_stick_touch_x) > 0.002f || std::abs(touch.y - m_stick_touch_y) > 0.002f) { @@ -502,8 +530,11 @@ void GameControllers::TryOpenSDLControllers() { i + 1, name != nullptr ? name : "unnamed", SDL_GetGamepadVendor(pad), SDL_GetGamepadProduct(pad), path != nullptr ? path : "?", SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) ? "yes" : "no", - SDL_GetNumGamepadTouchpads(pad) > 0 ? "yes" : "no, the right stick " - "stands in", + SDL_GetNumGamepadTouchpads(pad) > 0 + ? "yes" + : "no: the right stick stands in for a finger, the right " + "trigger presses the pad, the right shoulder button swipes " + "forward, the left trigger pulls back and lets go", SDL_GetBooleanProperty(SDL_GetGamepadProperties(pad), SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false) ? "yes" diff --git a/shadps4-arm64-main/src/input/controller.h b/shadps4-arm64-main/src/input/controller.h index 9d50602..18c3790 100644 --- a/shadps4-arm64-main/src/input/controller.h +++ b/shadps4-arm64-main/src/input/controller.h @@ -13,6 +13,7 @@ #include "common/ring_buffer_queue.h" #include "core/libraries/pad/pad.h" #include "core/libraries/system/userservice.h" +#include "input/pad_gestures.h" #include "input/stick_finger.h" struct SDL_Gamepad; @@ -146,6 +147,9 @@ private: bool m_finger_down = false; bool m_touchpad_noted = false; StickFinger m_stick_finger; + // On a gamepad without a touchpad, buttons do its gestures as well (see PadGestures). + PadGestures m_gestures; + bool m_gesture_press = false; bool m_stick_touch = false; float m_stick_touch_x = 0.5f; float m_stick_touch_y = 0.5f; diff --git a/shadps4-arm64-main/src/input/moved_finger.h b/shadps4-arm64-main/src/input/moved_finger.h new file mode 100644 index 0000000..6831a90 --- /dev/null +++ b/shadps4-arm64-main/src/input/moved_finger.h @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace Input { + +/// A finger on a touchpad that something other than a finger moves (a stick, a button), in +/// such a way that a title sees what a finger would have shown it. +/// +/// A title reads its controller once for every frame it draws, and makes what it makes of a +/// touch from what it saw then: where the finger was when it first saw it down, where when +/// it last did, how far it got from one frame to the next. A real finger takes a tenth of a +/// second and more over a swipe; a stick is at its end within a few hundredths, a button at +/// once. A finger that lands and is at the far end of its way before the title has looked +/// even once (at thirty frames a second it looks every 33 ms) never moved at all as far as +/// the title can tell: ASTRO BOT Rescue Mission would not shoot its catapult for a stick +/// pulled back quickly, because the pull began, for it, where it ended. +/// +/// So this finger stays where it lands until the title has seen it there, goes to where it +/// is wanted no faster than a finger flicks, and is only done once the title has seen it at +/// its end as well. +struct MovedFinger { + /// How long it stays where it landed, in seconds: three frames of a title that draws + /// thirty a second. + static constexpr double Dwell = 0.09; + /// How fast it goes, in widths of the pad a second: a brisk flick. + static constexpr float Speed = 6.0f; + /// How long it has to have been at its goal to count as arrived, in seconds. + static constexpr double Hold = 0.08; + + /// On the pad, 0 to 1: from its left to its right, from its far edge to its near one. + float x{0.5f}; + float y{0.5f}; + + void Land(double time, float at_x, float at_y) { + x = at_x; + y = at_y; + landed = time; + moved = time; + arrived = false; + } + + /// Goes on towards a goal, which may move. True once it has been there for `Hold`. + bool Follow(double time, float goal_x, float goal_y) { + const double passed = time - moved; + moved = time; + if (time - landed < Dwell) { + return false; + } + const float dx = goal_x - x; + const float dy = goal_y - y; + const float distance = std::hypot(dx, dy); + // (Called too seldom, it does not leap to make up for it.) + const float step = + Speed * static_cast(passed < 0.0 ? 0.0 : passed > 0.05 ? 0.05 : passed); + if (distance > step && distance > 1e-4f) { + x += dx / distance * step; + y += dy / distance * step; + arrived = false; + return false; + } + x = goal_x; + y = goal_y; + if (!arrived) { + arrived = true; + arrived_at = time; + } + return time - arrived_at >= Hold; + } + + /// How long ago it landed. + double Down(double time) const { + return time - landed; + } + +private: + double landed{}; + double moved{}; + bool arrived{}; + double arrived_at{}; +}; + +} // namespace Input diff --git a/shadps4-arm64-main/src/input/pad_gestures.h b/shadps4-arm64-main/src/input/pad_gestures.h new file mode 100644 index 0000000..48c51b5 --- /dev/null +++ b/shadps4-arm64-main/src/input/pad_gestures.h @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "moved_finger.h" + +namespace Input { + +/// What a finger does on a DualShock 4's touchpad, done by buttons: for controllers that +/// have no touchpad (other gamepads, a headset's own controllers), where a title made for +/// one wants more of it than a stick's finger does comfortably. Three things cover what +/// ASTRO BOT Rescue Mission asks of the pad: +/// - pressing it, for as long as a button is held (its water cannon and its machine gun +/// fire while the pad is pressed); +/// - a swipe forward, once for each press of a button (its hook is shot that way, its +/// throwing stars thrown, its chests opened); +/// - a pull back that is held for as long as a button is, and let go when the button is +/// (the catapult at the end of each of its levels, and pulling on the hook's rope). +/// Each is a finger a title can follow (see MovedFinger): it is where it lands for long +/// enough to be seen there, and it is lifted for long enough to be seen lifted before it +/// lands again. +class PadGestures { +public: + struct Controls { + bool press{}; + bool swipe{}; + bool pull{}; + }; + + struct Touch { + bool down{}; + /// On the pad, 0 to 1: from its left to its right, from its far edge to its near one. + float x{0.5f}; + float y{0.5f}; + /// The pad itself is pressed in. + bool pressed{}; + }; + + /// `time` in seconds, from any moment. To be called at least some thirty times a second, + /// whether a button is held or not. + Touch Update(double time, const Controls& controls) { + if (controls.swipe && !was.swipe && swipes < 2) { + // (One that is asked for while another is under way comes after it.) + ++swipes; + } + was = controls; + + switch (doing) { + case Doing::Nothing: + if (time - lifted < Apart) { + break; + } + if (controls.press) { + doing = Doing::Press; + finger.Land(time, 0.5f, 0.5f); + } else if (swipes != 0) { + --swipes; + doing = Doing::Swipe; + finger.Land(time, 0.5f, SwipeFrom); + } else if (controls.pull) { + doing = Doing::Pull; + let_go = false; + finger.Land(time, 0.5f, PullFrom); + } + break; + case Doing::Press: + if (!controls.press && finger.Down(time) >= ShortestPress) { + Lift(time); + } + break; + case Doing::Swipe: + if (finger.Follow(time, 0.5f, SwipeTo)) { + Lift(time); + } + break; + case Doing::Pull: { + // Once let go it is let go, however soon the button is pulled again: the way + // back is finished first, so that a quick pull of the button is a whole pull. + let_go = let_go || !controls.pull; + const bool there = finger.Follow(time, 0.5f, PullTo); + if (let_go && there && finger.Down(time) >= ShortestPull) { + Lift(time); + } + break; + } + } + return {doing != Doing::Nothing, finger.x, finger.y, doing == Doing::Press}; + } + + /// Whatever was under way is over, and the finger is off the pad. + void Reset() { + doing = Doing::Nothing; + swipes = 0; + was = {}; + } + + bool IsActive() const { + return doing != Doing::Nothing; + } + + /// Where a swipe forward begins and ends, of the pad's depth from its far edge. + static constexpr float SwipeFrom = 0.80f; + static constexpr float SwipeTo = 0.12f; + /// Where a pull back begins and ends. + static constexpr float PullFrom = 0.20f; + static constexpr float PullTo = 0.97f; + /// The pad stays pressed this long at least, in seconds, however briefly the button was. + static constexpr double ShortestPress = 0.12; + /// A pull lasts this long at least. (That catapult wants more than a quarter second.) + static constexpr double ShortestPull = 0.36; + /// The finger is off the pad this long at least between two things. + static constexpr double Apart = 0.08; + +private: + enum class Doing { Nothing, Press, Swipe, Pull }; + + void Lift(double time) { + doing = Doing::Nothing; + lifted = time; + } + + Doing doing{Doing::Nothing}; + Controls was; + int swipes{}; + bool let_go{}; + double lifted{-1.0}; + MovedFinger finger; +}; + +} // namespace Input diff --git a/shadps4-arm64-main/src/input/scripted_input.cpp b/shadps4-arm64-main/src/input/scripted_input.cpp index 2734564..9a6aa99 100644 --- a/shadps4-arm64-main/src/input/scripted_input.cpp +++ b/shadps4-arm64-main/src/input/scripted_input.cpp @@ -19,6 +19,7 @@ #include "core/vr/vr_runtime.h" #include "input/controller.h" #include "input/scripted_input.h" +#include "input/pad_gestures.h" #include "input/stick_finger.h" namespace Input { @@ -47,6 +48,9 @@ struct Step { // The right stick as it stands in for that finger (Input::StickFinger), -1..1 to the right // and towards the player. std::optional> finger; + // The buttons that do the touchpad's gestures on a controller without one + // (Input::PadGestures), held while the line lasts. + PadGestures::Controls gestures; // The place a controller that nothing locates is held to be at: moved by so many metres // (right, up, towards the player), or switched between the standard one and the player's. std::optional place_move; @@ -167,6 +171,12 @@ std::vector ParseScript(const std::filesystem::path& script) { } continue; } + if (token == "gesture=press" || token == "gesture=swipe" || token == "gesture=pull") { + step.gestures.press = step.gestures.press || token == "gesture=press"; + step.gestures.swipe = step.gestures.swipe || token == "gesture=swipe"; + step.gestures.pull = step.gestures.pull || token == "gesture=pull"; + continue; + } if (token == "place=switch") { step.place_switch = true; continue; @@ -207,6 +217,7 @@ void Replay(std::vector steps, std::filesystem::path live_script) { std::array previous_axes{128, 128, 128, 128, 0, 0}; std::optional> previous_touch; StickFinger stick_finger; + PadGestures gestures; std::filesystem::file_time_type live_stamp{}; while (true) { if (!live_script.empty()) { @@ -229,6 +240,7 @@ void Replay(std::vector steps, std::filesystem::path live_script) { const Step* hands_step = nullptr; std::optional> touch; std::array finger{}; + PadGestures::Controls controls; float microphone = 0.0f; for (const Step& step : steps) { if (now >= step.start) { @@ -254,6 +266,9 @@ void Replay(std::vector steps, std::filesystem::path live_script) { if (step.finger) { finger = *step.finger; } + controls.press = controls.press || step.gestures.press; + controls.swipe = controls.swipe || step.gestures.swipe; + controls.pull = controls.pull || step.gestures.pull; microphone = std::max(microphone, step.microphone); buttons |= step.buttons; for (size_t axis = 0; axis < step.sticks.size(); ++axis) { @@ -263,9 +278,16 @@ void Replay(std::vector steps, std::filesystem::path live_script) { } } microphone_level.store(microphone, std::memory_order_relaxed); - // (A stick at rest between its lines: that is what lifts its finger.) - if (const auto stick = stick_finger.Update(now, finger[0], finger[1]); - stick.down && !touch) { + // What buttons do on the touchpad comes before what the stick does there. + if (const auto made = gestures.Update(now, controls); made.down && !touch) { + touch = std::array{made.x, made.y}; + if (made.pressed) { + buttons |= Buttons::TouchPad; + } + stick_finger.Reset(); + } else if (const auto stick = stick_finger.Update(now, finger[0], finger[1]); + stick.down && !touch) { + // (A stick at rest between its lines: that is what lifts its finger.) touch = std::array{stick.x, stick.y}; } auto& vr = Core::Vr::Runtime::Instance(); diff --git a/shadps4-arm64-main/src/input/scripted_input.h b/shadps4-arm64-main/src/input/scripted_input.h index 3ce2675..a65912b 100644 --- a/shadps4-arm64-main/src/input/scripted_input.h +++ b/shadps4-arm64-main/src/input/scripted_input.h @@ -16,7 +16,10 @@ namespace Input { /// "padrot=yaw,pitch,roll"; those poses stay in effect until a later line replaces them. /// "hands=x,y,z[,heading]" stands for a host that sees where the controller is held but not how /// it is turned ("hands=off" for losing sight of it), "touch=x,y" (0..1) for a finger on the -/// touchpad while the line lasts, "mic=level" for noise of that loudness in the microphone +/// touchpad while the line lasts, "finger=x,y" (-1..1) for the right stick as it moves that +/// finger, "gesture=press", "gesture=swipe" or "gesture=pull" for the buttons that do the +/// touchpad's gestures on a controller without one, held while the line lasts, +/// "mic=level" for noise of that loudness in the microphone /// (root mean square, 1 is full scale: 0.35 is what a title takes for blowing hard), /// "recenter" for the player asking for the view to be reset where the head is at that moment, /// "worn=0" / "worn=1" for the headset being taken off and put back on. diff --git a/shadps4-arm64-main/src/input/stick_finger.h b/shadps4-arm64-main/src/input/stick_finger.h index d26d885..1bff738 100644 --- a/shadps4-arm64-main/src/input/stick_finger.h +++ b/shadps4-arm64-main/src/input/stick_finger.h @@ -7,19 +7,25 @@ #include #include +#include "moved_finger.h" + namespace Input { /// A finger on a touchpad, moved by a stick: for controllers that have no touchpad, where a /// title made for one has its players swipe and drag. Where the stick points is where the -/// finger is, with what a stick needs and a finger does not: -/// - The finger comes down in the middle of the pad and moves out from there, so that a push -/// of the stick is a swipe over all of the way it goes. +/// finger goes, with what a stick needs and a finger does not: +/// - The finger comes down behind the middle of the pad, on the side the stick comes from, +/// and is dragged from there to where the stick points: a push of the stick is a swipe +/// over all of the way it goes, and more. +/// - It is a finger a title can follow (see MovedFinger): it stays where it came down +/// until the title has seen it there, and goes no faster than a finger flicks. A stick +/// is at its end before a title that draws thirty frames a second has looked once. /// - A stick that is let go flies back to its centre; a finger that is lifted does not go /// back to where it came from first. A title that goes by how far the finger was dragged /// when it lifts (ASTRO BOT Rescue Mission pulls a catapult back that way at the end of /// every level, and shoots when the finger lets go) would find nothing pulled at all. So -/// the finger stays where the stick was before it started back, and lifts from there once -/// the stick has come to rest. +/// the finger goes to where the stick was before it started back, and lifts from there +/// once the stick has come to rest, however briefly the stick was out. /// - A stick may be moved through its centre, from one side to the other, without the finger /// lifting on the way: a drag over the whole pad. /// - The stick does not have to be pushed all the way for the finger to reach the pad's edge, @@ -53,11 +59,11 @@ public: return Lifted(); } down = true; - down_since = time; armed = false; held = false; - finger_x = 0.0f; - finger_y = 0.0f; + goal_x = x; + goal_y = y; + finger.Land(time, 0.5f - x / r * Back, 0.5f - y / r * Back); return Touching(); } @@ -72,15 +78,16 @@ public: const bool flying_back = r < peak->r - Drop && r <= previous_r + 0.01f; const bool inside = r <= Start; if (!inside && !flying_back) { - finger_x = x; - finger_y = y; + goal_x = x; + goal_y = y; held = false; } else if (flying_back && !held) { - finger_x = peak->x; - finger_y = peak->y; + goal_x = peak->x; + goal_y = peak->y; held = true; } - if (inside && IsStill(time) && time - down_since >= ShortestTouch) { + const bool there = finger.Follow(time, OnPad(goal_x), OnPad(goal_y)); + if (inside && IsStill(time) && there && finger.Down(time) >= ShortestTouch) { down = false; armed = r <= Rest; return Lifted(); @@ -104,11 +111,13 @@ public: /// it is further out than that. static constexpr float Rest = 0.15f; static constexpr float Start = 0.25f; + /// How far behind the pad's middle the finger comes down, of the pad's size. + static constexpr float Back = 0.25f; /// How far from the pad's centre the stick would put the finger if the pad went on, of /// the pad's size: the finger is at the pad's edge with the stick four fifths out. static constexpr float Reach = 0.62f; /// A touch lasts this long at least, in seconds. - static constexpr double ShortestTouch = 0.3; + static constexpr double ShortestTouch = 0.36; private: struct Sample { @@ -152,29 +161,29 @@ private: return false; } + /// Where on the pad the stick, that far out, wants the finger. + static float OnPad(float value) { + const float at = 0.5f + value * Reach; + return at < 0.0f ? 0.0f : at > 1.0f ? 1.0f : at; + } + Touch Touching() const { - const auto on_pad = [](float value) { - const float at = 0.5f + value * Reach; - return at < 0.0f ? 0.0f : at > 1.0f ? 1.0f : at; - }; - return {true, on_pad(finger_x), on_pad(finger_y)}; + return {true, finger.x, finger.y}; } Touch Lifted() const { - Touch touch = Touching(); - touch.down = false; - return touch; + return {false, finger.x, finger.y}; } std::array samples{}; size_t newest{}; size_t count{}; bool down{}; - double down_since{}; bool armed{}; bool held{}; - float finger_x{}; - float finger_y{}; + float goal_x{}; + float goal_y{}; + MovedFinger finger; }; } // namespace Input diff --git a/tools/quest-drystart.sh b/tools/quest-drystart.sh index 26b9d09..4cdbe4b 100644 --- a/tools/quest-drystart.sh +++ b/tools/quest-drystart.sh @@ -37,7 +37,7 @@ for name in host.log core.log; do [ -s "$out/$name" ] || echo "no $name" done # The pictures of the status panel, as the activity drew them. -for number in 0 1 2 3 4 5 6 7; do +for number in 0 1 2 3 4 5 6 7 8 9 10 11; do "$ADB" exec-out "run-as $package cat files/drystart/status-$number.png" \ > "$out/status-$number.png" 2>/dev/null [ -s "$out/status-$number.png" ] || rm -f "$out/status-$number.png" diff --git a/tools/tests/pad_gestures_test.cpp b/tools/tests/pad_gestures_test.cpp new file mode 100644 index 0000000..8e5ddb0 --- /dev/null +++ b/tools/tests/pad_gestures_test.cpp @@ -0,0 +1,155 @@ +// Checks the touchpad gestures that buttons make (controllers that have no touchpad): that +// the pad is pressed for as long as its button is held, that a button swipes forward once +// for each press, that a pull is held and let go with its button, and that the game sees each +// of them for what it is, however few frames a second it draws. +// +// clang-cl /std:c++latest /EHsc -fuse-ld=lld /I shadps4-arm64-main/src /I tools/tests +// tools/tests/pad_gestures_test.cpp /Fe:build/tests/pad_gestures_test.exe +#include +#include +#include +#include +#include + +#include "input/pad_gestures.h" +#include "touch_game_model.h" + +using Input::PadGestures; + +static int failures = 0; + +static void Check(bool ok, const std::string& what) { + std::printf("%s %s\n", ok ? "ok " : "FAIL", what.c_str()); + failures += ok ? 0 : 1; +} + +/// The gestures as a host makes them, ninety times a second, of what `at(seconds)` says the +/// buttons do. +static std::function Made( + const std::function& at) { + struct State { + PadGestures gestures; + double moved{-1.0}; + GameModel::Finger now; + }; + auto state = std::make_shared(); + return [state, at](double time) { + if (time - state->moved >= 1.0 / 90.0) { + state->moved = time; + const PadGestures::Touch touch = state->gestures.Update(time, at(time)); + state->now = {touch.down, touch.x, touch.y, touch.pressed}; + } + return state->now; + }; +} + +static bool Between(double time, double from, double to) { + return time >= from && time < to; +} + +int main() { + // By themselves. + { + PadGestures gestures; + PadGestures::Touch touch = gestures.Update(0.0, {}); + Check(!touch.down && !touch.pressed && !gestures.IsActive(), "no button, no finger"); + + touch = gestures.Update(0.10, {.press = true}); + Check(touch.down && touch.pressed && touch.x == 0.5f && touch.y == 0.5f, + "the press button presses the pad in its middle, at once"); + touch = gestures.Update(0.15, {}); + Check(touch.down && touch.pressed, "however briefly: it stays pressed for a moment"); + touch = gestures.Update(0.30, {}); + Check(!touch.down && !touch.pressed, "and is let go"); + touch = gestures.Update(0.50, {.press = true}); + touch = gestures.Update(3.00, {.press = true}); + Check(touch.down && touch.pressed, "held, it stays pressed"); + touch = gestures.Update(3.01, {}); + Check(!touch.down && !touch.pressed, "until the button is let go"); + + gestures.Reset(); + touch = gestures.Update(4.00, {.swipe = true}); + Check(touch.down && !touch.pressed && touch.y == PadGestures::SwipeFrom, + "the swipe button puts a finger near the player's edge"); + float least = 1.0f; + bool lifted = false; + double lifted_at = 0.0; + for (double time = 4.01; time < 5.0; time += 0.011) { + touch = gestures.Update(time, {.swipe = true}); + if (touch.down) { + least = std::fmin(least, touch.y); + } else if (!lifted) { + lifted = true; + lifted_at = time; + } + } + Check(lifted && least == PadGestures::SwipeTo && lifted_at > 4.25 && lifted_at < 4.45, + "takes it to the far edge and off, in a third of a second, though the button stays held"); + Check(!gestures.IsActive(), "once: a button that is held does not swipe again"); + } + + for (const double rate : {26.0, 30.0, 45.0, 60.0}) { + bool pressed = true, tapped = true, swiped = true, twice = true, pulled = true, tugged = true, + held = true, after = true; + for (const double phase : {0.0, 0.2, 0.4, 0.6, 0.8}) { + // Water: the button held for a second. + GameModel::Seen seen = GameModel::Watch(rate, phase, 2.0, Made([](double time) { + return PadGestures::Controls{.press = Between(time, 0.2, 1.2)}; + })); + pressed = pressed && seen.presses == 1 && seen.touches == 1 && seen.shots == 0 && + seen.pressed_frames >= static_cast(rate * 0.95) && + seen.pressed_frames <= static_cast(rate * 1.15) + 1; + // The button touched for a blink. + seen = GameModel::Watch(rate, phase, 1.0, Made([](double time) { + return PadGestures::Controls{.press = Between(time, 0.2, 0.22)}; + })); + tapped = tapped && seen.presses == 1 && seen.pressed_frames >= 3; + // One swipe. + seen = GameModel::Watch(rate, phase, 1.0, Made([](double time) { + return PadGestures::Controls{.swipe = Between(time, 0.2, 0.25)}; + })); + swiped = swiped && seen.touches == 1 && seen.frames >= 5 && seen.first_y < 0.25f && + seen.last_y > 0.85f && seen.largest_step < 0.45f && seen.shots == 0 && + seen.presses == 0; + // The button pressed three times in a hurry: two swipes at least, apart. + seen = GameModel::Watch(rate, phase, 2.0, Made([](double time) { + return PadGestures::Controls{.swipe = Between(time, 0.20, 0.25) || Between(time, 0.35, 0.40) || + Between(time, 0.75, 0.80)}; + })); + twice = twice && seen.touches == 3 && seen.least_gap >= 1; + // The catapult: pulled and held, let go. + seen = GameModel::Watch(rate, phase, 2.0, Made([](double time) { + return PadGestures::Controls{.pull = Between(time, 0.2, 1.0)}; + })); + pulled = pulled && seen.touches == 1 && seen.shots == 1 && seen.first_y > 0.75f && + seen.last_y < 0.05f; + // Pulled and let go at once. + seen = GameModel::Watch(rate, phase, 1.5, Made([](double time) { + return PadGestures::Controls{.pull = Between(time, 0.2, 0.23)}; + })); + tugged = tugged && seen.touches == 1 && seen.shots == 1; + // Held for five seconds: nothing shoots before it is let go. + seen = GameModel::Watch(rate, phase, 5.0, Made([](double time) { + return PadGestures::Controls{.pull = time >= 0.2}; + })); + held = held && seen.touches == 1 && seen.shots == 0 && seen.last_y < 0.05f; + // A pull asked for in the middle of a swipe comes after it. + seen = GameModel::Watch(rate, phase, 2.5, Made([](double time) { + return PadGestures::Controls{.swipe = Between(time, 0.2, 0.3), .pull = Between(time, 0.3, 1.2)}; + })); + after = after && seen.touches == 2 && seen.shots == 1 && seen.least_gap >= 1; + } + const std::string fps = " (a game of " + std::to_string(static_cast(rate)) + " frames a second)"; + Check(pressed, "the pad is pressed for as long as its button is held" + fps); + Check(tapped, "a blink of the button is a press the game sees" + fps); + Check(swiped, "a swipe is seen from near the player to the far edge, on its way" + fps); + Check(twice, "swipes one after the other are apart" + fps); + Check(pulled, "pulled, held and let go: the catapult shoots" + fps); + Check(tugged, "pulled and let go at once: it shoots as well" + fps); + Check(held, "held, it does not shoot" + fps); + Check(after, "a pull in the middle of a swipe comes after the swipe" + fps); + } + + std::printf("%d failed\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/tools/tests/pad_router_test.cpp b/tools/tests/pad_router_test.cpp index 627c82a..d9d4d44 100644 --- a/tools/tests/pad_router_test.cpp +++ b/tools/tests/pad_router_test.cpp @@ -1,7 +1,8 @@ // Checks what the Quest app makes of the controllers a player has (quest-host/cpp/pad_router.h): // which of a gamepad and the headset's own controllers the game is played with, what the // headset's controllers' buttons and sticks become, the right stick as a finger on the -// touchpad, and what buttons mean about the view besides. +// touchpad, the buttons that do the touchpad's gestures, and what buttons mean about the view +// besides. // // clang-cl /std:c++latest /EHsc -fuse-ld=lld /I quest-host/cpp /I shadps4-arm64-main/src/input // tools/tests/pad_router_test.cpp /Fe:build/tests/pad_router_test.exe @@ -87,7 +88,7 @@ int main() { // --- a gamepad on its own --------------------------------------------------------------- { Bench bench; - bench.router.SetGamepadConnected(true, 0.0); + bench.router.SetGamepadConnected(true, true, 0.0); bench.router.SetPlaying(true, 0.0); Check(bench.Source() == Source::Gamepad, "a gamepad alone is what the game is played with"); @@ -146,7 +147,7 @@ int main() { pad = {}; pad.buttons = Button::Triangle; bench.router.SetGamepad(pad, bench.time); - bench.router.SetGamepadConnected(false, bench.time); + bench.router.SetGamepadConnected(false, false, bench.time); Check(bench.out.pad.buttons == 0 && !bench.out.pad.has_motion && bench.Source() == Source::None, "nothing is held down on a gamepad that is gone"); } @@ -154,7 +155,7 @@ int main() { // --- a gamepad's touchpad, or what stands in for it --------------------------------------- { Bench bench; - bench.router.SetGamepadConnected(true, 0.0); + bench.router.SetGamepadConnected(true, true, 0.0); bench.router.SetPlaying(true, 0.0); GamepadState pad; pad.touch_down = true; @@ -311,7 +312,7 @@ int main() { // --- both at hand --------------------------------------------------------------------------- { Bench bench; - bench.router.SetGamepadConnected(true, 0.0); + bench.router.SetGamepadConnected(true, true, 0.0); bench.router.SetPlaying(true, 0.0); const float gyro[3]{}; const float accel[3]{0.0f, 9.81f, 0.0f}; @@ -370,12 +371,12 @@ int main() { Check(bench.Source() == Source::Gamepad, "so does a trigger of the gamepad"); // The gamepad's battery gives out. - bench.router.SetGamepadConnected(false, bench.time); + bench.router.SetGamepadConnected(false, false, bench.time); touch = Held(); bench.Pass(0.05, &touch); Check(bench.Source() == Source::Touch, "the gamepad gone, the headset's controllers play on"); // It comes back, and the headset's controllers are put down. - bench.router.SetGamepadConnected(true, bench.time); + bench.router.SetGamepadConnected(true, true, bench.time); Check(bench.Source() == Source::Touch, "a gamepad that comes back does not take over by being there"); TouchState away; bench.Pass(0.2, &away); @@ -426,8 +427,9 @@ int main() { touch.a = true; touch.right_trigger = 1.0f; bench.Pass(0.05, &touch); - Check(bench.out.pad.buttons == (Button::Cross | Button::R2) && bench.out.pad.right_trigger == 255, - "let go and pressed again, both are the game's"); + Check(bench.out.pad.buttons == (Button::Cross | Button::R2 | Button::TouchPad) && + bench.out.pad.right_trigger == 255 && bench.out.pad.touch_down, + "let go and pressed again, both are the game's (and the trigger presses the touchpad)"); Check(bench.router.TakeRecenter() == PadRouter::RecenterSeat, "and that press of A takes the seat"); TouchState away; @@ -437,7 +439,7 @@ int main() { { // The player chooses in the menu with the headset's controllers while a gamepad is on. Bench bench; - bench.router.SetGamepadConnected(true, 0.0); + bench.router.SetGamepadConnected(true, true, 0.0); TouchState touch = Held(); bench.Pass(0.1, &touch); Check(bench.Source() == Source::Gamepad, "a gamepad that is on is the controller before the menu"); @@ -451,6 +453,150 @@ int main() { Check(bench.Source() == Source::Gamepad, "controllers nobody holds are not chosen with"); } + // --- the touchpad's gestures, on buttons ----------------------------------------------------- + { + Bench bench; + bench.router.SetPlaying(true, 0.0); + TouchState touch = Held(); + bench.Pass(0.2, &touch); + Check(bench.router.Look().gesture_buttons, "the headset's controllers have no touchpad: buttons do its gestures"); + + // The trigger of the hand that holds the controller in the game presses the pad. + touch.right_trigger = 1.0f; + bench.Pass(0.05, &touch); + Check(bench.out.pad.buttons == (Button::R2 | Button::TouchPad) && bench.out.pad.touch_down && + bench.out.pad.touch_x == 960 && bench.out.pad.touch_y == 540, + "the right trigger presses the touchpad, in its middle (and is still R2)"); + bench.Pass(2.0, &touch); + Check((bench.out.pad.buttons & Button::TouchPad) != 0 && bench.out.pad.touch_down, + "for as long as it is pulled"); + touch = Held(); + bench.Pass(0.1, &touch); + Check(bench.out.pad.buttons == 0 && !bench.out.pad.touch_down, "and lets go of it"); + + // Its grip swipes forward, once. + touch.right_grip = 1.0f; + bench.Pass(0.03, &touch); + Check(bench.out.pad.buttons == Button::R1 && bench.out.pad.touch_down && bench.out.pad.touch_y == 863, + "the right grip puts a finger near the player's edge of the pad (and is still R1)"); + int least = 1079; + for (int i = 0; i < 60; ++i) { + bench.Pass(0.011, &touch); + if (bench.out.pad.touch_down) { + least = std::min(least, bench.out.pad.touch_y); + } + } + Check(least == 129 && !bench.out.pad.touch_down && bench.out.pad.buttons == Button::R1, + "takes it to the far edge and off, once, however long the grip is held"); + touch = Held(); + bench.Pass(0.1, &touch); + + // The other hand's trigger pulls back, and lets go when it is let go. + touch.left_trigger = 1.0f; + bench.Pass(0.03, &touch); + Check(bench.out.pad.buttons == Button::L2 && bench.out.pad.touch_down && bench.out.pad.touch_y == 216, + "the left trigger puts a finger near the far edge (and is still L2)"); + bench.Pass(1.0, &touch); + Check(bench.out.pad.touch_down && bench.out.pad.touch_y == 1047, "pulls it back to the near edge and holds it there"); + touch = Held(); + bench.Pass(0.1, &touch); + Check(!bench.out.pad.touch_down && bench.out.pad.touch_y == 1047, "and lets go there: the catapult shoots"); + + // A gesture comes before the stick's finger. + touch.right_y = -1.0f; + bench.Pass(0.5, &touch); + Check(bench.out.pad.touch_down && bench.out.pad.touch_y == 1079, "(the stick has pulled a finger back)"); + touch.right_trigger = 1.0f; + bench.Pass(0.05, &touch); + Check(bench.out.pad.touch_down && bench.out.pad.touch_y == 540 && (bench.out.pad.buttons & Button::TouchPad) != 0, + "a button's gesture takes the pad from the stick's finger"); + touch = Held(); + bench.Pass(0.3, &touch); + + // The other way round for a player whose left controller is the one in the game. + bench.router.SetPadHand(0); + touch.left_trigger = 1.0f; + bench.Pass(0.05, &touch); + Check(bench.out.pad.buttons == (Button::L2 | Button::TouchPad), "with the left controller as the one in the game, its trigger presses"); + touch = Held(); + bench.Pass(0.3, &touch); + touch.right_trigger = 1.0f; + bench.Pass(0.5, &touch); + Check(bench.out.pad.buttons == Button::R2 && bench.out.pad.touch_down && bench.out.pad.touch_y == 1047, + "and the right trigger pulls"); + touch = Held(); + touch.left_grip = 1.0f; + bench.Pass(0.5, &touch); + touch = Held(); + bench.Pass(0.1, &touch); + touch.left_grip = 1.0f; + bench.Pass(0.03, &touch); + Check(bench.out.pad.touch_down && bench.out.pad.touch_y == 863, "and its grip swipes"); + } + { + // A gamepad without a touchpad. + Bench bench; + bench.router.SetGamepadConnected(true, false, 0.0); + bench.router.SetPlaying(true, 0.0); + bench.Pass(0.2); + Check(bench.router.Look().gesture_buttons, "a gamepad without a touchpad: buttons do its gestures"); + GamepadState pad; + pad.buttons = Button::R2; + pad.right_trigger = 1.0f; + bench.router.SetGamepad(pad, bench.time); + bench.Pass(0.3); + Check(bench.out.pad.buttons == (Button::R2 | Button::TouchPad) && bench.out.pad.touch_down && + bench.out.pad.right_trigger == 255, + "R2 presses the touchpad"); + bench.router.SetGamepad({}, bench.time); + bench.Pass(0.2); + Check(bench.out.pad.buttons == 0 && !bench.out.pad.touch_down, "and lets go of it"); + pad = {}; + pad.buttons = Button::R1; + bench.router.SetGamepad(pad, bench.time); + bench.Pass(0.03); + Check(bench.out.pad.buttons == Button::R1 && bench.out.pad.touch_down && bench.out.pad.touch_y == 863, + "R1 swipes forward"); + bench.router.SetGamepad({}, bench.time); + bench.Pass(0.6); + Check(!bench.out.pad.touch_down && bench.out.pad.touch_y == 129, "to the far edge and off, though R1 was only tapped"); + pad = {}; + pad.buttons = Button::L2; + pad.left_trigger = 1.0f; + bench.router.SetGamepad(pad, bench.time); + bench.Pass(0.02); + bench.router.SetGamepad({}, bench.time); + bench.Pass(0.2); + Check(bench.out.pad.touch_down, "L2 tapped pulls a finger back, all of the way"); + bench.Pass(0.4); + Check(!bench.out.pad.touch_down && bench.out.pad.touch_y == 1047, "and lets go at the near edge"); + + // OPTIONS is counted, for a host that shows the controls with the game's pause. + const uint32_t before = bench.router.Look().options_presses; + pad = {}; + pad.buttons = Button::Options; + bench.router.SetGamepad(pad, bench.time); + bench.Pass(0.2); + bench.router.SetGamepad({}, bench.time); + bench.Pass(0.1); + Check(bench.router.Look().options_presses == before + 1, "a press of OPTIONS is counted once"); + } + { + // A gamepad with a touchpad of its own keeps its buttons to itself. + Bench bench; + bench.router.SetGamepadConnected(true, true, 0.0); + bench.router.SetPlaying(true, 0.0); + Check(!bench.router.Look().gesture_buttons, "a gamepad with a touchpad needs no buttons for it"); + GamepadState pad; + pad.buttons = Button::R2 | Button::R1 | Button::L2; + pad.right_trigger = 1.0f; + pad.left_trigger = 1.0f; + bench.router.SetGamepad(pad, bench.time); + bench.Pass(0.5); + Check(bench.out.pad.buttons == (Button::R2 | Button::R1 | Button::L2) && !bench.out.pad.touch_down, + "its triggers and shoulder buttons are only that"); + } + std::printf("%d failed\n", failures); return failures == 0 ? 0 : 1; } diff --git a/tools/tests/stick_finger_test.cpp b/tools/tests/stick_finger_test.cpp index 23d9a52..e4ee7d4 100644 --- a/tools/tests/stick_finger_test.cpp +++ b/tools/tests/stick_finger_test.cpp @@ -2,15 +2,17 @@ // stick let go lifts the finger where it was, not back at the centre it flies to, that a stick // moved through its centre drags without lifting, and that a stick which drifts touches nothing. // -// clang-cl /std:c++latest /EHsc -fuse-ld=lld /I shadps4-arm64-main/src +// clang-cl /std:c++latest /EHsc -fuse-ld=lld /I shadps4-arm64-main/src /I tools/tests // tools/tests/stick_finger_test.cpp /Fe:build/tests/stick_finger_test.exe #include #include #include +#include #include #include #include "input/stick_finger.h" +#include "touch_game_model.h" using Input::StickFinger; @@ -86,7 +88,8 @@ int main() { y = time < 0.5 ? Ramp(time, 0.1, 0.25, 0.0f, 1.0f) : LetGo(time, 0.5, 1.0f); }); Check(run.touches == 1 && !run.down, "pulled and let go: one touch, lifted" + hz); - Check(std::abs(run.first_y - 0.5f) < 0.01f, "it comes down in the middle of the pad" + hz); + Check(std::abs(run.first_y - 0.25f) < 0.01f && std::abs(run.first_x - 0.5f) < 0.01f, + "it comes down behind the middle of the pad, on the side the stick comes from" + hz); Check(run.last_y > 0.99f, "it lifts at the pad's near edge, not back at the centre" + hz); Check(run.lifted_at > 0.5 && run.lifted_at < 0.62, "it lifts within a moment" + hz); @@ -175,6 +178,72 @@ int main() { Check(run.touches == 0, "a stick short of touching does not touch" + hz); } + // What the game makes of it: it looks at the pad once a frame, and at thirty frames a + // second (which is what a Quest manages in a level) a stick is at its end before it has + // looked twice. + const auto stick = [](const std::function& at) { + // (The host moves the finger for every picture the headset shows.) + struct State { + StickFinger finger; + double moved{-1.0}; + GameModel::Finger now; + }; + auto state = std::make_shared(); + return [state, at](double time) { + if (time - state->moved >= 1.0 / 90.0) { + state->moved = time; + float x = 0.0f, y = 0.0f; + at(time, x, y); + const StickFinger::Touch touch = state->finger.Update(time, x, y); + state->now = {touch.down, touch.x, touch.y, false}; + } + return state->now; + }; + }; + for (const double rate : {26.0, 30.0, 45.0, 60.0}) { + bool pulled = true, flicked = true, half = true, swiped = true, twice = true; + for (const double phase : {0.0, 0.2, 0.4, 0.6, 0.8}) { + // The catapult: the stick pulled back as fast as a thumb does it, held, let go. + GameModel::Seen seen = GameModel::Watch(rate, phase, 1.5, stick([](double time, float& x, float& y) { + x = 0.0f; + y = time < 0.7 ? Ramp(time, 0.1, 0.14, 0.0f, 1.0f) : LetGo(time, 0.7, 1.0f); + })); + pulled = pulled && seen.touches == 1 && seen.shots == 1; + // Pulled and let go at once. + seen = GameModel::Watch(rate, phase, 1.5, stick([](double time, float& x, float& y) { + x = 0.0f; + y = time < 0.14 ? Ramp(time, 0.1, 0.14, 0.0f, 1.0f) : LetGo(time, 0.14, 1.0f); + })); + flicked = flicked && seen.touches == 1 && seen.shots == 1; + // Pulled six tenths of the way only. + seen = GameModel::Watch(rate, phase, 1.5, stick([](double time, float& x, float& y) { + x = 0.0f; + y = time < 0.7 ? Ramp(time, 0.1, 0.14, 0.0f, 0.6f) : LetGo(time, 0.7, 0.6f); + })); + half = half && seen.shots == 1; + // A flick forward: a swipe the game can follow, from near the player to the far edge. + seen = GameModel::Watch(rate, phase, 1.5, stick([](double time, float& x, float& y) { + x = 0.0f; + y = time < 0.14 ? Ramp(time, 0.1, 0.14, 0.0f, -1.0f) : LetGo(time, 0.14, -1.0f); + })); + swiped = swiped && seen.touches == 1 && seen.frames >= 5 && seen.first_y < 0.3f && + seen.last_y > 0.98f && seen.largest_step < 0.45f && seen.shots == 0; + // Two flicks, one right after the other: two swipes with the finger off between. + seen = GameModel::Watch(rate, phase, 1.8, stick([](double time, float& x, float& y) { + x = 0.0f; + const double in = std::fmod(time, 0.6); + y = in < 0.14 ? Ramp(in, 0.1, 0.14, 0.0f, -1.0f) : LetGo(in, 0.14, -1.0f); + })); + twice = twice && seen.touches == 3 && seen.least_gap >= 1; + } + const std::string fps = " (a game of " + std::to_string(static_cast(rate)) + " frames a second)"; + Check(pulled, "the stick pulled back fast, held and let go shoots the catapult" + fps); + Check(flicked, "so does the stick pulled back and let go at once" + fps); + Check(half, "and the stick pulled back six tenths of the way" + fps); + Check(swiped, "a flick forward is a swipe from near the player to the far edge, seen on its way" + fps); + Check(twice, "flicks one after the other are swipes one after the other" + fps); + } + // A finger taken off by something else (a real one took over): the stick has to rest first. StickFinger finger; finger.Update(0.00, 0.0f, 0.0f); diff --git a/tools/tests/touch_game_model.h b/tools/tests/touch_game_model.h new file mode 100644 index 0000000..941375d --- /dev/null +++ b/tools/tests/touch_game_model.h @@ -0,0 +1,106 @@ +// What ASTRO BOT Rescue Mission makes of a finger on the touchpad, as far as its code was +// read (version 1.00: the pad is read at 0xc04cc3, the level-end catapult is 0x436fc0), for +// the tests of fingers that are moved by sticks and buttons. +// +// The game reads the pad once for every frame it draws and keeps the finger's place as +// x = raw x / 1920 and y = 1 - raw y / 950 (the far edge of the pad is up). The emulator +// hands it raw y = place * 941. +#pragma once + +#include +#include + +namespace GameModel { + +struct Finger { + bool down{}; + float x{0.5f}; + float y{0.5f}; + bool pressed{}; +}; + +struct Seen { + /// Frames the game saw a finger on the pad in, and touches it told apart. + int frames{}; + int touches{}; + /// Where it first and last saw the finger of the last touch, in its own terms. + float first_x{}, first_y{}, last_x{}, last_y{}; + /// The furthest the finger got from one frame to the next. + float largest_step{}; + /// Frames in which the pad was pressed, and how often it went down. + int pressed_frames{}; + int presses{}; + /// Catapults shot. + int shots{}; + /// Frames without a finger between two touches, at the least (0: never two touches). + int least_gap{}; +}; + +/// Has `at(seconds)` say where the finger is, a thousand times a second, and the game look +/// `rate` times a second, the first time `phase` of a frame after the start. +inline Seen Watch(double rate, double phase, double length, + const std::function& at) { + static constexpr float MinTouchDuration = 0.25f; + static constexpr float MinTouchGestureAmplitude = 0.4f; + Seen seen; + const double frame = 1.0 / rate; + double next_look = phase * frame; + bool was_down = false; + bool was_pressed = false; + float duration = 0.0f; + float start_y = 0.0f; + float before_x = 0.0f, before_y = 0.0f; + int gap = 0; + bool had_touch = false; + Finger finger; + for (double time = 0.0; time < length; time += 0.001) { + finger = at(time); + if (time + 1e-9 < next_look) { + continue; + } + next_look += frame; + const float x = std::floor(finger.x * 1920.0f) / 1920.0f; + const float y = 1.0f - std::floor(finger.y * 941.0f) / 950.0f; + if (finger.down) { + ++seen.frames; + if (!was_down) { + ++seen.touches; + if (had_touch) { + seen.least_gap = seen.least_gap == 0 ? gap : (gap < seen.least_gap ? gap : seen.least_gap); + } + had_touch = true; + seen.first_x = x; + seen.first_y = y; + seen.largest_step = 0.0f; + start_y = y; + duration = 0.0f; + } else { + const float step = std::hypot(x - before_x, y - before_y); + seen.largest_step = step > seen.largest_step ? step : seen.largest_step; + } + duration += static_cast(frame); + seen.last_x = x; + seen.last_y = y; + before_x = x; + before_y = y; + gap = 0; + } else { + if (was_down && duration > MinTouchDuration && + start_y - seen.last_y > MinTouchGestureAmplitude) { + ++seen.shots; + } + ++gap; + } + if (finger.pressed) { + ++seen.pressed_frames; + if (!was_pressed) { + ++seen.presses; + } + } + was_down = finger.down; + was_pressed = finger.pressed; + } + return seen; +} + +} // namespace GameModel