mirror of
https://github.com/mitch030504/AstroQuest-Frame.git
synced 2026-10-29 04:02:13 +01:00
Quest app: the headset's own controllers play the game, and what other gamepads lack is made up for
The app was made for a DualSense and took everything else for a lesser one of those. Players without one could play on the PC, with the headset's controllers, and not on the headset. The headset's own controllers: the app reads them through the headset's runtime (OpenXR actions, the layout the PC host has: A cross, B square, X circle, Y triangle and both together to blow, grips L1/R1, triggers L2/R2, right stick the finger on the touchpad, left menu button OPTIONS, both sticks in to reset the view), the right one is the controller in the game with where it is and how it points, both shake with the game's rumble. The gamepad the system makes of them for other apps is no longer taken for a gamepad: it says neither where it is nor whether anybody holds it. Which of the two: a small router (quest-host/cpp/pad_router.h) between the activity, which hears of gamepads, and the session, which reads the controllers. The game is played with the one used last. From a gamepad only a button under a thumb or a stick takes over, never a trigger or a grip: while a gamepad is held it is hands the headset sees. What buttons mean about the view besides (the first press of X takes the seat, OPTIONS held resets the view) moved there from the activity, so that it is the same for both. The start-up menu takes either; what confirmed it is not pressed in the game. Gamepads that are not a PlayStation's: - The right stick as the touchpad's finger is Input::StickFinger now, as on the PC: the finger lifts where the stick was let go, which the catapult at the end of a level needs. A button that presses the touchpad touches it in its middle. - A gamepad without motion sensors followed the hands to where they were and kept pointing straight ahead. Now the hands turn and tilt it (quest-host/cpp/held_pad.h): the line between the palms is its sideways axis, the way the hands point is its tilt. The app says so with a new flag of the pad pose message (HeldInHands), and the emulator lets go of a controller that was a tracked device once it no longer is (it stayed where that device was last seen for as long as no motion sensor of its own spoke up). - Motion sensors are only taken from PlayStation controllers: which way is up for one is each maker's own affair. - A right stick on RX/RY, triggers that are only buttons, a Back button the system takes for its own "back". For trying a build on a headset whose owner plays with the installed one: quest-host/build.sh --test-package builds the app under another name, which installs next to it; the test tools take ASTRO_PACKAGE. The self-test can report a controller the way a session does (HOST_PAD) and keep pictures of it (HOST_SHOTS). Tried on a Quest 3 without wearing it: the runtime takes the bindings, the app starts and plays with a made-up gamepad, and in the game's picture the controller is where and how the self-test says a tracked controller or hands around a gamepad are, and comes back when they are lost. tools/tests/pad_router_test.cpp and held_pad_test.cpp try the rest on the PC. Not tried: any of it worn, and no gamepad other than a DualSense. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d5bd58fe6
commit
ee4f9aa03f
26 files changed
+2595
-240
No files matched your search
@@ -47,9 +47,11 @@ import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Hosts the PS4 emulator core on a Meta Quest: installs its Linux runtime, starts it, feeds it a
|
||||
* paired DualSense as the DualShock 4 the game expects (buttons, sticks, touchpad, motion
|
||||
* sensors in; rumble and light bar colour out) and lets the native side show its frames in the
|
||||
* headset.
|
||||
* gamepad paired with the headset as the DualShock 4 the game expects (of a DualSense
|
||||
* everything: buttons, sticks, touchpad, motion sensors in; rumble and light bar colour out;
|
||||
* of other gamepads what they have) and lets the native side show its frames in the headset.
|
||||
* The headset's own controllers play as well; those the native side reads itself, and it
|
||||
* decides which of the two the game is played with (PadRouter).
|
||||
*
|
||||
* Files the user provides live in the app's external folder
|
||||
* (/sdcard/Android/data/com.astrobotquest.vrhost/files):
|
||||
@@ -71,9 +73,6 @@ public class MainActivity extends Activity
|
||||
private static final int TRIANGLE = 0x1000, CIRCLE = 0x2000, CROSS = 0x4000, SQUARE = 0x8000;
|
||||
private static final int TOUCHPAD = 0x100000;
|
||||
|
||||
// The touchpad as the emulator takes it: the resolution of a DualSense's.
|
||||
private static final int TOUCH_WIDTH = 1920, TOUCH_HEIGHT = 1080;
|
||||
|
||||
private static final int STATUS_WIDTH = 1024, STATUS_HEIGHT = 512;
|
||||
|
||||
/** Second place games are looked for; every app may read what adb puts there. */
|
||||
@@ -94,7 +93,8 @@ public class MainActivity extends Activity
|
||||
|
||||
private native void nativeStartXr(float refreshRate, int eyeWidth, int eyeHeight,
|
||||
boolean trackHands, int sharpen, boolean cubic, boolean showStats, float predictMs,
|
||||
int dynamicResolution, boolean cpuBoost);
|
||||
int dynamicResolution, boolean cpuBoost, boolean ownControllers, int padHand,
|
||||
float padTilt, boolean rumble, boolean stickTouchpad);
|
||||
|
||||
private native boolean nativeStartCore(String loader, String runtimeRoot, String storageRoot,
|
||||
String game, String logFile, String[] extraArgs, String[] extraEnv);
|
||||
@@ -109,13 +109,38 @@ public class MainActivity extends Activity
|
||||
|
||||
private native void nativeSetStatusImage(ByteBuffer pixels);
|
||||
|
||||
private native void nativeInput(int buttons, int leftX, int leftY, int rightX, int rightY,
|
||||
int leftTrigger, int rightTrigger, boolean touchDown, int touchX, int touchY);
|
||||
/**
|
||||
* What the gamepad says: sticks from -1 to 1, triggers from 0 to 1, the finger on its own
|
||||
* touchpad from 0 to 1 across and from its far edge.
|
||||
*/
|
||||
private native void nativeInput(int buttons, float leftX, float leftY, float rightX,
|
||||
float rightY, float leftTrigger, float rightTrigger, boolean touchDown, float touchX,
|
||||
float touchY);
|
||||
|
||||
private native void nativeMotion(float gx, float gy, float gz, float ax, float ay, float az);
|
||||
|
||||
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);
|
||||
|
||||
/** The start-up menu is over: what the player presses is the game's. */
|
||||
private native void nativeSetPlaying(boolean playing);
|
||||
|
||||
/** What the game is played with, see the CONTROLLER_ constants. */
|
||||
private native int nativeController();
|
||||
|
||||
/** The player chose with it in the start-up menu. */
|
||||
private native void nativeUseController(int which);
|
||||
|
||||
/** What the headset's own controllers say to the start-up menu, see the MENU_ constants. */
|
||||
private native int nativeTouchMenu();
|
||||
|
||||
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;
|
||||
private static final int MENU_LEFT = 1, MENU_RIGHT = 2, MENU_CONFIRM = 4;
|
||||
|
||||
private static final int VENDOR_SONY = 0x054c;
|
||||
private static final int VENDOR_META = 0x2833;
|
||||
|
||||
@@ -151,6 +176,11 @@ public class MainActivity extends Activity
|
||||
private float padTouchX, padTouchY;
|
||||
private boolean padTouchClick;
|
||||
private final float[] acceleration = {0.0f, 9.81f, 0.0f};
|
||||
/** Whose sticks and triggers the fields below describe. */
|
||||
private int axesDevice = -1;
|
||||
private int rightXAxis = MotionEvent.AXIS_Z, rightYAxis = MotionEvent.AXIS_RZ;
|
||||
/** The gamepad says how far its triggers are pulled, and not only that they are. */
|
||||
private boolean analogTriggers = true;
|
||||
|
||||
// The controller itself: what the game's feedback goes to.
|
||||
private InputDevice gamepad;
|
||||
@@ -166,6 +196,9 @@ public class MainActivity extends Activity
|
||||
private boolean motion = true;
|
||||
private boolean rumble = true;
|
||||
private boolean trackHands = true;
|
||||
private boolean ownControllers = true;
|
||||
private int padHand = 1;
|
||||
private float padTilt;
|
||||
private int sharpen = 1;
|
||||
private boolean cubic;
|
||||
private boolean antialias = true;
|
||||
@@ -186,6 +219,12 @@ public class MainActivity extends Activity
|
||||
private long nextFovMoveAt;
|
||||
private float[] headsetFov;
|
||||
private static final int FOV_MIN = 50, FOV_MAX = 100, FOV_STEP = 5;
|
||||
private long nextOwnFovMoveAt;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private boolean ownConfirmArmed;
|
||||
/**
|
||||
* Set by the test that runs the activity without showing it (SandboxShell "drystart"):
|
||||
* nothing is asked of the user or of the system's window manager then.
|
||||
@@ -193,13 +232,6 @@ public class MainActivity extends Activity
|
||||
private boolean dryRun;
|
||||
private int statusPictures;
|
||||
private float microphoneGain = 1.0f;
|
||||
private boolean seatTaken;
|
||||
/** How long OPTIONS is held for the view to be reset, as on a PlayStation VR. */
|
||||
private static final long OPTIONS_HOLD_MS = 1000;
|
||||
private final Runnable optionsHeld = () -> {
|
||||
logInfo("OPTIONS held: view reset");
|
||||
nativeRecenter(RECENTER_PAD | RECENTER_SEAT);
|
||||
};
|
||||
private long quietFrames = -1;
|
||||
private long quietSince;
|
||||
private long statsFrames = -1;
|
||||
@@ -242,14 +274,23 @@ public class MainActivity extends Activity
|
||||
readSettings();
|
||||
choosingFov = !dryRun;
|
||||
nativeStartXr(refreshRate, 1440, 1536, trackHands, sharpen, cubic, showStats, predictMs,
|
||||
dynamicResolution, cpuBoost);
|
||||
dynamicResolution, cpuBoost, ownControllers, padHand, padTilt, rumble,
|
||||
stickTouchpad);
|
||||
|
||||
InputManager inputManager = getSystemService(InputManager.class);
|
||||
inputManager.registerInputDeviceListener(this, handler);
|
||||
findGamepad();
|
||||
|
||||
if (dryRun) {
|
||||
// Nobody sees the menu in a test: it is drawn for the pictures the test keeps,
|
||||
// once the way it waits for the headset and once with a headset's view.
|
||||
drawFovMenu();
|
||||
headsetFov = new float[] {1.376f, 0.839f, 0.966f, 1.428f};
|
||||
drawFovMenu();
|
||||
headsetFov = null;
|
||||
startGame();
|
||||
} else {
|
||||
handler.post(this::pollOwnControllers);
|
||||
}
|
||||
handler.post(this::refreshStatus);
|
||||
handler.post(this::applyFeedback);
|
||||
@@ -349,6 +390,12 @@ public class MainActivity extends Activity
|
||||
* emulator has it run by the clock
|
||||
* hands=0 do not use hand tracking to place the controller; it is then held
|
||||
* at a fixed spot in front of the player
|
||||
* own_controllers=0 never play with the headset's own controllers, whoever holds them
|
||||
* pad_hand=left with those, the left one is the controller in the game instead of
|
||||
* the right one
|
||||
* pad_tilt=-15 for a gamepad without motion sensors, which the hands holding it
|
||||
* turn and tilt in the game: how many degrees higher (lower, if
|
||||
* negative) its front points than the hands do, -60 to 60
|
||||
* sharpen=0 leave the picture as soft as it gets from being shown larger
|
||||
* than it was drawn. By default (1) the emulator sharpens it once
|
||||
* per frame of the game. 2 has the headset do it instead, with its
|
||||
@@ -359,7 +406,8 @@ public class MainActivity extends Activity
|
||||
* cubic=1 have the headset enlarge the picture with a cubic filter instead
|
||||
* of a linear one: sharper, for some GPU time on every refresh
|
||||
* stick_touchpad=0 stop using the right stick as a finger on the touchpad
|
||||
* motion=0 ignore the controller's motion sensors
|
||||
* motion=0 ignore the controller's motion sensors (the hands holding it
|
||||
* then say how it is turned, as for a gamepad that has none)
|
||||
* rumble=0 no vibration
|
||||
* msaa=4 let the game multisample as it does on the console (1, 2 or 4).
|
||||
* The default of 1 turns that off: it is by far the most expensive
|
||||
@@ -439,6 +487,15 @@ public class MainActivity extends Activity
|
||||
case "hands":
|
||||
trackHands = !value.equals("0");
|
||||
break;
|
||||
case "own_controllers":
|
||||
ownControllers = !value.equals("0");
|
||||
break;
|
||||
case "pad_hand":
|
||||
padHand = value.equalsIgnoreCase("left") || value.equals("0") ? 0 : 1;
|
||||
break;
|
||||
case "pad_tilt":
|
||||
padTilt = Math.max(-60.0f, Math.min(60.0f, Float.parseFloat(value)));
|
||||
break;
|
||||
case "sharpen":
|
||||
sharpen = Math.max(0, Math.min(4, Integer.parseInt(value)));
|
||||
break;
|
||||
@@ -510,6 +567,9 @@ public class MainActivity extends Activity
|
||||
}
|
||||
choosingFov = false;
|
||||
shownStatus = "";
|
||||
// From here on, what the player presses is the game's; what is still held down from
|
||||
// confirming the menu is not, until it is let go.
|
||||
nativeSetPlaying(true);
|
||||
nativeSetFieldOfView(fieldOfView);
|
||||
if (!dryRun) {
|
||||
getPreferences(MODE_PRIVATE).edit().putInt("fov", fieldOfView).apply();
|
||||
@@ -671,7 +731,8 @@ public class MainActivity extends Activity
|
||||
private void refreshStatus() {
|
||||
if (choosingFov) {
|
||||
headsetFov = nativeHeadsetFov();
|
||||
String status = "fov:" + fieldOfView + ":" + (gamepad != null)
|
||||
String status = "fov:" + fieldOfView + ":" + (gamepad != null) + ":"
|
||||
+ ((nativeController() & CONTROLLER_OWN_HELD) != 0)
|
||||
+ ":" + Arrays.toString(headsetFov);
|
||||
if (!status.equals(shownStatus)) {
|
||||
shownStatus = status;
|
||||
@@ -715,15 +776,24 @@ public class MainActivity extends Activity
|
||||
break;
|
||||
}
|
||||
// Waiting is the moment to say what the game will be played with.
|
||||
if (gamepad == null) {
|
||||
status += "\n\nNo controller found. Switch the DualSense on (PS button); pair it "
|
||||
+ "first under Settings > Bluetooth if it never was.";
|
||||
} else {
|
||||
int controller = nativeController();
|
||||
if ((controller & CONTROLLER_WHICH) == CONTROLLER_OWN) {
|
||||
status += "\n\nController: the headset's own (A cross, B square, X circle, Y "
|
||||
+ "triangle, right stick = touchpad, menu button = OPTIONS)."
|
||||
+ "\nTo reset the view, hold the menu button for a second or press both "
|
||||
+ "sticks in.";
|
||||
} else if (gamepad != null) {
|
||||
status += "\n\nController: " + gamepad.getName()
|
||||
+ (controllerSensors != null ? "" : " (no motion sensors)")
|
||||
+ "\nTo reset the view, hold OPTIONS for a second or press the PS button: "
|
||||
+ "where you are then is your seat, and the way you face is straight "
|
||||
+ (controllerSensors != null ? ""
|
||||
: " (no motion sensors: your hands turn it in the game)")
|
||||
+ "\nTo reset the view, hold OPTIONS for a second"
|
||||
+ (gamepad.getVendorId() == VENDOR_SONY ? " or press the PS button" : "")
|
||||
+ ": where you are then is your seat, and the way you face is straight "
|
||||
+ "ahead.";
|
||||
} else {
|
||||
status += "\n\nNo controller found. Pick up the headset's controllers, or switch "
|
||||
+ "a gamepad on (pair it first under Settings > Bluetooth if it never "
|
||||
+ "was).";
|
||||
}
|
||||
}
|
||||
if (!status.equals(shownStatus)) {
|
||||
@@ -750,7 +820,9 @@ public class MainActivity extends Activity
|
||||
String rate = statsRate;
|
||||
int xr = nativeXrStatus();
|
||||
return "Game: " + rate + " frames a second\nDisplay: " + ((xr >> 8) & 0xff) + " Hz\n"
|
||||
+ "Hands holding the controller: " + ((xr & 2) != 0 ? "seen" : "not seen");
|
||||
+ ((nativeController() & CONTROLLER_WHICH) == CONTROLLER_OWN
|
||||
? "The headset's controller: " + ((xr & 4) != 0 ? "seen" : "not seen")
|
||||
: "Hands holding the controller: " + ((xr & 2) != 0 ? "seen" : "not seen"));
|
||||
}
|
||||
|
||||
/** Visible coverage is the symmetric render envelope clipped by the actual headset. */
|
||||
@@ -814,17 +886,22 @@ public class MainActivity extends Activity
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(fieldOfView < 100 ? "Soft squircle border / Default: 85%"
|
||||
: "No added border / Default: 85%", STATUS_WIDTH / 2.0f, 385, paint);
|
||||
paint.setColor(gamepad != null ? Color.WHITE : Color.rgb(255, 200, 110));
|
||||
// Whatever the player has in their hands chooses: a gamepad, or the headset's own
|
||||
// controllers.
|
||||
boolean controller = gamepad != null
|
||||
|| (nativeController() & CONTROLLER_OWN_HELD) != 0;
|
||||
paint.setColor(controller ? Color.WHITE : Color.rgb(255, 200, 110));
|
||||
paint.setTextSize(25);
|
||||
canvas.drawText(gamepad != null ? "Left / Right on D-pad or left stick: choose"
|
||||
: "Connect your gamepad to choose and start", STATUS_WIDTH / 2.0f, 422, paint);
|
||||
canvas.drawText(controller ? "Left / Right on a stick or the D-pad: choose"
|
||||
: "Pick up the controllers, or switch a gamepad on, to choose and start",
|
||||
STATUS_WIDTH / 2.0f, 422, paint);
|
||||
|
||||
paint.setColor(headsetFov != null ? Color.rgb(45, 104, 182) : Color.rgb(48, 56, 76));
|
||||
canvas.drawRoundRect(new RectF(372, 443, 652, 495), 18, 18, paint);
|
||||
canvas.drawRoundRect(new RectF(322, 443, 702, 495), 18, 18, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextSize(28);
|
||||
paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
|
||||
canvas.drawText(headsetFov != null ? "X / A: Play" : "Detecting FOV...",
|
||||
canvas.drawText(headsetFov != null ? "X / A or trigger: Play" : "Detecting FOV...",
|
||||
STATUS_WIDTH / 2.0f, 478, paint);
|
||||
publishStatusBitmap(bitmap);
|
||||
}
|
||||
@@ -834,6 +911,36 @@ public class MainActivity extends Activity
|
||||
drawFovMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* The start-up menu, worked with the headset's own controllers: a stick to a side
|
||||
* chooses, A or a trigger starts the game. (The native side reads them; nothing of them
|
||||
* comes this way by itself.)
|
||||
*/
|
||||
private void pollOwnControllers() {
|
||||
if (!choosingFov) {
|
||||
return;
|
||||
}
|
||||
int menu = nativeTouchMenu();
|
||||
long now = SystemClock.uptimeMillis();
|
||||
int direction = (menu & MENU_LEFT) != 0 ? -1 : (menu & MENU_RIGHT) != 0 ? 1 : 0;
|
||||
if (direction == 0) {
|
||||
nextOwnFovMoveAt = 0;
|
||||
} else if (now >= nextOwnFovMoveAt) {
|
||||
nativeUseController(CONTROLLER_OWN);
|
||||
adjustFov(direction);
|
||||
nextOwnFovMoveAt = now + 250;
|
||||
}
|
||||
if ((menu & MENU_CONFIRM) == 0) {
|
||||
ownConfirmArmed = true;
|
||||
} else if (ownConfirmArmed && headsetFov != null) {
|
||||
logInfo("the menu was confirmed with the headset's own controllers");
|
||||
nativeUseController(CONTROLLER_OWN);
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
handler.postDelayed(this::pollOwnControllers, 50);
|
||||
}
|
||||
|
||||
private void drawStatus(String status) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(STATUS_WIDTH, STATUS_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
@@ -935,6 +1042,24 @@ public class MainActivity extends Activity
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (isOwnController(event.getDevice())) {
|
||||
// The headset's own controllers are read by the native side, through the
|
||||
// headset's runtime. Should the system pass their buttons this way as well, they
|
||||
// are not counted twice, nor left to what the system makes of a button nobody
|
||||
// took: it takes B for "back", and may pass the menu button on as "back" itself,
|
||||
// either of which would close the app in the middle of the game.
|
||||
int code = event.getKeyCode();
|
||||
boolean back = code == KeyEvent.KEYCODE_BACK || code == KeyEvent.KEYCODE_MENU;
|
||||
if (back && choosingFov) {
|
||||
// (Before the game, backing out of the app is what it has always done.)
|
||||
if (code == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return back || buttonFor(code) != 0 || code == KeyEvent.KEYCODE_BUTTON_MODE
|
||||
|| super.dispatchKeyEvent(event);
|
||||
}
|
||||
// Consume the entire confirming press, including repeats and its release. It must
|
||||
// not press X in the game, or move the player's seat before they have settled in.
|
||||
if (fovConfirmKey != KeyEvent.KEYCODE_UNKNOWN && event.getKeyCode() == fovConfirmKey) {
|
||||
@@ -946,6 +1071,9 @@ public class MainActivity extends Activity
|
||||
if (choosingFov) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
|
||||
noteInputFrom(event.getDevice());
|
||||
if (isGamepad(event.getDevice())) {
|
||||
nativeUseController(CONTROLLER_GAMEPAD);
|
||||
}
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
adjustFov(-1);
|
||||
@@ -980,31 +1108,24 @@ public class MainActivity extends Activity
|
||||
}
|
||||
return true;
|
||||
}
|
||||
int button = buttonFor(event.getKeyCode());
|
||||
int keyCode = event.getKeyCode();
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && isGamepad(event.getDevice())) {
|
||||
// Some gamepads have the system take their Back / View button for its own "back",
|
||||
// which would close the app in the middle of the game: it is the button that
|
||||
// presses the touchpad, as on every other gamepad.
|
||||
keyCode = KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
}
|
||||
int button = buttonFor(keyCode);
|
||||
if (button == 0) {
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
// (What the buttons mean about the view besides - the first press of X takes the
|
||||
// player's seat, OPTIONS held for a second resets the view - the native side sees
|
||||
// to, for whichever controller the game is played with.)
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
noteInputFrom(event.getDevice());
|
||||
if (button == CROSS && !seatTaken) {
|
||||
// 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.
|
||||
seatTaken = true;
|
||||
logInfo("first press of X: the player's seat is where they are now");
|
||||
nativeRecenter(RECENTER_SEAT);
|
||||
}
|
||||
if (button == OPTIONS && event.getRepeatCount() == 0) {
|
||||
// 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.
|
||||
handler.removeCallbacks(optionsHeld);
|
||||
handler.postDelayed(optionsHeld, OPTIONS_HOLD_MS);
|
||||
}
|
||||
keyButtons |= button;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
if (button == OPTIONS) {
|
||||
handler.removeCallbacks(optionsHeld);
|
||||
}
|
||||
keyButtons &= ~button;
|
||||
}
|
||||
sendInput();
|
||||
@@ -1019,8 +1140,42 @@ public class MainActivity extends Activity
|
||||
return range != null && Math.abs(value) <= range.getFlat() ? 0.0f : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a gamepad has its right stick and whether its triggers say how far they are
|
||||
* pulled: most gamepads agree on that, the others are met half way.
|
||||
*/
|
||||
private void learnAxes(InputDevice device) {
|
||||
if (device == null || device.getId() == axesDevice) {
|
||||
return;
|
||||
}
|
||||
axesDevice = device.getId();
|
||||
boolean standard = hasAxis(device, MotionEvent.AXIS_Z)
|
||||
&& hasAxis(device, MotionEvent.AXIS_RZ);
|
||||
boolean other = hasAxis(device, MotionEvent.AXIS_RX)
|
||||
&& hasAxis(device, MotionEvent.AXIS_RY);
|
||||
rightXAxis = standard || !other ? MotionEvent.AXIS_Z : MotionEvent.AXIS_RX;
|
||||
rightYAxis = standard || !other ? MotionEvent.AXIS_RZ : MotionEvent.AXIS_RY;
|
||||
analogTriggers = hasAxis(device, MotionEvent.AXIS_LTRIGGER)
|
||||
|| hasAxis(device, MotionEvent.AXIS_BRAKE)
|
||||
|| hasAxis(device, MotionEvent.AXIS_RTRIGGER)
|
||||
|| hasAxis(device, MotionEvent.AXIS_GAS);
|
||||
if (!standard || !analogTriggers) {
|
||||
logInfo("this gamepad: right stick on " + MotionEvent.axisToString(rightXAxis) + " and "
|
||||
+ MotionEvent.axisToString(rightYAxis) + ", triggers "
|
||||
+ (analogTriggers ? "by how far they are pulled" : "as buttons only"));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasAxis(InputDevice device, int axis) {
|
||||
return device.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent event) {
|
||||
if (isOwnController(event.getDevice())) {
|
||||
// Read by the native side, see dispatchKeyEvent.
|
||||
return true;
|
||||
}
|
||||
if (choosingFov) {
|
||||
if (event.isFromSource(InputDevice.SOURCE_JOYSTICK)
|
||||
&& event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
@@ -1033,6 +1188,7 @@ public class MainActivity extends Activity
|
||||
if (direction == 0) {
|
||||
nextFovMoveAt = 0;
|
||||
} else if (now >= nextFovMoveAt) {
|
||||
nativeUseController(CONTROLLER_GAMEPAD);
|
||||
adjustFov(direction);
|
||||
nextFovMoveAt = now + 250;
|
||||
}
|
||||
@@ -1041,10 +1197,11 @@ public class MainActivity extends Activity
|
||||
}
|
||||
if (event.isFromSource(InputDevice.SOURCE_JOYSTICK)
|
||||
&& event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
learnAxes(event.getDevice());
|
||||
leftX = axis(event, MotionEvent.AXIS_X);
|
||||
leftY = axis(event, MotionEvent.AXIS_Y);
|
||||
rightX = axis(event, MotionEvent.AXIS_Z);
|
||||
rightY = axis(event, MotionEvent.AXIS_RZ);
|
||||
rightX = axis(event, rightXAxis);
|
||||
rightY = axis(event, rightYAxis);
|
||||
leftTrigger = Math.max(event.getAxisValue(MotionEvent.AXIS_LTRIGGER),
|
||||
event.getAxisValue(MotionEvent.AXIS_BRAKE));
|
||||
rightTrigger = Math.max(event.getAxisValue(MotionEvent.AXIS_RTRIGGER),
|
||||
@@ -1096,56 +1253,55 @@ public class MainActivity extends Activity
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int stick(float value) {
|
||||
return Math.max(0, Math.min(255, Math.round(128.0f + value * 127.0f)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hands what the gamepad says to the native side as it is. What a gamepad lacks is made
|
||||
* up for there, the same way for every controller: the right stick as a finger on the
|
||||
* touchpad (unless stick_touchpad=0), a finger in the pad's middle for a button that
|
||||
* presses it.
|
||||
*/
|
||||
private void sendInput() {
|
||||
if (choosingFov) {
|
||||
return;
|
||||
}
|
||||
int buttons = keyButtons | hatButtons | triggerButtons | (padTouchClick ? TOUCHPAD : 0);
|
||||
|
||||
boolean touchDown = padTouchDown;
|
||||
float touchX = padTouchX, touchY = padTouchY;
|
||||
if (!touchDown && stickTouchpad) {
|
||||
// This title has no use for the right stick, while several of its gadgets want
|
||||
// swipes: the stick moves a finger that touches down at the pad's centre.
|
||||
float deflection = (float) Math.hypot(rightX, rightY);
|
||||
if (deflection > 0.25f) {
|
||||
touchDown = true;
|
||||
touchX = 0.5f + rightX * 0.45f;
|
||||
touchY = 0.5f + rightY * 0.45f;
|
||||
}
|
||||
float left = leftTrigger, right = rightTrigger;
|
||||
if (!analogTriggers) {
|
||||
// Triggers that are only buttons are pulled all the way or not at all.
|
||||
left = (keyButtons & L2) != 0 ? 1.0f : 0.0f;
|
||||
right = (keyButtons & R2) != 0 ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
nativeInput(buttons, stick(leftX), stick(leftY), stick(rightX), stick(rightY),
|
||||
Math.round(Math.max(0.0f, Math.min(1.0f, leftTrigger)) * 255.0f),
|
||||
Math.round(Math.max(0.0f, Math.min(1.0f, rightTrigger)) * 255.0f), touchDown,
|
||||
Math.max(0, Math.min(TOUCH_WIDTH - 1, Math.round(touchX * (TOUCH_WIDTH - 1)))),
|
||||
Math.max(0, Math.min(TOUCH_HEIGHT - 1, Math.round(touchY * (TOUCH_HEIGHT - 1)))));
|
||||
nativeInput(buttons, leftX, leftY, rightX, rightY, left, right, padTouchDown, padTouchX,
|
||||
padTouchY);
|
||||
}
|
||||
|
||||
// --- controller: the device, its motion sensors, rumble and light ----------------------------
|
||||
|
||||
/**
|
||||
* The headset's own controllers, which the system lists as gamepads too (and first, when
|
||||
* the app was started with one of them). They are no gamepad here: this way they come
|
||||
* without where they are, without rumble and without telling whether anybody holds them.
|
||||
* The native side reads them through the headset's runtime, which has all of that.
|
||||
*/
|
||||
private static boolean isOwnController(InputDevice device) {
|
||||
return device != null && device.getVendorId() == VENDOR_META
|
||||
&& device.supportsSource(InputDevice.SOURCE_GAMEPAD);
|
||||
}
|
||||
|
||||
private static boolean isGamepad(InputDevice device) {
|
||||
return device != null && !device.isVirtual()
|
||||
return device != null && !device.isVirtual() && !isOwnController(device)
|
||||
&& device.supportsSource(InputDevice.SOURCE_GAMEPAD)
|
||||
&& device.supportsSource(InputDevice.SOURCE_JOYSTICK);
|
||||
}
|
||||
|
||||
/**
|
||||
* How much a device looks like the controller the game is played with. The headset's own
|
||||
* controllers also show up as gamepads (and come first in the list when the app was started
|
||||
* with one of them): a controller from the console's maker goes before anything else, the
|
||||
* headset's own after everything else, and motion sensors and motors count for the rest.
|
||||
* How much a device looks like the controller the game was made for, where more than one
|
||||
* gamepad is there: a controller from the console's maker goes before anything else, and
|
||||
* motion sensors and motors count for the rest.
|
||||
*/
|
||||
private int rank(InputDevice device) {
|
||||
int rank = 0;
|
||||
if (device.getVendorId() == VENDOR_SONY) {
|
||||
rank += 8;
|
||||
} else if (device.getVendorId() == VENDOR_META) {
|
||||
rank -= 8;
|
||||
}
|
||||
try {
|
||||
if (findSensors(device) != null) {
|
||||
@@ -1204,9 +1360,20 @@ public class MainActivity extends Activity
|
||||
+ ", product " + device.getProductId() + ", sources 0x"
|
||||
+ Integer.toHexString(device.getSources()) + ")");
|
||||
|
||||
learnAxes(device);
|
||||
nativeSetGamepad(true);
|
||||
|
||||
// Every part is optional: what a controller offers depends on it and on the system.
|
||||
try {
|
||||
SensorManager sensors = motion ? findSensors(device) : null;
|
||||
if (sensors != null && device.getVendorId() != VENDOR_SONY) {
|
||||
// Which way is up for a motion sensor is each maker's own affair, and only
|
||||
// this one's is known here. A gamepad turned the wrong way round in the game
|
||||
// is worse than one the hands holding it turn.
|
||||
logInfo("this gamepad has motion sensors, which are not used: only for "
|
||||
+ "PlayStation controllers is it known which way theirs point");
|
||||
sensors = null;
|
||||
}
|
||||
if (sensors != null) {
|
||||
sensors.registerListener(this, sensors.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
|
||||
SensorManager.SENSOR_DELAY_FASTEST);
|
||||
@@ -1239,7 +1406,8 @@ public class MainActivity extends Activity
|
||||
}
|
||||
logInfo("controller offers: motion " + (controllerSensors != null) + ", rumble "
|
||||
+ (controllerVibrators != null) + ", light bar " + (lightBar != null)
|
||||
+ ", touchpad " + device.supportsSource(InputDevice.SOURCE_TOUCHPAD));
|
||||
+ ", touchpad " + device.supportsSource(InputDevice.SOURCE_TOUCHPAD)
|
||||
+ ", analog triggers " + analogTriggers);
|
||||
shownFeedback = -1;
|
||||
}
|
||||
|
||||
@@ -1298,6 +1466,11 @@ public class MainActivity extends Activity
|
||||
/** Carries out what the game last asked of the controller: rumble and light bar colour. */
|
||||
private void applyFeedback() {
|
||||
long feedback = nativePadFeedback();
|
||||
if ((nativeController() & CONTROLLER_WHICH) != CONTROLLER_GAMEPAD) {
|
||||
// The game is played with the headset's own controllers, which the native side
|
||||
// shakes: a gamepad lying about has no business rattling.
|
||||
feedback &= ~0xffffL;
|
||||
}
|
||||
long now = android.os.SystemClock.uptimeMillis();
|
||||
int small = (int) (feedback & 0xff);
|
||||
int large = (int) ((feedback >> 8) & 0xff);
|
||||
@@ -1377,7 +1550,8 @@ public class MainActivity extends Activity
|
||||
|
||||
/** The motion sensors of a controller may be listed a moment after the controller. */
|
||||
private void adoptLateSensors() {
|
||||
if (gamepad == null || controllerSensors != null || !motion) {
|
||||
if (gamepad == null || controllerSensors != null || !motion
|
||||
|| gamepad.getVendorId() != VENDOR_SONY) {
|
||||
return;
|
||||
}
|
||||
InputDevice device = getSystemService(InputManager.class).getInputDevice(gamepad.getId());
|
||||
@@ -1401,6 +1575,10 @@ public class MainActivity extends Activity
|
||||
padTouchClick = false;
|
||||
sendInput();
|
||||
findGamepad();
|
||||
if (gamepad == null) {
|
||||
// The headset's own controllers play on, if the player picks them up.
|
||||
nativeSetGamepad(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user