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