d8324d409e
build / build (push) Successful in 3m24s
- Free view: hold a mapped input (default: left grip) and that thumbstick drives the engine's mouse look, with the stick's normal bindings suspended while held. Also available for the right stick, unbound by default. - Cyberspace actions: eleven thrust/climb/dive/bank/roll actions with their own bindings, active only in cyberspace. The normal movement actions are inactive there, and capture exclusivity only compares actions that can be active together, so one input can be bound in both sets. - Cyberspace pitch and bank scale with thumbstick deflection instead of running at full strength. - Remove the toggle_freelook action; bind the automap view to the right stick click by default. - Engine patches: 24 exposes whether the player is in cyberspace, 25 lets the controller code scale cyberspace pitch and bank.
2490 lines
118 KiB
C
2490 lines
118 KiB
C
#include "xr_input.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include <android/log.h>
|
|
|
|
#include <GLES3/gl3.h>
|
|
#include <SDL.h>
|
|
|
|
#include "qs_config.h"
|
|
#include "xr_mouse.h"
|
|
#include "xr_overlay.h"
|
|
#include "xr_session.h"
|
|
#include "xr_swapchain.h"
|
|
|
|
#define TAG "QuestShock"
|
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
|
|
|
#define LEFT 0
|
|
#define RIGHT 1
|
|
|
|
// Same constant (deliberately not <math.h>'s M_PI - see xr_overlay.c's own
|
|
// copy of this comment) as xr_session.c's play-space rebuild and
|
|
// xr_overlay.c's quad-rotation math - this file's ray/oriented-quad
|
|
// intersection and thumbstick-turn math need the same conversion.
|
|
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
|
|
|
static const char *kHandName[2] = {"left", "right"};
|
|
static const float kHandColor[2][4] = {
|
|
{0.2f, 1.0f, 1.0f, 1.0f}, // left: cyan
|
|
{1.0f, 0.85f, 0.1f, 1.0f}, // right: amber
|
|
};
|
|
|
|
// Both overlays' title bars (drag handle, plus the keyboard's Close button)
|
|
// are handled entirely here, before a hit would normally be dispatched to
|
|
// the Java side as a touch. A hit in the top *_TITLE_BAR_V_FRACTION of a
|
|
// quad, at or left of its own drag-handle-U-fraction, is the drag handle;
|
|
// right of that (still within the title bar) is dispatched as an ordinary
|
|
// click instead - the keyboard's Close button, or (the menu has no such
|
|
// button - MENU_DRAG_HANDLE_U_FRACTION is 1.0f, so its whole title bar
|
|
// width is the drag handle) nothing.
|
|
//
|
|
// KEYBOARD_TITLE_BAR_V_FRACTION/KEYBOARD_CLOSE_BUTTON_U_FRACTION must agree
|
|
// with KeyboardOverlay.java's TITLE_BAR_HEIGHT/CLOSE_BUTTON_WIDTH constants
|
|
// (in pixels, out of KeyboardOverlay.WIDTH/HEIGHT there);
|
|
// MENU_TITLE_BAR_V_FRACTION with MenuOverlay.java's HEADER_HEIGHT (out of
|
|
// MenuOverlay.HEIGHT).
|
|
#define KEYBOARD_TITLE_BAR_V_FRACTION (90.0f / 768.0f)
|
|
#define KEYBOARD_CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.0f)
|
|
#define MENU_TITLE_BAR_V_FRACTION (56.0f / 800.0f)
|
|
#define MENU_DRAG_HANDLE_U_FRACTION 1.0f
|
|
|
|
// Every physical control a supported controller profile can bind, named
|
|
// independently of OpenXR objects. The first QS_RAW_CONCRETE_COUNT entries
|
|
// are real per-frame-readable inputs (g_raw_down[]/g_raw_prev[] cover exactly
|
|
// that range). QS_RAW_TRIGGER_SYMMETRIC/QS_RAW_SQUEEZE_SYMMETRIC mean "this
|
|
// hand's own trigger/squeeze" (see xr_input_raw_state_for_hand()) and are only
|
|
// valid for mouse_click/mouse_double_click; QS_RAW_NONE is unbound.
|
|
typedef enum {
|
|
QS_RAW_LEFT_TRIGGER,
|
|
QS_RAW_RIGHT_TRIGGER,
|
|
QS_RAW_LEFT_SQUEEZE,
|
|
QS_RAW_RIGHT_SQUEEZE,
|
|
QS_RAW_LEFT_X,
|
|
QS_RAW_LEFT_Y,
|
|
QS_RAW_RIGHT_A,
|
|
QS_RAW_RIGHT_B,
|
|
QS_RAW_LEFT_THUMBSTICK_CLICK,
|
|
QS_RAW_RIGHT_THUMBSTICK_CLICK,
|
|
QS_RAW_LEFT_THUMBSTICK_UP,
|
|
QS_RAW_LEFT_THUMBSTICK_DOWN,
|
|
QS_RAW_LEFT_THUMBSTICK_LEFT,
|
|
QS_RAW_LEFT_THUMBSTICK_RIGHT,
|
|
QS_RAW_RIGHT_THUMBSTICK_UP,
|
|
QS_RAW_RIGHT_THUMBSTICK_DOWN,
|
|
QS_RAW_RIGHT_THUMBSTICK_LEFT,
|
|
QS_RAW_RIGHT_THUMBSTICK_RIGHT,
|
|
QS_RAW_CONCRETE_COUNT,
|
|
QS_RAW_TRIGGER_SYMMETRIC = QS_RAW_CONCRETE_COUNT,
|
|
QS_RAW_SQUEEZE_SYMMETRIC,
|
|
QS_RAW_NONE,
|
|
QS_RAW_COUNT
|
|
} QsRawInput;
|
|
|
|
// Index-aligned with QsRawInput: the strings used in questshock.config.json
|
|
// and reported by capture (which only ever observes the concrete inputs).
|
|
static const char *kRawInputNames[QS_RAW_COUNT] = {
|
|
"left_trigger", "right_trigger", "left_squeeze", "right_squeeze", "left_x", "left_y",
|
|
"right_a", "right_b", "left_thumbstick_click", "right_thumbstick_click",
|
|
"left_thumbstick_up", "left_thumbstick_down", "left_thumbstick_left", "left_thumbstick_right",
|
|
"right_thumbstick_up", "right_thumbstick_down", "right_thumbstick_left",
|
|
"right_thumbstick_right", "trigger", "squeeze", "none",
|
|
};
|
|
|
|
// Remappable game actions: movement, click and jump, free view, a separate set
|
|
// of cyberspace movement actions, the engine's own keyboard hotkeys (Prefs.c's
|
|
// HotKeyLookup[]), and fire. The hotkey actions default to QS_RAW_NONE
|
|
// (unbound). menu_toggle is not an action; it is fixed
|
|
// (see xr_input_sync_and_draw()).
|
|
//
|
|
// HotKeyLookup[] entries not exposed:
|
|
// - cheat_*: debug cheats.
|
|
// - mac_help (disabled) and audiolog_cancel (AUDIOLOGS is not defined).
|
|
// - toggle_opengl: the same toggle as the Video Options renderer buttons.
|
|
// - the 10 numeric hw_hotkey_callback entries: they are registered as bare
|
|
// ASCII literals without KB_FLAG_DOWN, but kb_cook() always ORs that flag
|
|
// into a real keydown, so a keypress never matches them.
|
|
// - keypad 0-9: a controller button per digit is not useful; the on-screen
|
|
// keyboard covers keypad entry.
|
|
typedef enum {
|
|
QS_ACTION_MOVE_FORWARD,
|
|
QS_ACTION_MOVE_BACK,
|
|
QS_ACTION_STRAFE_LEFT,
|
|
QS_ACTION_STRAFE_RIGHT,
|
|
QS_ACTION_TURN_LEFT,
|
|
QS_ACTION_TURN_RIGHT,
|
|
QS_ACTION_JUMP,
|
|
QS_ACTION_MOUSE_CLICK,
|
|
QS_ACTION_MOUSE_DOUBLE_CLICK,
|
|
QS_ACTION_STAND,
|
|
QS_ACTION_CROUCH,
|
|
QS_ACTION_PRONE,
|
|
QS_ACTION_PAUSE,
|
|
QS_ACTION_FULL_VIEW,
|
|
QS_ACTION_NORMAL_VIEW,
|
|
QS_ACTION_MAP_VIEW,
|
|
QS_ACTION_SAVE_GAME,
|
|
QS_ACTION_LOAD_GAME,
|
|
QS_ACTION_SELECT_GRENADE,
|
|
QS_ACTION_ARM_GRENADE,
|
|
QS_ACTION_SELECT_DRUG,
|
|
QS_ACTION_USE_DRUG,
|
|
QS_ACTION_TOGGLE_ONLINE_HELP,
|
|
QS_ACTION_SHOW_HELP,
|
|
QS_ACTION_TOGGLE_MUSIC,
|
|
QS_ACTION_QUIT,
|
|
QS_ACTION_CYCLE_WEAPON_NEXT,
|
|
QS_ACTION_CYCLE_WEAPON_PREV,
|
|
QS_ACTION_CYCLE_DETAIL,
|
|
QS_ACTION_RELOAD_WEAPON_PRIMARY,
|
|
QS_ACTION_RELOAD_WEAPON_SECONDARY,
|
|
QS_ACTION_HUD_COLOR,
|
|
QS_ACTION_CLEAR_FULLSCREEN,
|
|
QS_ACTION_MFD_LEFT_1,
|
|
QS_ACTION_MFD_LEFT_2,
|
|
QS_ACTION_MFD_LEFT_3,
|
|
QS_ACTION_MFD_LEFT_4,
|
|
QS_ACTION_MFD_LEFT_5,
|
|
QS_ACTION_MFD_RIGHT_1,
|
|
QS_ACTION_MFD_RIGHT_2,
|
|
QS_ACTION_MFD_RIGHT_3,
|
|
QS_ACTION_MFD_RIGHT_4,
|
|
QS_ACTION_MFD_RIGHT_5,
|
|
QS_ACTION_FIRE_WEAPON,
|
|
QS_ACTION_FREE_LEFT_THUMBSTICK_VIEW,
|
|
QS_ACTION_FREE_RIGHT_THUMBSTICK_VIEW,
|
|
QS_ACTION_CYBER_THRUST,
|
|
QS_ACTION_CYBER_CLIMB,
|
|
QS_ACTION_CYBER_DIVE,
|
|
QS_ACTION_CYBER_BANK_LEFT,
|
|
QS_ACTION_CYBER_BANK_RIGHT,
|
|
QS_ACTION_CYBER_ROLL_LEFT,
|
|
QS_ACTION_CYBER_ROLL_RIGHT,
|
|
QS_ACTION_CYBER_CLIMB_LEFT,
|
|
QS_ACTION_CYBER_CLIMB_RIGHT,
|
|
QS_ACTION_CYBER_DIVE_LEFT,
|
|
QS_ACTION_CYBER_DIVE_RIGHT,
|
|
QS_ACTION_COUNT
|
|
} QsControlAction;
|
|
|
|
static const char *kActionNames[QS_ACTION_COUNT] = {
|
|
"move_forward",
|
|
"move_back",
|
|
"strafe_left",
|
|
"strafe_right",
|
|
"turn_left",
|
|
"turn_right",
|
|
"jump",
|
|
"mouse_click",
|
|
"mouse_double_click",
|
|
"stand",
|
|
"crouch",
|
|
"prone",
|
|
"pause",
|
|
"full_view",
|
|
"normal_view",
|
|
"map_view",
|
|
"save_game",
|
|
"load_game",
|
|
"select_grenade",
|
|
"arm_grenade",
|
|
"select_drug",
|
|
"use_drug",
|
|
"toggle_online_help",
|
|
"show_help",
|
|
"toggle_music",
|
|
"quit",
|
|
"cycle_weapon_next",
|
|
"cycle_weapon_prev",
|
|
"cycle_detail",
|
|
"reload_weapon_primary",
|
|
"reload_weapon_secondary",
|
|
"hud_color",
|
|
"clear_fullscreen",
|
|
"mfd_left_1",
|
|
"mfd_left_2",
|
|
"mfd_left_3",
|
|
"mfd_left_4",
|
|
"mfd_left_5",
|
|
"mfd_right_1",
|
|
"mfd_right_2",
|
|
"mfd_right_3",
|
|
"mfd_right_4",
|
|
"mfd_right_5",
|
|
"fire_weapon",
|
|
"free_left_thumbstick_view",
|
|
"free_right_thumbstick_view",
|
|
"cyber_thrust",
|
|
"cyber_climb",
|
|
"cyber_dive",
|
|
"cyber_bank_left",
|
|
"cyber_bank_right",
|
|
"cyber_roll_left",
|
|
"cyber_roll_right",
|
|
"cyber_climb_left",
|
|
"cyber_climb_right",
|
|
"cyber_dive_left",
|
|
"cyber_dive_right",
|
|
};
|
|
|
|
// Current bindings; xr_input.c owns them (qs_config.c applies parsed config via
|
|
// xr_input_set_action_binding() and reads them back for Save). Defaults: left
|
|
// stick moves (up/down) and strafes (left/right), its click jumps; right stick
|
|
// turns (left/right) and stands/goes prone (up/down), its click opens the
|
|
// automap; right A/B click/double-click; right trigger fires; left grip holds
|
|
// free view on the left stick. In cyberspace: left stick climbs/dives and banks,
|
|
// right stick thrusts (up) and rolls. Everything else is QS_RAW_NONE.
|
|
static QsRawInput s_binding[QS_ACTION_COUNT] = {
|
|
QS_RAW_LEFT_THUMBSTICK_UP, // move_forward
|
|
QS_RAW_LEFT_THUMBSTICK_DOWN, // move_back
|
|
QS_RAW_LEFT_THUMBSTICK_LEFT, // strafe_left
|
|
QS_RAW_LEFT_THUMBSTICK_RIGHT, // strafe_right
|
|
QS_RAW_RIGHT_THUMBSTICK_LEFT, // turn_left
|
|
QS_RAW_RIGHT_THUMBSTICK_RIGHT, // turn_right
|
|
QS_RAW_LEFT_THUMBSTICK_CLICK, // jump
|
|
QS_RAW_RIGHT_A, // mouse_click
|
|
QS_RAW_RIGHT_B, // mouse_double_click
|
|
QS_RAW_RIGHT_THUMBSTICK_UP, // stand
|
|
QS_RAW_NONE, // crouch
|
|
QS_RAW_RIGHT_THUMBSTICK_DOWN, // prone
|
|
QS_RAW_NONE, // pause
|
|
QS_RAW_NONE, // full_view
|
|
QS_RAW_NONE, // normal_view
|
|
QS_RAW_RIGHT_THUMBSTICK_CLICK, // map_view
|
|
QS_RAW_NONE, // save_game
|
|
QS_RAW_NONE, // load_game
|
|
QS_RAW_NONE, // select_grenade
|
|
QS_RAW_NONE, // arm_grenade
|
|
QS_RAW_NONE, // select_drug
|
|
QS_RAW_NONE, // use_drug
|
|
QS_RAW_NONE, // toggle_online_help
|
|
QS_RAW_NONE, // show_help
|
|
QS_RAW_NONE, // toggle_music
|
|
QS_RAW_NONE, // quit
|
|
QS_RAW_NONE, // cycle_weapon_next
|
|
QS_RAW_NONE, // cycle_weapon_prev
|
|
QS_RAW_NONE, // cycle_detail
|
|
QS_RAW_NONE, // reload_weapon_primary
|
|
QS_RAW_NONE, // reload_weapon_secondary
|
|
QS_RAW_NONE, // hud_color
|
|
QS_RAW_NONE, // clear_fullscreen
|
|
QS_RAW_NONE, // mfd_left_1
|
|
QS_RAW_NONE, // mfd_left_2
|
|
QS_RAW_NONE, // mfd_left_3
|
|
QS_RAW_NONE, // mfd_left_4
|
|
QS_RAW_NONE, // mfd_left_5
|
|
QS_RAW_NONE, // mfd_right_1
|
|
QS_RAW_NONE, // mfd_right_2
|
|
QS_RAW_NONE, // mfd_right_3
|
|
QS_RAW_NONE, // mfd_right_4
|
|
QS_RAW_NONE, // mfd_right_5
|
|
QS_RAW_RIGHT_TRIGGER, // fire_weapon
|
|
QS_RAW_LEFT_SQUEEZE, // free_left_thumbstick_view
|
|
QS_RAW_NONE, // free_right_thumbstick_view
|
|
QS_RAW_RIGHT_THUMBSTICK_UP, // cyber_thrust
|
|
QS_RAW_LEFT_THUMBSTICK_UP, // cyber_climb
|
|
QS_RAW_LEFT_THUMBSTICK_DOWN, // cyber_dive
|
|
QS_RAW_LEFT_THUMBSTICK_LEFT, // cyber_bank_left
|
|
QS_RAW_LEFT_THUMBSTICK_RIGHT, // cyber_bank_right
|
|
QS_RAW_RIGHT_THUMBSTICK_LEFT, // cyber_roll_left
|
|
QS_RAW_RIGHT_THUMBSTICK_RIGHT, // cyber_roll_right
|
|
QS_RAW_NONE, // cyber_climb_left
|
|
QS_RAW_NONE, // cyber_climb_right
|
|
QS_RAW_NONE, // cyber_dive_left
|
|
QS_RAW_NONE, // cyber_dive_right
|
|
};
|
|
|
|
// One entry per key-injecting action. A plain, unmodified, printable key
|
|
// (needsTextInput, textChar) is sent as SDL_TEXTINPUT plus SDL_KEYDOWN/UP:
|
|
// sdl_events.c only registers such a key from its TEXTINPUT event. Every other
|
|
// entry (modifier held, or a control-range/F-key) is plain SDL_KEYDOWN/UP, with
|
|
// mod carrying the KMOD_ALT/CTRL/SHIFT the hotkey needs (see Prefs.c's
|
|
// HotKeyLookup[] DOWN()/SHIFT()/CTRL()/ALT() macros).
|
|
typedef struct {
|
|
QsControlAction action;
|
|
SDL_Scancode scancode;
|
|
SDL_Keycode keycode;
|
|
Uint16 mod;
|
|
bool needsTextInput;
|
|
char textChar; // only meaningful when needsTextInput is true
|
|
} QsKeyAction;
|
|
|
|
static const QsKeyAction kKeyActions[] = {
|
|
{QS_ACTION_MOVE_FORWARD, SDL_SCANCODE_UP, SDLK_UP, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MOVE_BACK, SDL_SCANCODE_DOWN, SDLK_DOWN, KMOD_NONE, false, 0},
|
|
{QS_ACTION_STRAFE_LEFT, SDL_SCANCODE_LEFT, SDLK_LEFT, KMOD_ALT, false, 0},
|
|
{QS_ACTION_STRAFE_RIGHT, SDL_SCANCODE_RIGHT, SDLK_RIGHT, KMOD_ALT, false, 0},
|
|
{QS_ACTION_TURN_LEFT, SDL_SCANCODE_LEFT, SDLK_LEFT, KMOD_NONE, false, 0},
|
|
{QS_ACTION_TURN_RIGHT, SDL_SCANCODE_RIGHT, SDLK_RIGHT, KMOD_NONE, false, 0},
|
|
{QS_ACTION_JUMP, SDL_SCANCODE_SPACE, SDLK_SPACE, KMOD_NONE, true, ' '},
|
|
// "stand"/"crouch"/"prone"/"pause" - DOWN('t'/'g'/'b'/'p').
|
|
{QS_ACTION_STAND, SDL_SCANCODE_T, SDLK_t, KMOD_NONE, true, 't'},
|
|
{QS_ACTION_CROUCH, SDL_SCANCODE_G, SDLK_g, KMOD_NONE, true, 'g'},
|
|
{QS_ACTION_PRONE, SDL_SCANCODE_B, SDLK_b, KMOD_NONE, true, 'b'},
|
|
{QS_ACTION_PAUSE, SDL_SCANCODE_P, SDLK_p, KMOD_NONE, true, 'p'},
|
|
// "full_view"/"normal_view"/"map_view" - CTRL('f'/'d'/'a') - jump
|
|
// straight to the fullscreen/game/automap loop mode.
|
|
{QS_ACTION_FULL_VIEW, SDL_SCANCODE_F, SDLK_f, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_NORMAL_VIEW, SDL_SCANCODE_D, SDLK_d, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_MAP_VIEW, SDL_SCANCODE_A, SDLK_a, KMOD_CTRL, false, 0},
|
|
// "save_game"/"load_game" - CTRL('s'/'l').
|
|
{QS_ACTION_SAVE_GAME, SDL_SCANCODE_S, SDLK_s, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_LOAD_GAME, SDL_SCANCODE_L, SDLK_l, KMOD_CTRL, false, 0},
|
|
// "select_grenade"/"select_drug" - CTRL('\'' / ';').
|
|
{QS_ACTION_SELECT_GRENADE, SDL_SCANCODE_APOSTROPHE, SDLK_QUOTE, KMOD_CTRL, false, 0},
|
|
// "arm_grenade"/"use_drug" - ALT('\'' / ';').
|
|
{QS_ACTION_ARM_GRENADE, SDL_SCANCODE_APOSTROPHE, SDLK_QUOTE, KMOD_ALT, false, 0},
|
|
{QS_ACTION_SELECT_DRUG, SDL_SCANCODE_SEMICOLON, SDLK_SEMICOLON, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_USE_DRUG, SDL_SCANCODE_SEMICOLON, SDLK_SEMICOLON, KMOD_ALT, false, 0},
|
|
// "toggle_olh"/"showhelp" - CTRL('h') / ALT('o').
|
|
{QS_ACTION_TOGGLE_ONLINE_HELP, SDL_SCANCODE_H, SDLK_h, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_SHOW_HELP, SDL_SCANCODE_O, SDLK_o, KMOD_ALT, false, 0},
|
|
// "toggle_music"/"quit" - CTRL('m'/'q').
|
|
{QS_ACTION_TOGGLE_MUSIC, SDL_SCANCODE_M, SDLK_m, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_QUIT, SDL_SCANCODE_Q, SDLK_q, KMOD_CTRL, false, 0},
|
|
// "cycle_weapons 1"/"cycle_weapons -1" - Tab / Shift+Tab.
|
|
{QS_ACTION_CYCLE_WEAPON_NEXT, SDL_SCANCODE_TAB, SDLK_TAB, KMOD_NONE, false, 0},
|
|
{QS_ACTION_CYCLE_WEAPON_PREV, SDL_SCANCODE_TAB, SDLK_TAB, KMOD_SHIFT, false, 0},
|
|
// "cycle_detail" - CTRL('1').
|
|
{QS_ACTION_CYCLE_DETAIL, SDL_SCANCODE_1, SDLK_1, KMOD_CTRL, false, 0},
|
|
// "reload_weapon 1"/"reload_weapon 0" - CTRL(Backspace) / ALT(Backspace).
|
|
{QS_ACTION_RELOAD_WEAPON_PRIMARY, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE, KMOD_CTRL, false, 0},
|
|
{QS_ACTION_RELOAD_WEAPON_SECONDARY, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE, KMOD_ALT, false, 0},
|
|
// "hud_color" - ALT('h').
|
|
{QS_ACTION_HUD_COLOR, SDL_SCANCODE_H, SDLK_h, KMOD_ALT, false, 0},
|
|
// "clear_fullscreen" - DOWN(Backspace), no modifier.
|
|
{QS_ACTION_CLEAR_FULLSCREEN, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE, KMOD_NONE, false, 0},
|
|
// "mfd left/right N" - F1-F5 / F6-F10.
|
|
{QS_ACTION_MFD_LEFT_1, SDL_SCANCODE_F1, SDLK_F1, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_LEFT_2, SDL_SCANCODE_F2, SDLK_F2, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_LEFT_3, SDL_SCANCODE_F3, SDLK_F3, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_LEFT_4, SDL_SCANCODE_F4, SDLK_F4, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_LEFT_5, SDL_SCANCODE_F5, SDLK_F5, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_RIGHT_1, SDL_SCANCODE_F6, SDLK_F6, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_RIGHT_2, SDL_SCANCODE_F7, SDLK_F7, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_RIGHT_3, SDL_SCANCODE_F8, SDLK_F8, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_RIGHT_4, SDL_SCANCODE_F9, SDLK_F9, KMOD_NONE, false, 0},
|
|
{QS_ACTION_MFD_RIGHT_5, SDL_SCANCODE_F10, SDLK_F10, KMOD_NONE, false, 0},
|
|
// "fire_weapon" - the engine's FireKeys default (Prefs.c; input.c's
|
|
// view3d_key_handler()): Enter fires the readied weapon at the cursor and
|
|
// re-arms on release. Clicking in the 3D view (mouse_click) also fires.
|
|
{QS_ACTION_FIRE_WEAPON, SDL_SCANCODE_RETURN, SDLK_RETURN, KMOD_NONE, false, 0},
|
|
// Cyberspace movement (Prefs.c's MoveCyberKeybinds defaults): S thrust, W climb, X dive,
|
|
// A/D bank, E/Q roll, and the keypad 7/9/1/3 diagonal combinations.
|
|
{QS_ACTION_CYBER_THRUST, SDL_SCANCODE_S, SDLK_s, KMOD_NONE, true, 's'},
|
|
{QS_ACTION_CYBER_CLIMB, SDL_SCANCODE_W, SDLK_w, KMOD_NONE, true, 'w'},
|
|
{QS_ACTION_CYBER_DIVE, SDL_SCANCODE_X, SDLK_x, KMOD_NONE, true, 'x'},
|
|
{QS_ACTION_CYBER_BANK_LEFT, SDL_SCANCODE_A, SDLK_a, KMOD_NONE, true, 'a'},
|
|
{QS_ACTION_CYBER_BANK_RIGHT, SDL_SCANCODE_D, SDLK_d, KMOD_NONE, true, 'd'},
|
|
{QS_ACTION_CYBER_ROLL_LEFT, SDL_SCANCODE_E, SDLK_e, KMOD_NONE, true, 'e'},
|
|
{QS_ACTION_CYBER_ROLL_RIGHT, SDL_SCANCODE_Q, SDLK_q, KMOD_NONE, true, 'q'},
|
|
{QS_ACTION_CYBER_CLIMB_LEFT, SDL_SCANCODE_KP_7, SDLK_KP_7, KMOD_NONE, false, 0},
|
|
{QS_ACTION_CYBER_CLIMB_RIGHT, SDL_SCANCODE_KP_9, SDLK_KP_9, KMOD_NONE, false, 0},
|
|
{QS_ACTION_CYBER_DIVE_LEFT, SDL_SCANCODE_KP_1, SDLK_KP_1, KMOD_NONE, false, 0},
|
|
{QS_ACTION_CYBER_DIVE_RIGHT, SDL_SCANCODE_KP_3, SDLK_KP_3, KMOD_NONE, false, 0},
|
|
};
|
|
#define QS_NUM_KEY_ACTIONS (sizeof(kKeyActions) / sizeof(kKeyActions[0]))
|
|
|
|
static XrInstance g_instance = XR_NULL_HANDLE;
|
|
static XrSession g_session = XR_NULL_HANDLE;
|
|
static XrActionSet g_action_set = XR_NULL_HANDLE;
|
|
static XrAction g_aim_pose_action = XR_NULL_HANDLE;
|
|
// Fixed for menu/keyboard-overlay clicks (see xr_input_sync_and_draw()); also
|
|
// backs QS_RAW_{LEFT,RIGHT}_TRIGGER's per-frame state.
|
|
static XrAction g_select_click_action = XR_NULL_HANDLE;
|
|
static XrAction g_menu_toggle_action = XR_NULL_HANDLE;
|
|
// Right controller's A button / left's X (Vive has neither); see
|
|
// QS_RAW_RIGHT_A/QS_RAW_LEFT_X.
|
|
static XrAction g_a_click_action = XR_NULL_HANDLE;
|
|
static XrAction g_b_click_action = XR_NULL_HANDLE;
|
|
static XrAction g_x_click_action = XR_NULL_HANDLE;
|
|
static XrAction g_y_click_action = XR_NULL_HANDLE;
|
|
static XrAction g_squeeze_action = XR_NULL_HANDLE;
|
|
static XrAction g_thumbstick_click_action = XR_NULL_HANDLE;
|
|
// Player locomotion while the menu is open (see xr_session.h's
|
|
// xr_session_set_player_transform()) - left stick snap-turns, right stick
|
|
// smooth-moves, both read from the same Vector2f action via subactionPath.
|
|
// Also the backing action for QS_RAW_*_THUMBSTICK_{UP,DOWN,LEFT,RIGHT}'s
|
|
// digitization (see g_stick_digital below) - bound to the Vive profile's
|
|
// trackpad component instead of a thumbstick, since it's the same "2D
|
|
// directional input" shape either way.
|
|
static XrAction g_thumbstick_action = XR_NULL_HANDLE;
|
|
static XrPath g_hand_path[2];
|
|
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
|
|
|
// Edge-detection state, so touch dispatch and logging only fire on actual
|
|
// state changes, not every frame. Each interaction target (game quad, menu
|
|
// launcher, keyboard) tracks its own "was this hand's ray hitting it last
|
|
// frame" independently, since the menu and keyboard overlays can now both
|
|
// be visible at once.
|
|
static bool g_prev_select[2] = {false, false};
|
|
static bool g_prev_double_click[2] = {false, false};
|
|
static bool g_prev_menu = false;
|
|
// One prior-down flag per kKeyActions[] entry.
|
|
static bool g_key_action_prev[QS_NUM_KEY_ACTIONS];
|
|
|
|
// Per-frame state for every raw input QS_RAW_CONCRETE_COUNT covers -
|
|
// computed once per frame (xr_input_update_raw_states(), unconditionally,
|
|
// regardless of menuVisible) and consumed by capture mode, the key-action
|
|
// dispatch loop, and mouse_click/mouse_double_click's resolution. Indices
|
|
// QS_RAW_TRIGGER_SYMMETRIC/QS_RAW_SQUEEZE_SYMMETRIC/QS_RAW_NONE are never
|
|
// written here - see xr_input_raw_state_for_hand() for how those resolve
|
|
// instead.
|
|
static bool g_raw_down[QS_RAW_COUNT];
|
|
static bool g_raw_prev[QS_RAW_COUNT];
|
|
|
|
// Per-hand thumbstick digitization hysteresis (see xr_input_digitize_stick()),
|
|
// independent per direction so a diagonal deflection can hold two directions;
|
|
// separate from g_turn_ready below, which belongs to the menu's snap turn.
|
|
typedef struct {
|
|
bool up, down, left, right;
|
|
} XrStickDigital;
|
|
static XrStickDigital g_stick_digital[2];
|
|
// Raw analog stick/trackpad position per hand, for the free-view actions.
|
|
static float g_stick_xy[2][2];
|
|
|
|
// Rebind capture state (see xr_input_capture_begin()/qs_config.c's own
|
|
// comment on the cross-thread request that calls it) - g_capture_action is
|
|
// only meaningful while g_capture_active is true.
|
|
static bool g_capture_active = false;
|
|
// Last menuVisible value reported to qs_config_notify_menu_visibility() -
|
|
// that's how a change (however the menu got shown/hidden) is noticed.
|
|
static bool g_menu_was_visible = false;
|
|
// Set from MenuOverlay.java's UI thread (a single-byte flag - no need for
|
|
// the cross-thread event dispatch the multi-step requests use) whenever the
|
|
// Controller Options tab becomes/stops being the menu's current tab. While
|
|
// it's true, the menu's own thumbstick locomotion (snap turn/smooth move,
|
|
// see xr_input_sync_and_draw()) is disabled: pressing sticks there to test
|
|
// or rebind a control shouldn't also shift the player away from the
|
|
// virtual screen.
|
|
static volatile bool g_controller_options_tab_open = false;
|
|
static QsControlAction g_capture_action;
|
|
static Uint32 g_capture_start_ticks = 0;
|
|
#define QS_CAPTURE_TIMEOUT_MS 8000
|
|
|
|
// Refreshed once per frame (see xr_input_sync_and_draw()) via
|
|
// xrGetCurrentInteractionProfile()/xrPathToString() - which controller
|
|
// profile (Touch/Vive/generic) is actually bound right now, read by
|
|
// MenuOverlay.java's Controller Options tab to pick a matching profile
|
|
// file from controller_profiles/ (see CONFIG.md). Empty string until the
|
|
// first successful query.
|
|
static char g_active_profile_path[256] = "";
|
|
// Snap-turn hysteresis - true when the left stick has returned near center
|
|
// since the last turn it triggered, i.e. it's "armed" to trigger another
|
|
// one; false from the moment a turn triggers until the stick re-centers,
|
|
// so holding the stick deflected turns exactly once, not once per frame.
|
|
static bool g_turn_ready = true;
|
|
// Self-contained delta-time tracker for smooth movement - deliberately not
|
|
// shared with xr_session.c's own SDL_GetTicks()-based FPS smoothing, which
|
|
// is a private implementation detail of that file's stats display.
|
|
static Uint32 g_last_move_ticks = 0;
|
|
// True until either xr_input_recenter_player_to_screen() has succeeded
|
|
// once this session, or xr_input_skip_startup_recenter() has been called -
|
|
// checked once per frame in xr_input_sync_and_draw() (see its own comment
|
|
// there) as a *fallback* default player position/orientation for when
|
|
// there's no complete saved one to fall back on instead (see
|
|
// qs_config_load_startup()'s own comment on when it calls
|
|
// xr_input_skip_startup_recenter()). While still pending, retries every
|
|
// frame until a valid head pose is actually available - typically the
|
|
// very first frame, but tracking isn't guaranteed to be valid that early.
|
|
static bool g_pending_startup_recenter = true;
|
|
static bool g_game_prev_hit[2] = {false, false};
|
|
static bool g_game_touch_active[2] = {false, false};
|
|
static bool g_menu_prev_hit[2] = {false, false};
|
|
static bool g_menu_touch_active[2] = {false, false};
|
|
static bool g_keyboard_prev_hit[2] = {false, false};
|
|
static bool g_keyboard_touch_active[2] = {false, false};
|
|
|
|
// Drag state for repositioning an overlay via its title bar (see the
|
|
// *_TITLE_BAR_V_FRACTION/*_DRAG_HANDLE_U_FRACTION or
|
|
// *_CLOSE_BUTTON_U_FRACTION defines above). hand is -1 when dragging is
|
|
// false. offset_x/offset_y is the vector from the quad's center to the
|
|
// grabbed point at drag-start, kept constant for the whole gesture so the
|
|
// quad follows the hand rigidly rather than re-centering under the ray
|
|
// each frame. Each draggable overlay (keyboard, menu) gets its own
|
|
// instance so they can be dragged independently/simultaneously by
|
|
// different hands.
|
|
typedef struct {
|
|
bool dragging;
|
|
int hand;
|
|
float offset_x;
|
|
float offset_y;
|
|
} XrDragState;
|
|
|
|
static XrDragState g_keyboard_drag = {false, -1, 0.0f, 0.0f};
|
|
static XrDragState g_menu_drag = {false, -1, 0.0f, 0.0f};
|
|
|
|
// A "view" reference space, located once per frame (not per hand) to get
|
|
// an approximate head position for the laser-beam billboard math below -
|
|
// the only consumer of a head pose in this file. Both LOCAL (g_local_space,
|
|
// owned by xr_session.c) and VIEW reference spaces are mandated by core
|
|
// OpenXR, so no capability check is needed to create this.
|
|
static XrSpace g_view_space = XR_NULL_HANDLE;
|
|
|
|
// Per-hand laser-beam state: a thin, billboarded quad spanning from the
|
|
// controller to wherever that hand's ray currently crosses the plane of
|
|
// whichever target (keyboard, menu, or the game quad) claimed it this
|
|
// frame - see xr_input_build_beam()/xr_input_get_beam_layer(). Each hand
|
|
// gets its own tiny solid-color swapchain (no shared tint on XrCompositionLayerQuad,
|
|
// so two separately-colored textures is simplest). g_beam_quad_valid is
|
|
// reset to false at the top of every xr_input_sync_and_draw() call and
|
|
// only set back to true if that hand actually claims a beam this frame.
|
|
#define BEAM_TEX_SIZE 2
|
|
#define BEAM_THICKNESS_METERS 0.004f
|
|
#define MAX_BEAM_LENGTH_METERS 2.0f
|
|
|
|
static XrSwapchainState g_beam_swapchain[2];
|
|
static XrCompositionLayerQuad g_beam_quad[2];
|
|
static bool g_beam_quad_valid[2] = {false, false};
|
|
|
|
static bool xr_check(XrResult result, const char *what) {
|
|
if (XR_SUCCEEDED(result))
|
|
return true;
|
|
char resultString[XR_MAX_RESULT_STRING_SIZE] = {0};
|
|
if (g_instance != XR_NULL_HANDLE)
|
|
xrResultToString(g_instance, result, resultString);
|
|
LOGE("XR: %s failed: %s (%d)", what, resultString[0] ? resultString : "?", (int)result);
|
|
return false;
|
|
}
|
|
|
|
// Standard quaternion-vector rotation (v + 2*cross(q.xyz, cross(q.xyz, v) +
|
|
// q.w*v)) - used to turn the aim pose's orientation into its local -Z
|
|
// ("forward") axis, the ray direction per the OpenXR aim-pose convention.
|
|
static void quat_rotate_vec(const XrQuaternionf *q, float vx, float vy, float vz, float *outx,
|
|
float *outy, float *outz) {
|
|
float cx = q->y * vz - q->z * vy + q->w * vx;
|
|
float cy = q->z * vx - q->x * vz + q->w * vy;
|
|
float cz = q->x * vy - q->y * vx + q->w * vz;
|
|
*outx = vx + 2.0f * (q->y * cz - q->z * cy);
|
|
*outy = vy + 2.0f * (q->z * cx - q->x * cz);
|
|
*outz = vz + 2.0f * (q->x * cy - q->y * cx);
|
|
}
|
|
|
|
static void vec3_sub(const float a[3], const float b[3], float out[3]) {
|
|
out[0] = a[0] - b[0];
|
|
out[1] = a[1] - b[1];
|
|
out[2] = a[2] - b[2];
|
|
}
|
|
|
|
static float vec3_dot(const float a[3], const float b[3]) {
|
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
}
|
|
|
|
static void vec3_cross(const float a[3], const float b[3], float out[3]) {
|
|
out[0] = a[1] * b[2] - a[2] * b[1];
|
|
out[1] = a[2] * b[0] - a[0] * b[2];
|
|
out[2] = a[0] * b[1] - a[1] * b[0];
|
|
}
|
|
|
|
// Returns false (out left untouched) if v is too close to zero-length to
|
|
// normalize safely - callers use this to detect a degenerate billboard
|
|
// axis and fall back to another reference vector.
|
|
static bool vec3_normalize(const float v[3], float out[3]) {
|
|
float len = sqrtf(vec3_dot(v, v));
|
|
if (len < 1e-6f)
|
|
return false;
|
|
out[0] = v[0] / len;
|
|
out[1] = v[1] / len;
|
|
out[2] = v[2] / len;
|
|
return true;
|
|
}
|
|
|
|
// Converts an orthonormal local->world rotation matrix (given as its
|
|
// columns - x/y/z, each the world-space direction of that local axis) to
|
|
// an XrQuaternionf, via the standard trace-based (Shepperd) method.
|
|
static void mat3_to_quat(const float x[3], const float y[3], const float z[3], XrQuaternionf *q) {
|
|
float m00 = x[0], m10 = x[1], m20 = x[2];
|
|
float m01 = y[0], m11 = y[1], m21 = y[2];
|
|
float m02 = z[0], m12 = z[1], m22 = z[2];
|
|
float trace = m00 + m11 + m22;
|
|
if (trace > 0.0f) {
|
|
float s = sqrtf(trace + 1.0f) * 2.0f;
|
|
q->w = 0.25f * s;
|
|
q->x = (m21 - m12) / s;
|
|
q->y = (m02 - m20) / s;
|
|
q->z = (m10 - m01) / s;
|
|
} else if (m00 > m11 && m00 > m22) {
|
|
float s = sqrtf(1.0f + m00 - m11 - m22) * 2.0f;
|
|
q->w = (m21 - m12) / s;
|
|
q->x = 0.25f * s;
|
|
q->y = (m01 + m10) / s;
|
|
q->z = (m02 + m20) / s;
|
|
} else if (m11 > m22) {
|
|
float s = sqrtf(1.0f + m11 - m00 - m22) * 2.0f;
|
|
q->w = (m02 - m20) / s;
|
|
q->x = (m01 + m10) / s;
|
|
q->y = 0.25f * s;
|
|
q->z = (m12 + m21) / s;
|
|
} else {
|
|
float s = sqrtf(1.0f + m22 - m00 - m11) * 2.0f;
|
|
q->w = (m10 - m01) / s;
|
|
q->x = (m02 + m20) / s;
|
|
q->y = (m12 + m21) / s;
|
|
q->z = 0.25f * s;
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Remappable controls: raw-input naming, per-frame state, capture mode
|
|
// ===========================================================================
|
|
|
|
// Bounds-checked - always returns a valid string ("none" for an
|
|
// out-of-range value, which shouldn't happen but is cheap to guard).
|
|
static const char *xr_input_raw_input_name(QsRawInput raw) {
|
|
if (raw < 0 || raw >= QS_RAW_COUNT)
|
|
return kRawInputNames[QS_RAW_NONE];
|
|
return kRawInputNames[raw];
|
|
}
|
|
|
|
// Returns false (leaves *out untouched) if `name` isn't one of
|
|
// kRawInputNames[]'s exact strings - the caller (qs_config.c's
|
|
// qs_apply_player_controls()) treats that as an invalid config value, same
|
|
// WARN+skip handling as an invalid renderer/resolution string.
|
|
static bool xr_input_raw_input_from_name(const char *name, QsRawInput *out) {
|
|
for (int i = 0; i < QS_RAW_COUNT; i++) {
|
|
if (strcmp(name, kRawInputNames[i]) == 0) {
|
|
*out = (QsRawInput)i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// LEFT/RIGHT for any of the first QS_RAW_CONCRETE_COUNT values (every one
|
|
// of those names is already hand-qualified) - never called for the
|
|
// symmetric/none pseudo-values, which have no single hand.
|
|
static int xr_input_raw_input_hand(QsRawInput raw) {
|
|
switch (raw) {
|
|
case QS_RAW_LEFT_TRIGGER:
|
|
case QS_RAW_LEFT_SQUEEZE:
|
|
case QS_RAW_LEFT_X:
|
|
case QS_RAW_LEFT_Y:
|
|
case QS_RAW_LEFT_THUMBSTICK_CLICK:
|
|
case QS_RAW_LEFT_THUMBSTICK_UP:
|
|
case QS_RAW_LEFT_THUMBSTICK_DOWN:
|
|
case QS_RAW_LEFT_THUMBSTICK_LEFT:
|
|
case QS_RAW_LEFT_THUMBSTICK_RIGHT:
|
|
return LEFT;
|
|
default:
|
|
return RIGHT;
|
|
}
|
|
}
|
|
|
|
// Resolves what a bound raw input means for one specific hand's ray this
|
|
// frame - the one place symmetric bindings ("trigger"/"squeeze") and
|
|
// per-hand exclusivity (a concrete binding only ever fires for its own
|
|
// hand) both get handled, shared by mouse_click/mouse_double_click's
|
|
// resolution in the per-hand loop below.
|
|
static bool xr_input_raw_state_for_hand(QsRawInput raw, int hand) {
|
|
if (raw == QS_RAW_NONE)
|
|
return false;
|
|
if (raw == QS_RAW_TRIGGER_SYMMETRIC)
|
|
return g_raw_down[hand == LEFT ? QS_RAW_LEFT_TRIGGER : QS_RAW_RIGHT_TRIGGER];
|
|
if (raw == QS_RAW_SQUEEZE_SYMMETRIC)
|
|
return g_raw_down[hand == LEFT ? QS_RAW_LEFT_SQUEEZE : QS_RAW_RIGHT_SQUEEZE];
|
|
if (xr_input_raw_input_hand(raw) != hand)
|
|
return false;
|
|
return g_raw_down[raw];
|
|
}
|
|
|
|
// True while `action` shouldn't fire its own effect this frame because
|
|
// it's the very action currently being captured (see xr_input_capture_begin())
|
|
// - e.g. capturing "jump" shouldn't also make the avatar jump.
|
|
static bool xr_input_action_suppressed_by_capture(QsControlAction action) {
|
|
return g_capture_active && g_capture_action == action;
|
|
}
|
|
|
|
static bool xr_input_bool_state(XrAction action, int hand) {
|
|
if (action == XR_NULL_HANDLE)
|
|
return false;
|
|
XrActionStateGetInfo info = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
info.action = action;
|
|
info.subactionPath = g_hand_path[hand];
|
|
XrActionStateBoolean state = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
|
xrGetActionStateBoolean(g_session, &info, &state);
|
|
return state.isActive && state.currentState;
|
|
}
|
|
|
|
// Digitizes one hand's thumbstick/trackpad Vector2f into 4 independent
|
|
// directions with hysteresis (engage past kEngage, release back below
|
|
// kRelease) - same engage/release reasoning as the existing menu-only
|
|
// snap-turn's own hysteresis latch (g_turn_ready), just applied
|
|
// per-direction here so a diagonal deflection can hold two directions at
|
|
// once. y>0 is "away from the player" (forward), matching the existing
|
|
// smooth-move code's own convention for the same Vector2f action.
|
|
static void xr_input_digitize_stick(XrStickDigital *state, bool active, float x, float y) {
|
|
const float kEngage = 0.5f;
|
|
const float kRelease = 0.3f;
|
|
if (!active) {
|
|
state->up = state->down = state->left = state->right = false;
|
|
return;
|
|
}
|
|
if (state->up) {
|
|
if (y < kRelease)
|
|
state->up = false;
|
|
} else if (y > kEngage) {
|
|
state->up = true;
|
|
}
|
|
if (state->down) {
|
|
if (-y < kRelease)
|
|
state->down = false;
|
|
} else if (-y > kEngage) {
|
|
state->down = true;
|
|
}
|
|
if (state->left) {
|
|
if (-x < kRelease)
|
|
state->left = false;
|
|
} else if (-x > kEngage) {
|
|
state->left = true;
|
|
}
|
|
if (state->right) {
|
|
if (x < kRelease)
|
|
state->right = false;
|
|
} else if (x > kEngage) {
|
|
state->right = true;
|
|
}
|
|
}
|
|
|
|
// Fills g_raw_down[] for every one of the first QS_RAW_CONCRETE_COUNT raw
|
|
// inputs - called once per frame, unconditionally (regardless of
|
|
// menuVisible), before capture mode's scan and the key-action dispatch
|
|
// loop both read it. g_select_click_action doubles as the source for
|
|
// QS_RAW_*_TRIGGER (see its own declaration comment) - already synced by
|
|
// xrSyncActions() before this runs, same as every other action here.
|
|
static void xr_input_update_raw_states(void) {
|
|
g_raw_down[QS_RAW_LEFT_TRIGGER] = xr_input_bool_state(g_select_click_action, LEFT);
|
|
g_raw_down[QS_RAW_RIGHT_TRIGGER] = xr_input_bool_state(g_select_click_action, RIGHT);
|
|
g_raw_down[QS_RAW_LEFT_SQUEEZE] = xr_input_bool_state(g_squeeze_action, LEFT);
|
|
g_raw_down[QS_RAW_RIGHT_SQUEEZE] = xr_input_bool_state(g_squeeze_action, RIGHT);
|
|
g_raw_down[QS_RAW_LEFT_X] = xr_input_bool_state(g_x_click_action, LEFT);
|
|
g_raw_down[QS_RAW_LEFT_Y] = xr_input_bool_state(g_y_click_action, LEFT);
|
|
g_raw_down[QS_RAW_RIGHT_A] = xr_input_bool_state(g_a_click_action, RIGHT);
|
|
g_raw_down[QS_RAW_RIGHT_B] = xr_input_bool_state(g_b_click_action, RIGHT);
|
|
g_raw_down[QS_RAW_LEFT_THUMBSTICK_CLICK] = xr_input_bool_state(g_thumbstick_click_action, LEFT);
|
|
g_raw_down[QS_RAW_RIGHT_THUMBSTICK_CLICK] =
|
|
xr_input_bool_state(g_thumbstick_click_action, RIGHT);
|
|
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
XrActionStateGetInfo info = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
info.action = g_thumbstick_action;
|
|
info.subactionPath = g_hand_path[hand];
|
|
XrActionStateVector2f state = {XR_TYPE_ACTION_STATE_VECTOR2F};
|
|
xrGetActionStateVector2f(g_session, &info, &state);
|
|
xr_input_digitize_stick(&g_stick_digital[hand], state.isActive, state.currentState.x,
|
|
state.currentState.y);
|
|
g_stick_xy[hand][0] = state.isActive ? state.currentState.x : 0.0f;
|
|
g_stick_xy[hand][1] = state.isActive ? state.currentState.y : 0.0f;
|
|
}
|
|
g_raw_down[QS_RAW_LEFT_THUMBSTICK_UP] = g_stick_digital[LEFT].up;
|
|
g_raw_down[QS_RAW_LEFT_THUMBSTICK_DOWN] = g_stick_digital[LEFT].down;
|
|
g_raw_down[QS_RAW_LEFT_THUMBSTICK_LEFT] = g_stick_digital[LEFT].left;
|
|
g_raw_down[QS_RAW_LEFT_THUMBSTICK_RIGHT] = g_stick_digital[LEFT].right;
|
|
g_raw_down[QS_RAW_RIGHT_THUMBSTICK_UP] = g_stick_digital[RIGHT].up;
|
|
g_raw_down[QS_RAW_RIGHT_THUMBSTICK_DOWN] = g_stick_digital[RIGHT].down;
|
|
g_raw_down[QS_RAW_RIGHT_THUMBSTICK_LEFT] = g_stick_digital[RIGHT].left;
|
|
g_raw_down[QS_RAW_RIGHT_THUMBSTICK_RIGHT] = g_stick_digital[RIGHT].right;
|
|
}
|
|
|
|
// True for the 8 thumbstick-direction identifiers specifically - see
|
|
// xr_input_sync_and_draw()'s own comment on why a movement action bound to
|
|
// one of these is suppressed while the menu is open (the same physical
|
|
// stick already drives VR-position calibration in that state).
|
|
static bool xr_input_raw_is_thumbstick_direction(QsRawInput raw) {
|
|
return raw >= QS_RAW_LEFT_THUMBSTICK_UP && raw <= QS_RAW_RIGHT_THUMBSTICK_RIGHT;
|
|
}
|
|
|
|
void xr_input_capture_begin(int action_index) {
|
|
if (action_index < 0 || action_index >= QS_ACTION_COUNT)
|
|
return;
|
|
g_capture_active = true;
|
|
g_capture_action = (QsControlAction)action_index;
|
|
g_capture_start_ticks = SDL_GetTicks();
|
|
}
|
|
|
|
void xr_input_capture_cancel(void) { g_capture_active = false; }
|
|
|
|
void xr_input_reset_player_controls(void) {
|
|
// Mirrors s_binding[]'s own static initializer exactly (see its own
|
|
// comment) - the hotkey actions all default to unbound.
|
|
static const QsRawInput kDefaults[QS_ACTION_COUNT] = {
|
|
QS_RAW_LEFT_THUMBSTICK_UP, // move_forward
|
|
QS_RAW_LEFT_THUMBSTICK_DOWN, // move_back
|
|
QS_RAW_LEFT_THUMBSTICK_LEFT, // strafe_left
|
|
QS_RAW_LEFT_THUMBSTICK_RIGHT, // strafe_right
|
|
QS_RAW_RIGHT_THUMBSTICK_LEFT, // turn_left
|
|
QS_RAW_RIGHT_THUMBSTICK_RIGHT, // turn_right
|
|
QS_RAW_LEFT_THUMBSTICK_CLICK, // jump
|
|
QS_RAW_RIGHT_A, // mouse_click
|
|
QS_RAW_RIGHT_B, // mouse_double_click
|
|
QS_RAW_RIGHT_THUMBSTICK_UP, // stand
|
|
QS_RAW_NONE, // crouch
|
|
QS_RAW_RIGHT_THUMBSTICK_DOWN, // prone
|
|
QS_RAW_NONE, // pause
|
|
QS_RAW_NONE, // full_view
|
|
QS_RAW_NONE, // normal_view
|
|
QS_RAW_RIGHT_THUMBSTICK_CLICK, // map_view
|
|
QS_RAW_NONE, // save_game
|
|
QS_RAW_NONE, // load_game
|
|
QS_RAW_NONE, // select_grenade
|
|
QS_RAW_NONE, // arm_grenade
|
|
QS_RAW_NONE, // select_drug
|
|
QS_RAW_NONE, // use_drug
|
|
QS_RAW_NONE, // toggle_online_help
|
|
QS_RAW_NONE, // show_help
|
|
QS_RAW_NONE, // toggle_music
|
|
QS_RAW_NONE, // quit
|
|
QS_RAW_NONE, // cycle_weapon_next
|
|
QS_RAW_NONE, // cycle_weapon_prev
|
|
QS_RAW_NONE, // cycle_detail
|
|
QS_RAW_NONE, // reload_weapon_primary
|
|
QS_RAW_NONE, // reload_weapon_secondary
|
|
QS_RAW_NONE, // hud_color
|
|
QS_RAW_NONE, // clear_fullscreen
|
|
QS_RAW_NONE, // mfd_left_1
|
|
QS_RAW_NONE, // mfd_left_2
|
|
QS_RAW_NONE, // mfd_left_3
|
|
QS_RAW_NONE, // mfd_left_4
|
|
QS_RAW_NONE, // mfd_left_5
|
|
QS_RAW_NONE, // mfd_right_1
|
|
QS_RAW_NONE, // mfd_right_2
|
|
QS_RAW_NONE, // mfd_right_3
|
|
QS_RAW_NONE, // mfd_right_4
|
|
QS_RAW_NONE, // mfd_right_5
|
|
QS_RAW_RIGHT_TRIGGER, // fire_weapon
|
|
QS_RAW_LEFT_SQUEEZE, // free_left_thumbstick_view
|
|
QS_RAW_NONE, // free_right_thumbstick_view
|
|
QS_RAW_RIGHT_THUMBSTICK_UP, // cyber_thrust
|
|
QS_RAW_LEFT_THUMBSTICK_UP, // cyber_climb
|
|
QS_RAW_LEFT_THUMBSTICK_DOWN, // cyber_dive
|
|
QS_RAW_LEFT_THUMBSTICK_LEFT, // cyber_bank_left
|
|
QS_RAW_LEFT_THUMBSTICK_RIGHT, // cyber_bank_right
|
|
QS_RAW_RIGHT_THUMBSTICK_LEFT, // cyber_roll_left
|
|
QS_RAW_RIGHT_THUMBSTICK_RIGHT, // cyber_roll_right
|
|
QS_RAW_NONE, // cyber_climb_left
|
|
QS_RAW_NONE, // cyber_climb_right
|
|
QS_RAW_NONE, // cyber_dive_left
|
|
QS_RAW_NONE, // cyber_dive_right
|
|
};
|
|
memcpy(s_binding, kDefaults, sizeof(s_binding));
|
|
g_capture_active = false;
|
|
}
|
|
|
|
int xr_input_get_action_binding(int action_index) {
|
|
if (action_index < 0 || action_index >= QS_ACTION_COUNT)
|
|
return QS_RAW_NONE;
|
|
return (int)s_binding[action_index];
|
|
}
|
|
|
|
bool xr_input_set_action_binding(int action_index, const char *raw_name) {
|
|
if (action_index < 0 || action_index >= QS_ACTION_COUNT)
|
|
return false;
|
|
QsRawInput raw;
|
|
if (!xr_input_raw_input_from_name(raw_name, &raw))
|
|
return false;
|
|
// Only mouse_click/mouse_double_click may use the symmetric forms -
|
|
// see QsRawInput's own comment.
|
|
if ((raw == QS_RAW_TRIGGER_SYMMETRIC || raw == QS_RAW_SQUEEZE_SYMMETRIC) &&
|
|
action_index != QS_ACTION_MOUSE_CLICK && action_index != QS_ACTION_MOUSE_DOUBLE_CLICK)
|
|
return false;
|
|
s_binding[action_index] = raw;
|
|
return true;
|
|
}
|
|
|
|
const char *xr_input_get_action_binding_name(int action_index) {
|
|
return xr_input_raw_input_name((QsRawInput)xr_input_get_action_binding(action_index));
|
|
}
|
|
|
|
// Where an action applies. NORMAL actions (walking, jumping, posture) are
|
|
// inactive in cyberspace and CYBER actions only work there, so each set has its
|
|
// own bindings and one input can be bound in both. Everything else (clicks,
|
|
// fire, free view, hotkeys) applies everywhere.
|
|
typedef enum { QS_CONTEXT_ALWAYS, QS_CONTEXT_NORMAL, QS_CONTEXT_CYBER } QsActionContext;
|
|
|
|
static QsActionContext xr_input_action_context(QsControlAction action) {
|
|
if (action >= QS_ACTION_CYBER_THRUST)
|
|
return QS_CONTEXT_CYBER;
|
|
switch (action) {
|
|
case QS_ACTION_MOVE_FORWARD:
|
|
case QS_ACTION_MOVE_BACK:
|
|
case QS_ACTION_STRAFE_LEFT:
|
|
case QS_ACTION_STRAFE_RIGHT:
|
|
case QS_ACTION_TURN_LEFT:
|
|
case QS_ACTION_TURN_RIGHT:
|
|
case QS_ACTION_JUMP:
|
|
case QS_ACTION_STAND:
|
|
case QS_ACTION_CROUCH:
|
|
case QS_ACTION_PRONE:
|
|
return QS_CONTEXT_NORMAL;
|
|
default:
|
|
return QS_CONTEXT_ALWAYS;
|
|
}
|
|
}
|
|
|
|
// True if two actions can be active at the same time, so one input shared by
|
|
// both would fire both.
|
|
static bool xr_input_contexts_overlap(QsControlAction a, QsControlAction b) {
|
|
QsActionContext ca = xr_input_action_context(a), cb = xr_input_action_context(b);
|
|
return ca == QS_CONTEXT_ALWAYS || cb == QS_CONTEXT_ALWAYS || ca == cb;
|
|
}
|
|
|
|
// True if an action holding `binding` would still fire from a press of
|
|
// `matched`: the same concrete input, or a symmetric "trigger"/"squeeze"
|
|
// binding when `matched` is that hand's trigger/squeeze.
|
|
static bool xr_input_binding_conflicts(QsRawInput binding, QsRawInput matched) {
|
|
if (binding == matched)
|
|
return true;
|
|
if (binding == QS_RAW_TRIGGER_SYMMETRIC)
|
|
return matched == QS_RAW_LEFT_TRIGGER || matched == QS_RAW_RIGHT_TRIGGER;
|
|
if (binding == QS_RAW_SQUEEZE_SYMMETRIC)
|
|
return matched == QS_RAW_LEFT_SQUEEZE || matched == QS_RAW_RIGHT_SQUEEZE;
|
|
return false;
|
|
}
|
|
|
|
// Scans for a fresh down-edge among the QS_RAW_CONCRETE_COUNT raw inputs
|
|
// while a capture is active - called once per frame, right after
|
|
// xr_input_update_raw_states() and before the key-action dispatch loop, so
|
|
// a same-frame press isn't also consumed as this action's normal effect
|
|
// (see xr_input_action_suppressed_by_capture() for the other half of that
|
|
// guarantee). On a match, enforces exclusivity (at most one action that can
|
|
// be active together with it - see xr_input_contexts_overlap() - may respond
|
|
// to a given raw input, see xr_input_binding_conflicts()) by clearing any
|
|
// other such action that would otherwise still fire from it,
|
|
// then reports both results in one call.
|
|
static void xr_input_process_capture(void) {
|
|
if (!g_capture_active)
|
|
return;
|
|
if (SDL_GetTicks() - g_capture_start_ticks > QS_CAPTURE_TIMEOUT_MS) {
|
|
g_capture_active = false;
|
|
qs_config_notify_control_capture_result((int)g_capture_action, NULL, -1, NULL);
|
|
return;
|
|
}
|
|
for (int i = 0; i < QS_RAW_CONCRETE_COUNT; i++) {
|
|
if (!(g_raw_down[i] && !g_raw_prev[i]))
|
|
continue;
|
|
QsRawInput matched = (QsRawInput)i;
|
|
int displacedAction = -1;
|
|
for (int a = 0; a < QS_ACTION_COUNT; a++) {
|
|
if (a != (int)g_capture_action &&
|
|
xr_input_contexts_overlap((QsControlAction)a, g_capture_action) &&
|
|
xr_input_binding_conflicts(s_binding[a], matched)) {
|
|
s_binding[a] = QS_RAW_NONE;
|
|
displacedAction = a;
|
|
break; // exclusivity guarantees at most one prior holder
|
|
}
|
|
}
|
|
s_binding[g_capture_action] = matched;
|
|
g_capture_active = false;
|
|
qs_config_notify_control_capture_result(
|
|
(int)g_capture_action, xr_input_raw_input_name(matched), displacedAction,
|
|
displacedAction >= 0 ? xr_input_raw_input_name(QS_RAW_NONE) : NULL);
|
|
return;
|
|
}
|
|
}
|
|
|
|
int xr_input_action_count(void) { return QS_ACTION_COUNT; }
|
|
|
|
// Bounds-checked, same convention as xr_input_raw_input_name() - "" for an
|
|
// out-of-range index.
|
|
const char *xr_input_action_name(int action_index) {
|
|
if (action_index < 0 || action_index >= QS_ACTION_COUNT)
|
|
return "";
|
|
return kActionNames[action_index];
|
|
}
|
|
|
|
bool xr_input_action_index_from_name(const char *name, int *out_index) {
|
|
for (int i = 0; i < QS_ACTION_COUNT; i++) {
|
|
if (strcmp(name, kActionNames[i]) == 0) {
|
|
*out_index = i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const char *xr_input_get_active_interaction_profile_path(void) { return g_active_profile_path; }
|
|
|
|
void xr_input_set_controller_options_tab_open(bool open) { g_controller_options_tab_open = open; }
|
|
|
|
// Shared boilerplate for every boolean action below - aim_pose (Pose) and
|
|
// thumbstick (Vector2f) stay special-cased since they're a different
|
|
// XrActionType.
|
|
static bool xr_input_create_bool_action(const char *name, const char *localized,
|
|
uint32_t subaction_count, const XrPath *subaction_paths,
|
|
XrAction *out) {
|
|
XrActionCreateInfo info = {XR_TYPE_ACTION_CREATE_INFO};
|
|
strncpy(info.actionName, name, XR_MAX_ACTION_NAME_SIZE - 1);
|
|
strncpy(info.localizedActionName, localized, XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
|
info.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
|
|
info.countSubactionPaths = subaction_count;
|
|
info.subactionPaths = subaction_paths;
|
|
char what[64];
|
|
snprintf(what, sizeof(what), "xrCreateAction(%s)", name);
|
|
return xr_check(xrCreateAction(g_action_set, &info, out), what);
|
|
}
|
|
|
|
// Suggests one interaction profile's bindings and, on failure, reports the
|
|
// fatal-startup error (see qs_config_report_fatal_input_error()'s own
|
|
// comment) - a wrong/nonexistent path anywhere in `bindings` fails this
|
|
// entire call, not just that one entry, so this is the one place that
|
|
// matters for every profile.
|
|
static bool xr_input_suggest_bindings(XrInstance instance, const char *profile_path,
|
|
const char *profile_display_name,
|
|
const XrActionSuggestedBinding *bindings, uint32_t count) {
|
|
XrPath profilePath;
|
|
xrStringToPath(instance, profile_path, &profilePath);
|
|
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
|
suggested.interactionProfile = profilePath;
|
|
suggested.countSuggestedBindings = count;
|
|
suggested.suggestedBindings = bindings;
|
|
char what[96];
|
|
snprintf(what, sizeof(what), "xrSuggestInteractionProfileBindings(%s)", profile_display_name);
|
|
if (!xr_check(xrSuggestInteractionProfileBindings(instance, &suggested), what)) {
|
|
qs_config_report_fatal_input_error(profile_display_name);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) {
|
|
g_instance = instance;
|
|
g_session = session;
|
|
|
|
xrStringToPath(instance, "/user/hand/left", &g_hand_path[LEFT]);
|
|
xrStringToPath(instance, "/user/hand/right", &g_hand_path[RIGHT]);
|
|
|
|
XrActionSetCreateInfo setInfo = {XR_TYPE_ACTION_SET_CREATE_INFO};
|
|
strncpy(setInfo.actionSetName, "gameplay", XR_MAX_ACTION_SET_NAME_SIZE - 1);
|
|
strncpy(setInfo.localizedActionSetName, "Gameplay", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1);
|
|
setInfo.priority = 0;
|
|
if (!xr_check(xrCreateActionSet(instance, &setInfo, &g_action_set), "xrCreateActionSet"))
|
|
return false;
|
|
|
|
XrActionCreateInfo aimInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
|
strncpy(aimInfo.actionName, "aim_pose", XR_MAX_ACTION_NAME_SIZE - 1);
|
|
strncpy(aimInfo.localizedActionName, "Aim Pose", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
|
aimInfo.actionType = XR_ACTION_TYPE_POSE_INPUT;
|
|
aimInfo.countSubactionPaths = 2;
|
|
aimInfo.subactionPaths = g_hand_path;
|
|
if (!xr_check(xrCreateAction(g_action_set, &aimInfo, &g_aim_pose_action),
|
|
"xrCreateAction(aim_pose)"))
|
|
return false;
|
|
|
|
if (!xr_input_create_bool_action("select_click", "Select", 2, g_hand_path,
|
|
&g_select_click_action))
|
|
return false;
|
|
// The touch_controller/vive_controller/simple_controller profiles'
|
|
// "menu" component only exists on the left controller (by hardware, on
|
|
// Touch) or is only ever suggested there anyway (Vive, simple - see
|
|
// this function's own per-profile binding arrays below) - there's no
|
|
// right-hand equivalent suggested for any of them.
|
|
if (!xr_input_create_bool_action("menu_toggle", "Menu Toggle", 1, &g_hand_path[LEFT],
|
|
&g_menu_toggle_action))
|
|
return false;
|
|
if (!xr_input_create_bool_action("a_click", "A Button", 1, &g_hand_path[RIGHT],
|
|
&g_a_click_action))
|
|
return false;
|
|
if (!xr_input_create_bool_action("b_click", "B Button", 1, &g_hand_path[RIGHT],
|
|
&g_b_click_action))
|
|
return false;
|
|
if (!xr_input_create_bool_action("x_click", "X Button", 1, &g_hand_path[LEFT],
|
|
&g_x_click_action))
|
|
return false;
|
|
if (!xr_input_create_bool_action("y_click", "Y Button", 1, &g_hand_path[LEFT],
|
|
&g_y_click_action))
|
|
return false;
|
|
if (!xr_input_create_bool_action("squeeze", "Squeeze", 2, g_hand_path, &g_squeeze_action))
|
|
return false;
|
|
if (!xr_input_create_bool_action("thumbstick_click", "Thumbstick Click", 2, g_hand_path,
|
|
&g_thumbstick_click_action))
|
|
return false;
|
|
|
|
// Player locomotion, see g_thumbstick_action's own declaration comment.
|
|
XrActionCreateInfo thumbstickInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
|
strncpy(thumbstickInfo.actionName, "thumbstick", XR_MAX_ACTION_NAME_SIZE - 1);
|
|
strncpy(thumbstickInfo.localizedActionName, "Thumbstick", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
|
thumbstickInfo.actionType = XR_ACTION_TYPE_VECTOR2F_INPUT;
|
|
thumbstickInfo.countSubactionPaths = 2;
|
|
thumbstickInfo.subactionPaths = g_hand_path;
|
|
if (!xr_check(xrCreateAction(g_action_set, &thumbstickInfo, &g_thumbstick_action),
|
|
"xrCreateAction(thumbstick)"))
|
|
return false;
|
|
|
|
// Every path the three suggested-bindings arrays below reference. One
|
|
// wrong or nonexistent path fails that profile's whole
|
|
// xrSuggestInteractionProfileBindings call (see
|
|
// xr_input_suggest_bindings()).
|
|
XrPath aimPoseLeft, aimPoseRight, menuClick;
|
|
xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRight);
|
|
xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClick);
|
|
|
|
// touch_controller has no .../trigger/click component, only the float
|
|
// .../trigger/value - binding this boolean action straight to it
|
|
// relies on OpenXR's standard action-type equivalence (runtimes
|
|
// auto-convert float inputs to boolean via an internal threshold), the
|
|
// same pattern Khronos' own hello_xr sample uses for its "select"
|
|
// action.
|
|
XrPath touchTriggerLeft, touchTriggerRight, touchAClick, touchBClick, touchXClick, touchYClick,
|
|
touchSqueezeLeft, touchSqueezeRight, touchStickLeft, touchStickRight, touchStickClickLeft,
|
|
touchStickClickRight;
|
|
xrStringToPath(instance, "/user/hand/left/input/trigger/value", &touchTriggerLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/trigger/value", &touchTriggerRight);
|
|
xrStringToPath(instance, "/user/hand/right/input/a/click", &touchAClick);
|
|
xrStringToPath(instance, "/user/hand/right/input/b/click", &touchBClick);
|
|
xrStringToPath(instance, "/user/hand/left/input/x/click", &touchXClick);
|
|
xrStringToPath(instance, "/user/hand/left/input/y/click", &touchYClick);
|
|
xrStringToPath(instance, "/user/hand/left/input/squeeze/value", &touchSqueezeLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/squeeze/value", &touchSqueezeRight);
|
|
xrStringToPath(instance, "/user/hand/left/input/thumbstick", &touchStickLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/thumbstick", &touchStickRight);
|
|
xrStringToPath(instance, "/user/hand/left/input/thumbstick/click", &touchStickClickLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/thumbstick/click", &touchStickClickRight);
|
|
const XrActionSuggestedBinding touchBindings[] = {
|
|
{g_aim_pose_action, aimPoseLeft},
|
|
{g_aim_pose_action, aimPoseRight},
|
|
{g_select_click_action, touchTriggerLeft},
|
|
{g_select_click_action, touchTriggerRight},
|
|
{g_menu_toggle_action, menuClick},
|
|
{g_a_click_action, touchAClick},
|
|
{g_b_click_action, touchBClick},
|
|
{g_x_click_action, touchXClick},
|
|
{g_y_click_action, touchYClick},
|
|
{g_squeeze_action, touchSqueezeLeft},
|
|
{g_squeeze_action, touchSqueezeRight},
|
|
{g_thumbstick_action, touchStickLeft},
|
|
{g_thumbstick_action, touchStickRight},
|
|
{g_thumbstick_click_action, touchStickClickLeft},
|
|
{g_thumbstick_click_action, touchStickClickRight},
|
|
};
|
|
if (!xr_input_suggest_bindings(instance, "/interaction_profiles/oculus/touch_controller",
|
|
"Meta Quest Touch", touchBindings,
|
|
sizeof(touchBindings) / sizeof(touchBindings[0])))
|
|
return false;
|
|
|
|
// HTC Vive wand: trigger and squeeze are native booleans (squeeze has no
|
|
// analog value); the trackpad takes the thumbstick/thumbstick_click
|
|
// actions' place, so xr_input_update_raw_states()'s digitization applies
|
|
// unchanged. There is no A/B/X/Y, so those four actions are never bound
|
|
// under this profile.
|
|
XrPath viveTriggerLeft, viveTriggerRight, viveSqueezeLeft, viveSqueezeRight, viveTrackpadLeft,
|
|
viveTrackpadRight, viveTrackpadClickLeft, viveTrackpadClickRight;
|
|
xrStringToPath(instance, "/user/hand/left/input/trigger/click", &viveTriggerLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/trigger/click", &viveTriggerRight);
|
|
xrStringToPath(instance, "/user/hand/left/input/squeeze/click", &viveSqueezeLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/squeeze/click", &viveSqueezeRight);
|
|
xrStringToPath(instance, "/user/hand/left/input/trackpad", &viveTrackpadLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/trackpad", &viveTrackpadRight);
|
|
xrStringToPath(instance, "/user/hand/left/input/trackpad/click", &viveTrackpadClickLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/trackpad/click", &viveTrackpadClickRight);
|
|
const XrActionSuggestedBinding viveBindings[] = {
|
|
{g_aim_pose_action, aimPoseLeft},
|
|
{g_aim_pose_action, aimPoseRight},
|
|
{g_select_click_action, viveTriggerLeft},
|
|
{g_select_click_action, viveTriggerRight},
|
|
{g_menu_toggle_action, menuClick},
|
|
{g_squeeze_action, viveSqueezeLeft},
|
|
{g_squeeze_action, viveSqueezeRight},
|
|
{g_thumbstick_action, viveTrackpadLeft},
|
|
{g_thumbstick_action, viveTrackpadRight},
|
|
{g_thumbstick_click_action, viveTrackpadClickLeft},
|
|
{g_thumbstick_click_action, viveTrackpadClickRight},
|
|
};
|
|
if (!xr_input_suggest_bindings(instance, "/interaction_profiles/htc/vive_controller",
|
|
"HTC Vive", viveBindings,
|
|
sizeof(viveBindings) / sizeof(viveBindings[0])))
|
|
return false;
|
|
|
|
// khr/simple_controller - OpenXR's own minimal baseline profile, not a
|
|
// specific device: select+menu only, deliberately so (see CONFIG.md).
|
|
// No squeeze/thumbstick/A-B-X-Y bindings exist for it - every action
|
|
// other than select_click/menu_toggle/aim_pose is permanently inactive
|
|
// under this profile.
|
|
XrPath simpleSelectLeft, simpleSelectRight;
|
|
xrStringToPath(instance, "/user/hand/left/input/select/click", &simpleSelectLeft);
|
|
xrStringToPath(instance, "/user/hand/right/input/select/click", &simpleSelectRight);
|
|
const XrActionSuggestedBinding simpleBindings[] = {
|
|
{g_aim_pose_action, aimPoseLeft},
|
|
{g_aim_pose_action, aimPoseRight},
|
|
{g_select_click_action, simpleSelectLeft},
|
|
{g_select_click_action, simpleSelectRight},
|
|
{g_menu_toggle_action, menuClick},
|
|
};
|
|
if (!xr_input_suggest_bindings(instance, "/interaction_profiles/khr/simple_controller",
|
|
"Generic OpenXR Controller", simpleBindings,
|
|
sizeof(simpleBindings) / sizeof(simpleBindings[0])))
|
|
return false;
|
|
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
XrActionSpaceCreateInfo spaceInfo = {XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
|
spaceInfo.action = g_aim_pose_action;
|
|
spaceInfo.subactionPath = g_hand_path[hand];
|
|
spaceInfo.poseInActionSpace.orientation.w = 1.0f;
|
|
if (!xr_check(xrCreateActionSpace(session, &spaceInfo, &g_aim_space[hand]),
|
|
"xrCreateActionSpace(aim)"))
|
|
return false;
|
|
}
|
|
|
|
XrSessionActionSetsAttachInfo attachInfo = {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
|
|
attachInfo.countActionSets = 1;
|
|
attachInfo.actionSets = &g_action_set;
|
|
if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets"))
|
|
return false;
|
|
|
|
XrReferenceSpaceCreateInfo viewSpaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
|
viewSpaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
|
|
viewSpaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
|
|
if (!xr_check(xrCreateReferenceSpace(session, &viewSpaceInfo, &g_view_space),
|
|
"xrCreateReferenceSpace(view)"))
|
|
return false;
|
|
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
if (!xr_swapchain_create(instance, session, swapchain_format, BEAM_TEX_SIZE, BEAM_TEX_SIZE,
|
|
&g_beam_swapchain[hand])) {
|
|
LOGE("XR: beam swapchain setup failed for hand %d", hand);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
LOGI("XR: input action set ready (Touch/Vive/generic bindings suggested, %d remappable "
|
|
"actions)",
|
|
(int)QS_ACTION_COUNT);
|
|
return true;
|
|
}
|
|
|
|
// Hit-tests/dispatches this hand's ray against one overlay quad - shared
|
|
// by the menu launcher and keyboard overlays in xr_input_sync_and_draw()
|
|
// below. titleBarVFraction/dragHandleUFraction and dragState select the
|
|
// per-overlay title-bar/drag-handle geometry and drag bookkeeping (see the
|
|
// *_TITLE_BAR_V_FRACTION/*_DRAG_HANDLE_U_FRACTION/*_CLOSE_BUTTON_U_FRACTION
|
|
// defines above and g_keyboard_drag/g_menu_drag) - both overlays are
|
|
// draggable via their title bar, and any hit below titleBarVFraction is
|
|
// always a plain click regardless of overlay. Returns true only when this
|
|
// hand's ray is actually relevant to this overlay this frame - landing on
|
|
// it now, or continuing a touch/drag begun on a previous frame for this
|
|
// hand that hasn't been released yet - not merely because the overlay is
|
|
// visible. The caller should only stop trying other targets (menu/
|
|
// keyboard/game quad) when this returns true; a visible-but-unclaimed
|
|
// overlay falls through so a lower-priority target (ultimately the game
|
|
// quad) can still be aimed at.
|
|
static bool xr_input_try_overlay(XrOverlay *overlay, float titleBarVFraction,
|
|
float dragHandleUFraction, XrDragState *dragState, int hand,
|
|
const XrSpaceLocation *location, float fx, float fy, float fz,
|
|
bool selectDownEdge, bool selectUpEdge, bool *touchActive,
|
|
bool *prevHit, const char *quadName, float *outCursorU,
|
|
float *outCursorV, bool *outCursorHit, bool *outPlaneHit,
|
|
float *outPlaneDistance) {
|
|
*outPlaneHit = false;
|
|
if (!xr_overlay_is_visible(overlay))
|
|
return false;
|
|
|
|
// Captured before this call's own down/up-edge handling below can
|
|
// mutate them, so a hand with a pending down-touch (waiting for its
|
|
// matching up) or an in-progress drag on *this* overlay still claims
|
|
// the ray this frame even if it has strayed off the panel's current
|
|
// bounds - see `claims` below.
|
|
bool hadTouch = *touchActive;
|
|
bool hadDrag = dragState->dragging && dragState->hand == hand;
|
|
|
|
float quadCenterX, quadCenterY, distance, halfWidth, halfHeight, yawDeg;
|
|
xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth,
|
|
&halfHeight, &yawDeg);
|
|
|
|
// General ray/oriented-quad intersection - the quad's plane passes
|
|
// through (quadCenterX, quadCenterY, -distance) with normal
|
|
// R_y(-yawDeg)*(0,0,1) (see xr_overlay.c's own pose-building comment on
|
|
// the same rotation). Solved by rotating the ray's origin (relative to
|
|
// the quad's center) and direction into the quad's own local frame via
|
|
// R_y(+yawDeg) - the inverse of the render-time rotation - and
|
|
// intersecting against the local z=0 plane there; A/B below are that
|
|
// rotated frame's z-components of the (center-relative) origin and the
|
|
// direction, so `t = -A/B` solves local_z(t) = A + t*B = 0. Degenerates
|
|
// to exactly the old plain "z = -distance, no rotation" formula when
|
|
// yawDeg is 0 (cosPhi=1, sinPhi=0) - true for every overlay except the
|
|
// menu launcher right after xr_input_sync_and_draw() has re-centered it
|
|
// in front of a turned player (see its own comment there).
|
|
float phi = yawDeg * QS_DEG_TO_RAD;
|
|
float cosPhi = cosf(phi), sinPhi = sinf(phi);
|
|
float dx0 = location->pose.position.x - quadCenterX;
|
|
float dz0 = location->pose.position.z + distance;
|
|
float a = -dx0 * sinPhi + dz0 * cosPhi;
|
|
float b = -fx * sinPhi + fz * cosPhi;
|
|
|
|
// worldX/Y is the raw base-space intersection point, needed as-is for
|
|
// dragging (which can legitimately move the quad to where the ray
|
|
// isn't currently hitting it, and always operates in base-space X/Y via
|
|
// xr_overlay_set_position() regardless of any rotation); cx/cy are that
|
|
// same point in the quad's own [-1,1] local space; hit/u/v (the
|
|
// equivalent 0..1, top-left-origin coordinates the Java view's pixel
|
|
// grid and the on-quad hit logs use) are only meaningful within the
|
|
// quad's current bounds, unlike planeHit.
|
|
bool planeHit = false, hit = false;
|
|
float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f, t = 0.0f;
|
|
if (fabsf(b) > 1e-5f) {
|
|
t = -a / b;
|
|
if (t > 0.0f) {
|
|
planeHit = true;
|
|
worldX = location->pose.position.x + t * fx;
|
|
worldY = location->pose.position.y + t * fy;
|
|
float worldZ = location->pose.position.z + t * fz;
|
|
float localX = (worldX - quadCenterX) * cosPhi + (worldZ + distance) * sinPhi;
|
|
float localY = worldY - quadCenterY;
|
|
float cx = localX / halfWidth;
|
|
float cy = localY / halfHeight;
|
|
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
|
|
u = (cx + 1.0f) * 0.5f;
|
|
v = (1.0f - cy) * 0.5f;
|
|
}
|
|
}
|
|
*outPlaneHit = planeHit;
|
|
*outPlaneDistance = t;
|
|
|
|
if (hit != *prevHit) {
|
|
// Disabled for now - noisy while chasing an unrelated texture bug.
|
|
// LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
|
|
// hit ? "entered" : "left", quadName, u, v);
|
|
*prevHit = hit;
|
|
}
|
|
if (hit) {
|
|
*outCursorHit = true;
|
|
*outCursorU = u;
|
|
*outCursorV = v;
|
|
}
|
|
|
|
// Only claim (and thus route/beam) this hand's ray to this overlay
|
|
// when it's actually relevant this frame: landing on it now, or
|
|
// continuing a touch/drag that started on it - never merely because
|
|
// it's visible.
|
|
bool claims = hit || hadTouch || hadDrag;
|
|
|
|
bool onTitleBar = hit && v < titleBarVFraction;
|
|
bool onDragHandle = onTitleBar && u <= dragHandleUFraction;
|
|
|
|
if (selectDownEdge) {
|
|
if (onDragHandle) {
|
|
dragState->dragging = true;
|
|
dragState->hand = hand;
|
|
dragState->offset_x = worldX - quadCenterX;
|
|
dragState->offset_y = worldY - quadCenterY;
|
|
} else if (hit) {
|
|
xr_overlay_touch(overlay, u, v, true);
|
|
*touchActive = true;
|
|
}
|
|
} else if (selectUpEdge) {
|
|
if (dragState->dragging && dragState->hand == hand) {
|
|
dragState->dragging = false;
|
|
dragState->hand = -1;
|
|
} else if (*touchActive) {
|
|
xr_overlay_touch(overlay, u, v, false);
|
|
*touchActive = false;
|
|
}
|
|
}
|
|
|
|
// Re-intersects against the same fixed-distance plane every frame the
|
|
// drag continues, so the quad tracks the hand in real time rather than
|
|
// only jumping on the down/up edges above.
|
|
if (dragState->dragging && dragState->hand == hand && planeHit) {
|
|
xr_overlay_set_position(overlay, worldX - dragState->offset_x,
|
|
worldY - dragState->offset_y);
|
|
}
|
|
return claims;
|
|
}
|
|
|
|
// Builds this hand's laser-beam quad - a thin, billboarded ribbon from the
|
|
// controller (location->pose.position) along the aim ray (fx,fy,fz,
|
|
// already unit length) to length_m meters out, oriented so it reads as a
|
|
// line from the viewer's eye (headLoc->pose.position) regardless of angle:
|
|
// one in-plane axis follows the ray direction, the other is the thin
|
|
// "width", and the quad's normal is billboarded toward the eye
|
|
// (orthogonalized against the ray axis, so the ribbon only rotates around
|
|
// its own long axis as the hand moves, never twists). Acquires/clears/
|
|
// releases this hand's tiny beam swapchain and leaves g_beam_quad[hand]
|
|
// ready for xr_input_get_beam_layer() - doesn't set space/eyeVisibility/
|
|
// layerFlags (see that function's doc comment in xr_input.h).
|
|
static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float fx, float fy,
|
|
float fz, float length_m, const XrSpaceLocation *headLoc) {
|
|
float origin[3] = {location->pose.position.x, location->pose.position.y,
|
|
location->pose.position.z};
|
|
float dirY[3] = {fx, fy, fz};
|
|
float endpoint[3] = {origin[0] + length_m * fx, origin[1] + length_m * fy,
|
|
origin[2] + length_m * fz};
|
|
float center[3] = {(origin[0] + endpoint[0]) * 0.5f, (origin[1] + endpoint[1]) * 0.5f,
|
|
(origin[2] + endpoint[2]) * 0.5f};
|
|
float head[3] = {headLoc->pose.position.x, headLoc->pose.position.y,
|
|
headLoc->pose.position.z};
|
|
|
|
float toEyeRaw[3], toEye[3];
|
|
vec3_sub(head, center, toEyeRaw);
|
|
if (!vec3_normalize(toEyeRaw, toEye)) {
|
|
// Head is (almost) exactly at the beam's midpoint - astronomically
|
|
// unlikely, but fall back to a fixed direction rather than divide
|
|
// by ~0.
|
|
toEye[0] = 0.0f;
|
|
toEye[1] = 0.0f;
|
|
toEye[2] = 1.0f;
|
|
}
|
|
|
|
// Orthogonalize toEye against the ray axis to get the billboard
|
|
// normal - if the ray points almost straight at/away from the eye,
|
|
// that leaves ~nothing to normalize, so fall back to world-up then
|
|
// world-Z, each reprojected the same way.
|
|
float normalZ[3];
|
|
float d = vec3_dot(toEye, dirY);
|
|
float proj[3] = {toEye[0] - dirY[0] * d, toEye[1] - dirY[1] * d, toEye[2] - dirY[2] * d};
|
|
if (!vec3_normalize(proj, normalZ)) {
|
|
static const float kWorldUp[3] = {0.0f, 1.0f, 0.0f};
|
|
d = vec3_dot(kWorldUp, dirY);
|
|
proj[0] = kWorldUp[0] - dirY[0] * d;
|
|
proj[1] = kWorldUp[1] - dirY[1] * d;
|
|
proj[2] = kWorldUp[2] - dirY[2] * d;
|
|
if (!vec3_normalize(proj, normalZ)) {
|
|
static const float kWorldZ[3] = {0.0f, 0.0f, 1.0f};
|
|
d = vec3_dot(kWorldZ, dirY);
|
|
proj[0] = kWorldZ[0] - dirY[0] * d;
|
|
proj[1] = kWorldZ[1] - dirY[1] * d;
|
|
proj[2] = kWorldZ[2] - dirY[2] * d;
|
|
if (!vec3_normalize(proj, normalZ)) {
|
|
// dirY parallel to both world-up and world-Z is impossible
|
|
// for two non-parallel vectors - unreachable in practice,
|
|
// but keep the beam well-defined regardless.
|
|
normalZ[0] = 1.0f;
|
|
normalZ[1] = 0.0f;
|
|
normalZ[2] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
float axisX[3];
|
|
vec3_cross(dirY, normalZ, axisX);
|
|
if (!vec3_normalize(axisX, axisX)) {
|
|
axisX[0] = 1.0f;
|
|
axisX[1] = 0.0f;
|
|
axisX[2] = 0.0f;
|
|
}
|
|
float axisZ[3];
|
|
vec3_cross(axisX, dirY, axisZ); // already unit length - axisX/dirY are orthonormal
|
|
|
|
XrQuaternionf orientation;
|
|
mat3_to_quat(axisX, dirY, axisZ, &orientation);
|
|
|
|
if (xr_swapchain_acquire(g_instance, &g_beam_swapchain[hand])) {
|
|
// Premultiplied alpha (matches OverlayPanel.compositeAndPublish()'s
|
|
// convention, and XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT's
|
|
// assumption - see xr_session.c) - kHandColor's RGB scaled by this
|
|
// beam's own alpha, not the opaque RGB itself.
|
|
const float alpha = 0.55f;
|
|
const float *c = kHandColor[hand];
|
|
glClearColor(c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
xr_swapchain_release(g_instance, &g_beam_swapchain[hand]);
|
|
}
|
|
|
|
XrCompositionLayerQuad *quad = &g_beam_quad[hand];
|
|
quad->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
|
|
quad->next = NULL;
|
|
quad->subImage.swapchain = g_beam_swapchain[hand].swapchain;
|
|
quad->subImage.imageRect.offset.x = 0;
|
|
quad->subImage.imageRect.offset.y = 0;
|
|
quad->subImage.imageRect.extent.width = BEAM_TEX_SIZE;
|
|
quad->subImage.imageRect.extent.height = BEAM_TEX_SIZE;
|
|
quad->subImage.imageArrayIndex = 0;
|
|
quad->pose.position.x = center[0];
|
|
quad->pose.position.y = center[1];
|
|
quad->pose.position.z = center[2];
|
|
quad->pose.orientation = orientation;
|
|
quad->size.width = BEAM_THICKNESS_METERS;
|
|
quad->size.height = length_m;
|
|
|
|
g_beam_quad_valid[hand] = true;
|
|
}
|
|
|
|
// Hit-tests this hand's ray against the game screen (xr_get_game_quad_extent())
|
|
// - the lowest-priority target, tried only once neither the keyboard nor
|
|
// menu overlay claimed the ray this frame (or, symmetrically, once this
|
|
// hand already has a pending mouse-down on the game screen - see
|
|
// g_game_touch_active and the keyboard/menu call sites in
|
|
// xr_input_sync_and_draw()). outU/outV (only meaningful when the
|
|
// returned hit is true) let the caller drive the game's own mouse cursor
|
|
// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual
|
|
// clicks is the caller's responsibility (xr_mouse_click()), not this
|
|
// function's, unlike xr_input_try_overlay().
|
|
//
|
|
// Two shapes: a flat plane (centralAngle == 0, the default and the only
|
|
// shape this function supported before screen curving existed - kept
|
|
// byte-for-byte unchanged as its own branch below, so nobody who never
|
|
// touches the new Video Options curve setting sees any change at all), or
|
|
// a curved cylinder segment. The curve is horizontal-only (like a real
|
|
// curved monitor) - the cylinder's vertical axis runs parallel to the
|
|
// screen's own local Y at (x=0, z=Zc) for every y, so y drops out of the
|
|
// lateral-surface intersection test entirely; halfHeight bounds the
|
|
// vertical extent exactly like the flat case, unchanged.
|
|
static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy,
|
|
float fz, bool *outPlaneHit, float *outPlaneDistance,
|
|
float *outU, float *outV) {
|
|
float distance, halfWidth, halfHeight, radius, centralAngle;
|
|
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight, &radius, ¢ralAngle);
|
|
|
|
bool planeHit = false, hit = false;
|
|
float u = 0.0f, v = 0.0f, t = 0.0f;
|
|
|
|
if (centralAngle <= 0.0f) {
|
|
if (fabsf(fz) > 1e-5f) {
|
|
t = (-distance - location->pose.position.z) / fz;
|
|
if (t > 0.0f) {
|
|
planeHit = true;
|
|
float cx = (location->pose.position.x + t * fx) / halfWidth;
|
|
float cy = (location->pose.position.y + t * fy) / halfHeight;
|
|
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
|
|
u = (cx + 1.0f) * 0.5f;
|
|
v = (1.0f - cy) * 0.5f;
|
|
}
|
|
}
|
|
} else {
|
|
// Zc: the cylinder's axis position along Z (see xr_session.c's
|
|
// own concavity-derivation comment - same offset the render path
|
|
// uses to place the actual XrCompositionLayerCylinderKHR, so the
|
|
// two can never disagree about where the surface actually is).
|
|
float Zc = -distance + radius;
|
|
float x0 = location->pose.position.x;
|
|
float dz0 = location->pose.position.z - Zc;
|
|
|
|
// Ray-vs-infinite-cylinder quadratic (only x/z participate, per
|
|
// this function's own comment above) - A<=~0 means the ray runs
|
|
// parallel to the axis (never crosses the lateral surface),
|
|
// mirroring the flat case's fabsf(fz)>1e-5f guard.
|
|
float A = fx * fx + fz * fz;
|
|
if (A > 1e-8f) {
|
|
float B = 2.0f * (fx * x0 + fz * dz0);
|
|
float C = x0 * x0 + dz0 * dz0 - radius * radius;
|
|
float disc = B * B - 4.0f * A * C;
|
|
if (disc >= 0.0f) {
|
|
float sq = sqrtf(disc);
|
|
// Smallest positive root - the near intersection, correct
|
|
// both for the normal case (viewer inside the cylinder,
|
|
// so exactly one root is positive) and if the viewer ever
|
|
// ends up outside it.
|
|
float t1 = (-B - sq) / (2.0f * A);
|
|
float t2 = (-B + sq) / (2.0f * A);
|
|
if (t1 > 0.0f)
|
|
t = t1;
|
|
else if (t2 > 0.0f)
|
|
t = t2;
|
|
if (t > 0.0f) {
|
|
planeHit = true;
|
|
float px = location->pose.position.x + t * fx;
|
|
float py = location->pose.position.y + t * fy;
|
|
float pz = location->pose.position.z + t * fz;
|
|
// Signed angle from center (angle 0 is dead ahead,
|
|
// matching the flat case's cx==0 being dead center) -
|
|
// bounded by the configured centralAngle instead of a
|
|
// halfWidth.
|
|
float phi = atan2f(px, Zc - pz);
|
|
float halfAngle = centralAngle * 0.5f;
|
|
float cy = py / halfHeight;
|
|
hit = fabsf(phi) <= halfAngle && fabsf(cy) <= 1.0f;
|
|
u = (phi / halfAngle + 1.0f) * 0.5f;
|
|
v = (1.0f - cy) * 0.5f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*outPlaneHit = planeHit;
|
|
*outPlaneDistance = t;
|
|
*outU = u;
|
|
*outV = v;
|
|
|
|
if (hit != g_game_prev_hit[hand]) {
|
|
// Disabled for now - noisy while chasing an unrelated texture bug.
|
|
// LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
|
|
// hit ? "entered" : "left", u, v);
|
|
g_game_prev_hit[hand] = hit;
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
// Re-centers the menu launcher in front of wherever the player is
|
|
// currently looking, each time it's freshly opened (see the menu-toggle
|
|
// handling below) - otherwise a player who's walked/turned away from the
|
|
// menu's last position (via the thumbstick locomotion further down, or a
|
|
// manual drag) would find it stuck somewhere behind/beside them. Uses
|
|
// xr_overlay_get_default_distance_m() (the overlay's original, never-
|
|
// mutated distance_m) rather than duplicating MENU_DISTANCE_METERS here -
|
|
// deliberately NOT the live distance_m from xr_overlay_get_quad_extent(),
|
|
// which xr_overlay_set_pose() repurposes as a raw base-space Z coordinate
|
|
// once rotation is involved (see that function's own comment) - reusing
|
|
// that as a "how far in front" magnitude would drift the menu further
|
|
// away with every reopen instead of holding a stable distance. Height
|
|
// (center_y_m) doesn't have this problem (it's never touched by any
|
|
// rotation math), so that one *is* read live, preserving a manual drag.
|
|
// Independent of needBeams/draw below (a menu toggle can happen on a frame
|
|
// nothing renders), so this does its own dedicated head locate rather than
|
|
// reusing headLoc.
|
|
static void xr_input_recenter_menu(XrOverlay *menuOverlay, XrSpace baseSpace, XrTime time) {
|
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
|
const XrSpaceLocationFlags needed =
|
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
|
if (!xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
|
|
"xrLocateSpace(view, menu recenter)") ||
|
|
(headLoc.locationFlags & needed) != needed)
|
|
return;
|
|
|
|
float fx, fy, fz;
|
|
quat_rotate_vec(&headLoc.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
|
|
// Horizontal-only, matching the play space's own yaw-only rotation -
|
|
// looking sharply up/down shouldn't tip the menu with you.
|
|
float len = sqrtf(fx * fx + fz * fz);
|
|
if (len < 1e-4f)
|
|
return; // looking almost straight up/down - leave the existing pose alone
|
|
fx /= len;
|
|
fz /= len;
|
|
|
|
float currentX, currentY, liveDistance, halfWidth, halfHeight, currentYaw;
|
|
xr_overlay_get_quad_extent(menuOverlay, ¤tX, ¤tY, &liveDistance, &halfWidth,
|
|
&halfHeight, ¤tYaw);
|
|
(void)currentX;
|
|
(void)liveDistance;
|
|
(void)halfWidth;
|
|
(void)halfHeight;
|
|
(void)currentYaw;
|
|
float distance = xr_overlay_get_default_distance_m(menuOverlay);
|
|
|
|
float newX = headLoc.pose.position.x + fx * distance;
|
|
float newZ = headLoc.pose.position.z + fz * distance;
|
|
// xr_overlay_set_pose()'s distance_m is simply the quad's base-space Z
|
|
// position negated (see xr_overlay_render_and_build_layer()) - not
|
|
// necessarily positive once the quad can end up anywhere the player
|
|
// has walked to, unlike every other overlay's fixed, always-in-front-
|
|
// of-the-origin spawn pose.
|
|
float newDistance = -newZ;
|
|
// See xr_overlay_render_and_build_layer()'s own rotation comment -
|
|
// solved so the quad's local front normal (0,0,1) ends up pointing
|
|
// along -forward, i.e. back at the player looking along +forward at it.
|
|
float newYawDeg = -atan2f(-fx, -fz) / QS_DEG_TO_RAD;
|
|
|
|
xr_overlay_set_pose(menuOverlay, newX, currentY, newDistance, newYawDeg);
|
|
}
|
|
|
|
// See xr_input.h's own comment. Chooses a new player transform such that
|
|
// the player's CURRENT real head pose maps to play-space position (0,0)
|
|
// facing (0,0,-1) - i.e. exactly reproducing this app's own startup
|
|
// framing (the fixed game quad dead ahead at its authored distance), just
|
|
// re-anchored to wherever the player is standing/facing right now instead
|
|
// of wherever their headset happened to be when the session first began.
|
|
// Deliberately locates the head against the REAL local space (via
|
|
// xr_session_get_local_space()), not the play space every other consumer
|
|
// in this file uses - computing a brand new transform from the player's
|
|
// real pose would be circular if measured through the very transform
|
|
// being replaced.
|
|
//
|
|
// Math: with V(p) = R_y(yaw_deg) * p + (x, z) defined as "real-space pose p
|
|
// maps to this play-space pose" (see xr_session_rebuild_play_space()'s own
|
|
// derivation, which builds the inverse of this same V), solving
|
|
// V(headPos) = (0,0) and V(headForward) = (0,0,-1) for (x, z, yaw_deg)
|
|
// gives the formulas below - verified by substitution that a
|
|
// player standing at the real origin facing the real default -Z (i.e. one
|
|
// who has never moved) yields (0, 0, 0), the same as this feature's
|
|
// original naive reset-to-identity behavior.
|
|
//
|
|
// keepFacing=true (Reset Position) re-anchors only the position: the
|
|
// transform's yaw is kept, so the game quad does not rotate to follow the
|
|
// gaze. Same formulas, with theta taken from the current yaw instead.
|
|
static bool xr_input_recenter(bool keepFacing) {
|
|
XrSpace localSpace = xr_session_get_local_space();
|
|
if (localSpace == XR_NULL_HANDLE || g_view_space == XR_NULL_HANDLE)
|
|
return false;
|
|
|
|
XrTime time = xr_session_get_predicted_display_time();
|
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
|
const XrSpaceLocationFlags needed =
|
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
|
if (!xr_check(xrLocateSpace(g_view_space, localSpace, time, &headLoc),
|
|
"xrLocateSpace(view, recenter to screen)") ||
|
|
(headLoc.locationFlags & needed) != needed)
|
|
return false;
|
|
|
|
float theta;
|
|
if (keepFacing) {
|
|
float curX, curZ, curYawDeg;
|
|
xr_session_get_player_transform(&curX, &curZ, &curYawDeg);
|
|
theta = curYawDeg * QS_DEG_TO_RAD;
|
|
} else {
|
|
float fx, fy, fz;
|
|
quat_rotate_vec(&headLoc.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
|
|
float len = sqrtf(fx * fx + fz * fz);
|
|
if (len < 1e-4f)
|
|
return false; // looking almost straight up/down - nothing sane to recenter onto
|
|
fx /= len;
|
|
fz /= len;
|
|
|
|
// angle(0,-1) - angle(fx,fz), using the same atan2f(x, z) "angle"
|
|
// convention as xr_overlay_render_and_build_layer()'s own rotation
|
|
// comment (not the more usual atan2f(y, x)).
|
|
theta = atan2f(0.0f, -1.0f) - atan2f(fx, fz);
|
|
}
|
|
float cosTheta = cosf(theta), sinTheta = sinf(theta);
|
|
float hx = headLoc.pose.position.x, hz = headLoc.pose.position.z;
|
|
float newX = -(hx * cosTheta + hz * sinTheta);
|
|
float newZ = hx * sinTheta - hz * cosTheta;
|
|
float newYawDeg = theta / QS_DEG_TO_RAD;
|
|
|
|
xr_session_set_player_transform(newX, newZ, newYawDeg);
|
|
return true;
|
|
}
|
|
|
|
bool xr_input_recenter_player_to_screen(void) { return xr_input_recenter(false); }
|
|
|
|
bool xr_input_reset_player_position(void) { return xr_input_recenter(true); }
|
|
|
|
void xr_input_skip_startup_recenter(void) { g_pending_startup_recenter = false; }
|
|
|
|
// sdl_events.c - the engine's own held-key table, indexed by its "SystemShock/Mac
|
|
// keycode" (see xr_input_sync_and_draw()'s watchdog).
|
|
extern unsigned char sshockKeyStates[256];
|
|
|
|
// See kKeyActions' own comment on why plain printable keys need
|
|
// SDL_TEXTINPUT in addition to SDL_KEYDOWN/UP (only jump needs Space
|
|
// specifically; every other plain-key action supplies its own character).
|
|
// Returns true only if SDL accepted the event.
|
|
static bool xr_input_push_text(char ch) {
|
|
SDL_Event textEvent;
|
|
SDL_zero(textEvent);
|
|
textEvent.type = SDL_TEXTINPUT;
|
|
textEvent.text.timestamp = SDL_GetTicks();
|
|
textEvent.text.windowID = 0;
|
|
textEvent.text.text[0] = ch;
|
|
textEvent.text.text[1] = '\0';
|
|
return SDL_PushEvent(&textEvent) == 1;
|
|
}
|
|
|
|
static bool xr_input_push_key(const QsKeyAction *keyAction, bool down) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = down ? SDL_KEYDOWN : SDL_KEYUP;
|
|
event.key.timestamp = SDL_GetTicks();
|
|
event.key.windowID = 0;
|
|
event.key.state = down ? SDL_PRESSED : SDL_RELEASED;
|
|
event.key.repeat = 0;
|
|
event.key.keysym.scancode = keyAction->scancode;
|
|
event.key.keysym.sym = keyAction->keycode;
|
|
event.key.keysym.mod = keyAction->mod;
|
|
return SDL_PushEvent(&event) == 1;
|
|
}
|
|
|
|
// Free view: while a free_{left,right}_thumbstick_view action's button is
|
|
// held (and the menu is closed), that hand's stick looks around instead of
|
|
// driving its normal bindings. It feeds the engine's own mouse look
|
|
// (mouselook.c): mlook_enabled is forced on for as long as a free view is
|
|
// held, and the stick adds to MouseChaosX/Y, the extra look velocity that
|
|
// get_mouselook_vel() (sdl_events.c) consumes once per physics tick.
|
|
// get_mouselook_vel() returns zero while SDL considers the cursor shown, so
|
|
// the cursor is kept hidden for the duration.
|
|
extern int mlook_enabled;
|
|
extern int qs_in_cyberspace(void); // engine patch 24 (mouselook.c)
|
|
extern unsigned char game_paused;
|
|
extern int MouseChaosX, MouseChaosY;
|
|
|
|
#define QS_FREE_VIEW_DEADZONE 0.15f
|
|
#define QS_FREE_VIEW_YAW_PER_SEC 500.0f
|
|
#define QS_FREE_VIEW_PITCH_PER_SEC 250.0f
|
|
|
|
static bool g_free_view_forced = false; // true while we have mlook_enabled forced on
|
|
static int g_free_view_saved_mlook = 0; // mlook_enabled before that
|
|
static float g_free_view_acc[2]; // fractional look counts not yet emitted
|
|
static int g_free_view_saved_cursor = 0; // SDL_ShowCursor() state before it
|
|
static Uint32 g_free_view_last_ticks = 0;
|
|
static Uint32 g_free_view_log_ticks = 0;
|
|
|
|
static void xr_input_apply_free_view(const bool active[2]) {
|
|
Uint32 now = SDL_GetTicks();
|
|
float dt = g_free_view_last_ticks ? (float)(now - g_free_view_last_ticks) / 1000.0f : 0.0f;
|
|
g_free_view_last_ticks = now;
|
|
if (dt > 0.1f)
|
|
dt = 0.1f;
|
|
|
|
if (!active[LEFT] && !active[RIGHT]) {
|
|
if (g_free_view_forced) {
|
|
mlook_enabled = g_free_view_saved_mlook;
|
|
SDL_ShowCursor(g_free_view_saved_cursor ? SDL_ENABLE : SDL_DISABLE);
|
|
g_free_view_forced = false;
|
|
g_free_view_acc[0] = g_free_view_acc[1] = 0.0f;
|
|
}
|
|
return;
|
|
}
|
|
if (!g_free_view_forced) {
|
|
g_free_view_saved_mlook = mlook_enabled;
|
|
g_free_view_saved_cursor = SDL_ShowCursor(SDL_QUERY);
|
|
g_free_view_forced = true;
|
|
}
|
|
mlook_enabled = 1;
|
|
SDL_ShowCursor(SDL_DISABLE);
|
|
if (game_paused)
|
|
return;
|
|
|
|
float x = 0.0f, y = 0.0f;
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
if (active[hand]) {
|
|
x += g_stick_xy[hand][0];
|
|
y += g_stick_xy[hand][1];
|
|
}
|
|
}
|
|
float mag = sqrtf(x * x + y * y);
|
|
if (mag > QS_FREE_VIEW_DEADZONE) {
|
|
float scaled = (mag - QS_FREE_VIEW_DEADZONE) / (1.0f - QS_FREE_VIEW_DEADZONE);
|
|
if (scaled > 1.0f)
|
|
scaled = 1.0f;
|
|
// Quadratic response: fine control near the centre, full rate at the edge.
|
|
float factor = scaled * scaled / mag;
|
|
x *= factor;
|
|
y *= factor;
|
|
} else {
|
|
x = y = 0.0f;
|
|
}
|
|
// Stick right = look right; stick up = look up (mouse motion up is negative y).
|
|
g_free_view_acc[0] += x * QS_FREE_VIEW_YAW_PER_SEC * dt;
|
|
g_free_view_acc[1] += -y * QS_FREE_VIEW_PITCH_PER_SEC * dt;
|
|
int dx = (int)g_free_view_acc[0];
|
|
int dy = (int)g_free_view_acc[1];
|
|
g_free_view_acc[0] -= (float)dx;
|
|
g_free_view_acc[1] -= (float)dy;
|
|
// Bounded so a held stick can't pile up unconsumed velocity when the
|
|
// engine isn't running physics (e.g. outside gameplay).
|
|
if (MouseChaosX > -4096 && MouseChaosX < 4096)
|
|
MouseChaosX += dx;
|
|
if (MouseChaosY > -4096 && MouseChaosY < 4096)
|
|
MouseChaosY += dy;
|
|
|
|
if (now - g_free_view_log_ticks > 500) {
|
|
g_free_view_log_ticks = now;
|
|
LOGI("XR: free view mlook=%d cursorShown=%d paused=%d stick=(%.2f,%.2f) chaos=(%d,%d)",
|
|
mlook_enabled, SDL_ShowCursor(SDL_QUERY), (int)game_paused, x, y, MouseChaosX,
|
|
MouseChaosY);
|
|
}
|
|
}
|
|
|
|
// Cyberspace pitch and bank are full-strength on/off keys in the engine. For an
|
|
// action bound to a thumbstick direction, engine patch 25 (movekeys.c) scales
|
|
// that strength by these globals, set every frame from how far the stick is
|
|
// pushed: MIN just past the digital threshold up to MAX at full deflection. A
|
|
// button-bound action keeps full strength.
|
|
extern float qs_cyber_pitch_scale;
|
|
extern float qs_cyber_bank_scale;
|
|
|
|
#define QS_CYBER_ROTATION_MIN_SCALE 0.2f
|
|
#define QS_CYBER_ROTATION_MAX_SCALE 0.6f
|
|
|
|
// Deflection (0..1) of the stick direction `raw` names, or -1 if `raw` is not a
|
|
// thumbstick direction. The 8 direction inputs are ordered up/down/left/right
|
|
// per hand.
|
|
static float xr_input_stick_direction_deflection(QsRawInput raw) {
|
|
if (!xr_input_raw_is_thumbstick_direction(raw))
|
|
return -1.0f;
|
|
const float *xy = g_stick_xy[xr_input_raw_input_hand(raw)];
|
|
float d;
|
|
switch ((raw - QS_RAW_LEFT_THUMBSTICK_UP) % 4) {
|
|
case 0: d = xy[1]; break;
|
|
case 1: d = -xy[1]; break;
|
|
case 2: d = -xy[0]; break;
|
|
default: d = xy[0]; break;
|
|
}
|
|
return d < 0.0f ? 0.0f : (d > 1.0f ? 1.0f : d);
|
|
}
|
|
|
|
static float xr_input_cyber_rotation_scale(QsRawInput raw) {
|
|
float d = xr_input_stick_direction_deflection(raw);
|
|
if (d < 0.0f)
|
|
return 1.0f;
|
|
float t = (d - 0.5f) / 0.5f;
|
|
t = t < 0.0f ? 0.0f : (t > 1.0f ? 1.0f : t);
|
|
return QS_CYBER_ROTATION_MIN_SCALE + (QS_CYBER_ROTATION_MAX_SCALE - QS_CYBER_ROTATION_MIN_SCALE) * t;
|
|
}
|
|
|
|
// Sets the two globals from the cyberspace pitch/bank actions currently held.
|
|
static void xr_input_update_cyber_scales(const bool wantDown[]) {
|
|
float pitchScale = 1.0f, bankScale = 1.0f;
|
|
bool havePitch = false, haveBank = false;
|
|
for (size_t i = 0; i < QS_NUM_KEY_ACTIONS; i++) {
|
|
QsControlAction a = kKeyActions[i].action;
|
|
bool pitch = a == QS_ACTION_CYBER_CLIMB || a == QS_ACTION_CYBER_DIVE ||
|
|
a == QS_ACTION_CYBER_CLIMB_LEFT || a == QS_ACTION_CYBER_CLIMB_RIGHT ||
|
|
a == QS_ACTION_CYBER_DIVE_LEFT || a == QS_ACTION_CYBER_DIVE_RIGHT;
|
|
bool bank = a == QS_ACTION_CYBER_BANK_LEFT || a == QS_ACTION_CYBER_BANK_RIGHT ||
|
|
a == QS_ACTION_CYBER_CLIMB_LEFT || a == QS_ACTION_CYBER_CLIMB_RIGHT ||
|
|
a == QS_ACTION_CYBER_DIVE_LEFT || a == QS_ACTION_CYBER_DIVE_RIGHT;
|
|
if (!wantDown[i] || (!pitch && !bank))
|
|
continue;
|
|
float scale = xr_input_cyber_rotation_scale(s_binding[a]);
|
|
if (pitch) {
|
|
pitchScale = havePitch ? fmaxf(pitchScale, scale) : scale;
|
|
havePitch = true;
|
|
}
|
|
if (bank) {
|
|
bankScale = haveBank ? fmaxf(bankScale, scale) : scale;
|
|
haveBank = true;
|
|
}
|
|
}
|
|
qs_cyber_pitch_scale = pitchScale;
|
|
qs_cyber_bank_scale = bankScale;
|
|
}
|
|
|
|
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
|
if (g_action_set == XR_NULL_HANDLE)
|
|
return;
|
|
|
|
XrActiveActionSet activeSet = {g_action_set, XR_NULL_PATH};
|
|
XrActionsSyncInfo syncInfo = {XR_TYPE_ACTIONS_SYNC_INFO};
|
|
syncInfo.countActiveActionSets = 1;
|
|
syncInfo.activeActionSets = &activeSet;
|
|
xr_check(xrSyncActions(g_session, &syncInfo), "xrSyncActions");
|
|
|
|
// Cheap (no query on the runtime side, just a cached path lookup) -
|
|
// refreshed every frame so MenuOverlay's Controller Options tab always
|
|
// reflects whichever controller is actually connected right now.
|
|
XrInteractionProfileState profileState = {XR_TYPE_INTERACTION_PROFILE_STATE};
|
|
if (xr_check(xrGetCurrentInteractionProfile(g_session, g_hand_path[LEFT], &profileState),
|
|
"xrGetCurrentInteractionProfile") &&
|
|
profileState.interactionProfile != XR_NULL_PATH) {
|
|
uint32_t len = 0;
|
|
xrPathToString(g_instance, profileState.interactionProfile,
|
|
sizeof(g_active_profile_path), &len, g_active_profile_path);
|
|
} else {
|
|
g_active_profile_path[0] = '\0';
|
|
}
|
|
|
|
// See g_pending_startup_recenter's own comment - retried every frame
|
|
// (cheap: one xrLocateSpace call) until head tracking actually has a
|
|
// valid pose to recenter onto, typically the very first frame.
|
|
if (g_pending_startup_recenter && xr_input_recenter_player_to_screen())
|
|
g_pending_startup_recenter = false;
|
|
|
|
XrOverlay *menuOverlay = xr_session_get_menu_overlay();
|
|
XrOverlay *keyboardOverlay = xr_session_get_keyboard_overlay();
|
|
bool menuVisible = xr_overlay_is_visible(menuOverlay);
|
|
bool keyboardVisible = xr_overlay_is_visible(keyboardOverlay);
|
|
if (menuVisible != g_menu_was_visible) {
|
|
g_menu_was_visible = menuVisible;
|
|
qs_config_notify_menu_visibility(menuVisible);
|
|
}
|
|
|
|
// Remappable controls: refresh every raw input's state, let an active
|
|
// capture consume a fresh press first, then dispatch the key-injecting
|
|
// actions. An action bound to a thumbstick direction is suppressed while
|
|
// the menu is open, since that stick drives the menu's snap-turn/smooth-
|
|
// move. mouse_click/mouse_double_click are resolved per hand inside the
|
|
// ray-dispatch loop below.
|
|
xr_input_update_raw_states();
|
|
xr_input_process_capture();
|
|
// A free-view action's button held: that hand's stick looks around and
|
|
// its own direction bindings are suspended (see xr_input_apply_free_view()).
|
|
bool freeView[2];
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
QsControlAction viewAction =
|
|
hand == LEFT ? QS_ACTION_FREE_LEFT_THUMBSTICK_VIEW : QS_ACTION_FREE_RIGHT_THUMBSTICK_VIEW;
|
|
QsRawInput viewRaw = s_binding[viewAction];
|
|
freeView[hand] = viewRaw < QS_RAW_CONCRETE_COUNT && !menuVisible &&
|
|
!xr_input_action_suppressed_by_capture(viewAction) && g_raw_down[viewRaw];
|
|
}
|
|
bool inCyber = qs_in_cyberspace() != 0;
|
|
bool wantDown[QS_NUM_KEY_ACTIONS];
|
|
for (size_t i = 0; i < QS_NUM_KEY_ACTIONS; i++) {
|
|
const QsKeyAction *keyAction = &kKeyActions[i];
|
|
QsRawInput raw = s_binding[keyAction->action];
|
|
QsActionContext context = xr_input_action_context(keyAction->action);
|
|
bool contextActive = context == QS_CONTEXT_ALWAYS ||
|
|
(context == QS_CONTEXT_CYBER) == inCyber;
|
|
wantDown[i] = raw != QS_RAW_NONE && contextActive &&
|
|
!xr_input_action_suppressed_by_capture(keyAction->action) &&
|
|
!(xr_input_raw_is_thumbstick_direction(raw) &&
|
|
(menuVisible || freeView[xr_input_raw_input_hand(raw)])) &&
|
|
g_raw_down[raw];
|
|
}
|
|
xr_input_update_cyber_scales(wantDown);
|
|
xr_input_apply_free_view(freeView);
|
|
for (size_t i = 0; i < QS_NUM_KEY_ACTIONS; i++) {
|
|
const QsKeyAction *keyAction = &kKeyActions[i];
|
|
bool down = wantDown[i];
|
|
if (down == g_key_action_prev[i])
|
|
continue;
|
|
// g_key_action_prev[i] is only advanced once every event this edge
|
|
// needs was actually accepted by SDL - a rejected push is retried
|
|
// next frame rather than leaving this action believing it already
|
|
// sent a press/release the engine never saw.
|
|
if (down) {
|
|
if (keyAction->needsTextInput && !xr_input_push_text(keyAction->textChar))
|
|
continue;
|
|
if (!xr_input_push_key(keyAction, true))
|
|
continue;
|
|
} else {
|
|
// The engine tracks held keys per key (sdl_events.c's
|
|
// sshockKeyStates[]), not per action - e.g. turn_left and
|
|
// strafe_left are both the Left arrow, differing only in
|
|
// modifier. A release for one must not clear the key while
|
|
// another action sharing it is still held.
|
|
int sibling = -1;
|
|
for (size_t j = 0; j < QS_NUM_KEY_ACTIONS; j++) {
|
|
if (j != i && wantDown[j] && kKeyActions[j].keycode == keyAction->keycode) {
|
|
sibling = (int)j;
|
|
break;
|
|
}
|
|
}
|
|
if (sibling >= 0) {
|
|
// Restores the still-held sibling's own modifiers if it
|
|
// was already down (a sibling only pressed this frame
|
|
// sends its own KEYDOWN when the loop reaches it).
|
|
if (g_key_action_prev[sibling] && !xr_input_push_key(&kKeyActions[sibling], true))
|
|
continue;
|
|
} else if (!xr_input_push_key(keyAction, false)) {
|
|
continue;
|
|
}
|
|
}
|
|
LOGI("XR: key action %s %s", kActionNames[keyAction->action], down ? "DOWN" : "UP");
|
|
g_key_action_prev[i] = down;
|
|
}
|
|
|
|
// Watchdog for the continuously-polled movement keys: if the engine
|
|
// still has one held (sshockKeyStates[], sdl_events.c) while none of our
|
|
// actions is holding it, its release never took effect - the character
|
|
// would keep walking/turning until that key is next pressed and
|
|
// released. The grace period covers the normal 1-2 frame lag between
|
|
// pushing a release and the engine's next pump_events(). Skipped while
|
|
// the on-screen keyboard is up, since its own arrow/space keys (real
|
|
// SDL key events, held for as long as the trigger is) legitimately
|
|
// hold these too.
|
|
static const struct {
|
|
SDL_Keycode keycode;
|
|
Uint8 engineCode; // sdlKeyCodeToSSHOCKkeyCode()'s result (movekeys.h's CODE_*)
|
|
} kWatchedKeys[] = {
|
|
{SDLK_UP, 0x7E}, {SDLK_DOWN, 0x7D}, {SDLK_LEFT, 0x7B}, {SDLK_RIGHT, 0x7C}, {SDLK_SPACE, 0x31},
|
|
{SDLK_RETURN, 0x24},
|
|
// Cyberspace movement keys (letters and keypad, movekeys.h's CODE_*).
|
|
{SDLK_w, 0x0D}, {SDLK_a, 0x00}, {SDLK_s, 0x01}, {SDLK_d, 0x02}, {SDLK_q, 0x0C},
|
|
{SDLK_e, 0x0E}, {SDLK_x, 0x07}, {SDLK_KP_7, 0x59}, {SDLK_KP_9, 0x5C}, {SDLK_KP_1, 0x53},
|
|
{SDLK_KP_3, 0x55},
|
|
};
|
|
static Uint32 s_stuck_since[sizeof(kWatchedKeys) / sizeof(kWatchedKeys[0])];
|
|
for (size_t w = 0; w < sizeof(kWatchedKeys) / sizeof(kWatchedKeys[0]); w++) {
|
|
const QsKeyAction *releaseWith = NULL;
|
|
bool heldByUs = false;
|
|
for (size_t i = 0; i < QS_NUM_KEY_ACTIONS; i++) {
|
|
if (kKeyActions[i].keycode != kWatchedKeys[w].keycode)
|
|
continue;
|
|
if (!releaseWith)
|
|
releaseWith = &kKeyActions[i];
|
|
if (g_key_action_prev[i])
|
|
heldByUs = true;
|
|
}
|
|
if (!releaseWith || heldByUs || keyboardVisible || sshockKeyStates[kWatchedKeys[w].engineCode] == 0) {
|
|
s_stuck_since[w] = 0;
|
|
continue;
|
|
}
|
|
Uint32 now = SDL_GetTicks();
|
|
if (s_stuck_since[w] == 0) {
|
|
s_stuck_since[w] = now;
|
|
} else if (now - s_stuck_since[w] > 200) {
|
|
LOGI("XR: key watchdog releasing stuck engine key 0x%02x", kWatchedKeys[w].engineCode);
|
|
xr_input_push_key(releaseWith, false);
|
|
s_stuck_since[w] = now;
|
|
}
|
|
}
|
|
|
|
// A drag can't get stuck active across a hide/reopen - e.g. Close
|
|
// being hit on one hand mid-drag on the other, or the session ending.
|
|
if (!keyboardVisible) {
|
|
g_keyboard_drag.dragging = false;
|
|
g_keyboard_drag.hand = -1;
|
|
}
|
|
if (!menuVisible) {
|
|
g_menu_drag.dragging = false;
|
|
g_menu_drag.hand = -1;
|
|
}
|
|
|
|
// A hand only ever gets a laser-beam quad this frame if it actually
|
|
// claims the keyboard or menu overlay's ray below (see the
|
|
// xr_input_build_beam() calls) - reset here so a hand that doesn't
|
|
// claim one this frame doesn't keep showing last frame's beam.
|
|
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
|
|
|
|
// The billboard math needs an approximate head position - only bother
|
|
// locating it on frames where a beam could possibly be drawn at all
|
|
// (i.e. any drawn frame - the game quad can claim a beam on its own
|
|
// even with both overlays closed).
|
|
bool needBeams = draw;
|
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
|
bool haveHead = false;
|
|
if (needBeams) {
|
|
const XrSpaceLocationFlags neededHead =
|
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
|
haveHead = xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
|
|
"xrLocateSpace(view)") &&
|
|
(headLoc.locationFlags & neededHead) == neededHead;
|
|
}
|
|
|
|
// Fed to xr_overlay_update_cursor() after the loop below - whichever
|
|
// hand's ray hits a given overlay last wins if both do, good enough
|
|
// for a single on-quad cursor per overlay (only ever updated when hit
|
|
// is true, so a later hand that misses doesn't hide an earlier hand's
|
|
// hit).
|
|
bool menuCursorHit = false, keyboardCursorHit = false;
|
|
float menuCursorU = 0.0f, menuCursorV = 0.0f;
|
|
float keyboardCursorU = 0.0f, keyboardCursorV = 0.0f;
|
|
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
// The click signal shared by keyboard, menu and game-quad dispatch,
|
|
// resolved once per hand: while the menu is open it is the trigger
|
|
// (fixed, so the menu is always operable whatever mouse_click is
|
|
// bound to); otherwise it is mouse_click's binding, so an on-screen
|
|
// keyboard press uses the same click as the game.
|
|
bool selectDown;
|
|
if (menuVisible) {
|
|
XrActionStateGetInfo selectInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
selectInfo.action = g_select_click_action;
|
|
selectInfo.subactionPath = g_hand_path[hand];
|
|
XrActionStateBoolean selectState = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
|
xrGetActionStateBoolean(g_session, &selectInfo, &selectState);
|
|
selectDown = selectState.isActive && selectState.currentState;
|
|
} else {
|
|
selectDown = !xr_input_action_suppressed_by_capture(QS_ACTION_MOUSE_CLICK) &&
|
|
xr_input_raw_state_for_hand(s_binding[QS_ACTION_MOUSE_CLICK], hand);
|
|
}
|
|
bool selectDownEdge = selectDown && !g_prev_select[hand];
|
|
bool selectUpEdge = !selectDown && g_prev_select[hand];
|
|
if (selectDown != g_prev_select[hand]) {
|
|
LOGI("XR: %s select %s", kHandName[hand], selectDown ? "DOWN" : "UP");
|
|
g_prev_select[hand] = selectDown;
|
|
}
|
|
|
|
// mouse_double_click - game-quad-only (see its own dispatch further
|
|
// down) and only while the menu is closed, matching the reasoning
|
|
// above (double-click is a gameplay interaction primitive, not a
|
|
// menu one - keyboard keys have no double-click semantic either).
|
|
bool doubleClickEdge = false;
|
|
if (!menuVisible && !xr_input_action_suppressed_by_capture(QS_ACTION_MOUSE_DOUBLE_CLICK)) {
|
|
bool doubleClickDown =
|
|
xr_input_raw_state_for_hand(s_binding[QS_ACTION_MOUSE_DOUBLE_CLICK], hand);
|
|
doubleClickEdge = doubleClickDown && !g_prev_double_click[hand];
|
|
g_prev_double_click[hand] = doubleClickDown;
|
|
} else {
|
|
g_prev_double_click[hand] = false;
|
|
}
|
|
|
|
if (hand == LEFT) {
|
|
XrActionStateGetInfo menuInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
menuInfo.action = g_menu_toggle_action;
|
|
menuInfo.subactionPath = g_hand_path[LEFT];
|
|
XrActionStateBoolean menuState = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
|
xrGetActionStateBoolean(g_session, &menuInfo, &menuState);
|
|
bool menuDown = menuState.isActive && menuState.currentState;
|
|
if (menuDown && !g_prev_menu) {
|
|
LOGI("XR: menu-toggle button DOWN");
|
|
// Only ever affects the menu launcher - the keyboard is
|
|
// fully independent (its own Close button is the only way
|
|
// to hide it, see KeyboardOverlay.java).
|
|
if (menuOverlay != NULL) {
|
|
bool wasHidden = !menuVisible;
|
|
xr_overlay_toggle_visible(menuOverlay);
|
|
g_menu_prev_hit[LEFT] = g_menu_prev_hit[RIGHT] = false;
|
|
g_menu_touch_active[LEFT] = g_menu_touch_active[RIGHT] = false;
|
|
if (wasHidden && xr_overlay_is_visible(menuOverlay)) {
|
|
xr_input_recenter_menu(menuOverlay, baseSpace, time);
|
|
} else if (!wasHidden && !xr_overlay_is_visible(menuOverlay) && g_capture_active) {
|
|
// The menu closed with a capture pending: cancel it and
|
|
// notify MenuOverlay, whose capturingActionIndex would
|
|
// otherwise stay set.
|
|
int cancelledAction = (int)g_capture_action;
|
|
g_capture_active = false;
|
|
qs_config_notify_control_capture_result(cancelledAction, NULL, -1, NULL);
|
|
}
|
|
}
|
|
} else if (!menuDown && g_prev_menu) {
|
|
LOGI("XR: menu-toggle button UP");
|
|
}
|
|
g_prev_menu = menuDown;
|
|
|
|
// Snap turn - only while the menu is open (per the feature's
|
|
// own design: repositioning is for lining yourself up with the
|
|
// game quad from the menu, not a general gameplay locomotion
|
|
// scheme). g_turn_ready is a hysteresis latch (see its own
|
|
// declaration comment) so holding the stick past the threshold
|
|
// turns exactly once, not once per frame - it only re-arms once
|
|
// the stick has come back near center. Also suppressed during a
|
|
// Controller Options capture, so a stick pressed then doesn't
|
|
// also snap-turn the view.
|
|
if (menuVisible && !g_capture_active && !g_controller_options_tab_open) {
|
|
XrActionStateGetInfo turnInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
turnInfo.action = g_thumbstick_action;
|
|
turnInfo.subactionPath = g_hand_path[LEFT];
|
|
XrActionStateVector2f turnState = {XR_TYPE_ACTION_STATE_VECTOR2F};
|
|
xrGetActionStateVector2f(g_session, &turnInfo, &turnState);
|
|
if (turnState.isActive) {
|
|
const float kSnapThreshold = 0.7f;
|
|
const float kSnapRearm = 0.3f;
|
|
const float kSnapDegrees = 30.0f;
|
|
float x = turnState.currentState.x;
|
|
if (g_turn_ready && fabsf(x) > kSnapThreshold) {
|
|
float px, pz, pyaw;
|
|
xr_session_get_player_transform(&px, &pz, &pyaw);
|
|
// Confirmed backwards on-device from the initial
|
|
// (unverifiable-without-a-headset) guess - flipped.
|
|
pyaw += (x > 0.0f) ? -kSnapDegrees : kSnapDegrees;
|
|
xr_session_set_player_transform(px, pz, pyaw);
|
|
g_turn_ready = false;
|
|
} else if (fabsf(x) < kSnapRearm) {
|
|
g_turn_ready = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hand == RIGHT) {
|
|
// Smooth move - only while the menu is open, with the same
|
|
// suppression as the snap turn above. g_last_move_ticks is
|
|
// refreshed every frame so dt is always the time since the
|
|
// previous frame.
|
|
Uint32 nowTicks = SDL_GetTicks();
|
|
Uint32 lastMoveTicks = g_last_move_ticks;
|
|
g_last_move_ticks = nowTicks;
|
|
if (menuVisible && !g_capture_active && !g_controller_options_tab_open &&
|
|
lastMoveTicks != 0) {
|
|
XrActionStateGetInfo moveInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
moveInfo.action = g_thumbstick_action;
|
|
moveInfo.subactionPath = g_hand_path[RIGHT];
|
|
XrActionStateVector2f moveState = {XR_TYPE_ACTION_STATE_VECTOR2F};
|
|
xrGetActionStateVector2f(g_session, &moveInfo, &moveState);
|
|
const float kMoveDeadzone = 0.15f;
|
|
float mx = moveState.currentState.x, my = moveState.currentState.y;
|
|
float mag = sqrtf(mx * mx + my * my);
|
|
if (moveState.isActive && mag > kMoveDeadzone) {
|
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
|
const XrSpaceLocationFlags neededHead2 =
|
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
|
if (xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
|
|
"xrLocateSpace(view, move)") &&
|
|
(headLoc.locationFlags & neededHead2) == neededHead2) {
|
|
float hfx, hfy, hfz;
|
|
quat_rotate_vec(&headLoc.pose.orientation, 0.0f, 0.0f, -1.0f, &hfx, &hfy,
|
|
&hfz);
|
|
float horizLen = sqrtf(hfx * hfx + hfz * hfz);
|
|
if (horizLen > 1e-4f) {
|
|
hfx /= horizLen;
|
|
hfz /= horizLen;
|
|
// "Right" relative to forward, in the horizontal
|
|
// plane - R_y(-90deg) applied to (hfx, hfz), see
|
|
// xr_overlay.c's own rotation-matrix comment for
|
|
// the same R_y() convention.
|
|
float rfx = -hfz, rfz = hfx;
|
|
const float kMoveSpeedMetersPerSec = 1.2f;
|
|
float dt = (float)(nowTicks - lastMoveTicks) / 1000.0f;
|
|
float moveX = (hfx * my + rfx * mx) * kMoveSpeedMetersPerSec * dt;
|
|
float moveZ = (hfz * my + rfz * mx) * kMoveSpeedMetersPerSec * dt;
|
|
float px, pz, pyaw;
|
|
xr_session_get_player_transform(&px, &pz, &pyaw);
|
|
// Moves the play space opposite the player's
|
|
// intended travel, so quads (fixed in play
|
|
// space) get closer/farther as the player
|
|
// virtually walks toward/away from them - see
|
|
// xr_session_set_player_transform()'s own
|
|
// comment. Confirmed backwards on-device from
|
|
// the initial (unverifiable-without-a-headset)
|
|
// guess - flipped.
|
|
xr_session_set_player_transform(px + moveX, pz + moveZ, pyaw);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
|
poseInfo.action = g_aim_pose_action;
|
|
poseInfo.subactionPath = g_hand_path[hand];
|
|
XrActionStatePose poseState = {XR_TYPE_ACTION_STATE_POSE};
|
|
xrGetActionStatePose(g_session, &poseInfo, &poseState);
|
|
if (!poseState.isActive)
|
|
continue;
|
|
|
|
XrSpaceLocation location = {XR_TYPE_SPACE_LOCATION};
|
|
if (!xr_check(xrLocateSpace(g_aim_space[hand], baseSpace, time, &location), "xrLocateSpace"))
|
|
continue;
|
|
const XrSpaceLocationFlags needed =
|
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
|
if ((location.locationFlags & needed) != needed)
|
|
continue;
|
|
|
|
float fx, fy, fz;
|
|
quat_rotate_vec(&location.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
|
|
|
|
// Keyboard first (it's the more likely target while it's up), then
|
|
// the menu launcher, then - only once neither actually claims the
|
|
// ray this frame (not merely "isn't visible" - see
|
|
// xr_input_try_overlay()'s doc comment) - the game quad itself.
|
|
// Whichever target claims the ray gets a laser-beam quad built for
|
|
// it (if a head pose is available and the ray actually crosses
|
|
// that target's plane) - see xr_input_build_beam(). Keyboard/menu
|
|
// are additionally gated on !g_game_touch_active[hand]: a pending
|
|
// mouse-down on the game quad (trigger still held since a
|
|
// down-edge dispatched there) must keep claiming the ray even if
|
|
// it strays onto another panel before release, the cross-target
|
|
// analogue of xr_input_try_overlay()'s own hadTouch/hadDrag - the
|
|
// eventual mouse-up has to reach the game quad, not whatever the
|
|
// ray happens to be over on the release frame.
|
|
bool keyboardPlaneHit = false;
|
|
float keyboardPlaneDistance = 0.0f;
|
|
if (!g_game_touch_active[hand] &&
|
|
xr_input_try_overlay(keyboardOverlay, KEYBOARD_TITLE_BAR_V_FRACTION,
|
|
KEYBOARD_CLOSE_BUTTON_U_FRACTION, &g_keyboard_drag, hand,
|
|
&location, fx, fy, fz, selectDownEdge, selectUpEdge,
|
|
&g_keyboard_touch_active[hand], &g_keyboard_prev_hit[hand],
|
|
"keyboard", &keyboardCursorU, &keyboardCursorV,
|
|
&keyboardCursorHit, &keyboardPlaneHit, &keyboardPlaneDistance)) {
|
|
if (haveHead && keyboardPlaneHit)
|
|
xr_input_build_beam(hand, &location, fx, fy, fz,
|
|
fminf(keyboardPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
|
|
continue;
|
|
}
|
|
bool menuPlaneHit = false;
|
|
float menuPlaneDistance = 0.0f;
|
|
if (!g_game_touch_active[hand] &&
|
|
xr_input_try_overlay(menuOverlay, MENU_TITLE_BAR_V_FRACTION,
|
|
MENU_DRAG_HANDLE_U_FRACTION, &g_menu_drag, hand, &location, fx,
|
|
fy, fz, selectDownEdge, selectUpEdge, &g_menu_touch_active[hand],
|
|
&g_menu_prev_hit[hand], "menu", &menuCursorU, &menuCursorV,
|
|
&menuCursorHit, &menuPlaneHit, &menuPlaneDistance)) {
|
|
if (haveHead && menuPlaneHit)
|
|
xr_input_build_beam(hand, &location, fx, fy, fz,
|
|
fminf(menuPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
|
|
continue;
|
|
}
|
|
|
|
// Lowest priority (for a fresh hit): the game quad itself - drives
|
|
// the engine's own mouse cursor/clicks via xr_mouse.h, exactly
|
|
// like a desktop mouse (absolute position + left button), plus the
|
|
// same laser-beam visual the other two targets get.
|
|
bool gamePlaneHit = false;
|
|
float gamePlaneDistance = 0.0f, gameU = 0.0f, gameV = 0.0f;
|
|
bool gameHit = xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit,
|
|
&gamePlaneDistance, &gameU, &gameV);
|
|
if (gameHit) {
|
|
int gameWidth, gameHeight;
|
|
xr_get_game_resolution(&gameWidth, &gameHeight);
|
|
int px = (int)(gameU * (float)gameWidth);
|
|
int py = (int)(gameV * (float)gameHeight);
|
|
if (px < 0)
|
|
px = 0;
|
|
else if (px >= gameWidth)
|
|
px = gameWidth - 1;
|
|
if (py < 0)
|
|
py = 0;
|
|
else if (py >= gameHeight)
|
|
py = gameHeight - 1;
|
|
// Mouse look keeps the engine's cursor centred; don't fight it.
|
|
if (!freeView[LEFT] && !freeView[RIGHT])
|
|
xr_mouse_move(px, py);
|
|
}
|
|
if (selectDownEdge && gameHit) {
|
|
xr_mouse_click(true);
|
|
g_game_touch_active[hand] = true;
|
|
} else if (selectUpEdge && g_game_touch_active[hand]) {
|
|
xr_mouse_click(false);
|
|
g_game_touch_active[hand] = false;
|
|
}
|
|
if (doubleClickEdge && gameHit) {
|
|
// Two complete click-down/up pairs, same frame - the engine's
|
|
// own double-click detection (input.c) compares two MOUSE_LUP
|
|
// events' timing/position, so this is indistinguishable from a
|
|
// fast real double-click, no engine change needed.
|
|
xr_mouse_click(true);
|
|
xr_mouse_click(false);
|
|
xr_mouse_click(true);
|
|
xr_mouse_click(false);
|
|
}
|
|
// gameHit implies gamePlaneHit (both only ever set together above),
|
|
// kept as a separate out-param for symmetry with
|
|
// xr_input_try_overlay()'s outPlaneHit/outPlaneDistance pair.
|
|
if (haveHead && gameHit)
|
|
xr_input_build_beam(hand, &location, fx, fy, fz,
|
|
fminf(gamePlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
|
|
}
|
|
|
|
if (menuVisible)
|
|
xr_overlay_update_cursor(menuOverlay, menuCursorU, menuCursorV, menuCursorHit);
|
|
if (keyboardVisible)
|
|
xr_overlay_update_cursor(keyboardOverlay, keyboardCursorU, keyboardCursorV,
|
|
keyboardCursorHit);
|
|
|
|
// Snapshot for next frame's edge detection - after everything above
|
|
// (capture-mode scan, key-action dispatch, mouse_click/double_click
|
|
// resolution) has finished comparing this frame's g_raw_down against
|
|
// the previous frame's values.
|
|
memcpy(g_raw_prev, g_raw_down, sizeof(g_raw_down));
|
|
}
|
|
|
|
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad) {
|
|
if (hand < 0 || hand > 1 || !g_beam_quad_valid[hand])
|
|
return false;
|
|
*out_quad = g_beam_quad[hand];
|
|
return true;
|
|
}
|
|
|
|
void xr_input_shutdown(void) {
|
|
if (g_free_view_forced) {
|
|
mlook_enabled = g_free_view_saved_mlook;
|
|
SDL_ShowCursor(g_free_view_saved_cursor ? SDL_ENABLE : SDL_DISABLE);
|
|
g_free_view_forced = false;
|
|
}
|
|
// Clearing g_key_action_prev below without releasing what it says is
|
|
// held would leave the engine's own held-key state stuck on (nothing
|
|
// else ever sends that key's release).
|
|
for (size_t i = 0; i < QS_NUM_KEY_ACTIONS; i++) {
|
|
if (g_key_action_prev[i])
|
|
xr_input_push_key(&kKeyActions[i], false);
|
|
}
|
|
for (int hand = 0; hand < 2; hand++) {
|
|
if (g_aim_space[hand] != XR_NULL_HANDLE)
|
|
xrDestroySpace(g_aim_space[hand]);
|
|
g_aim_space[hand] = XR_NULL_HANDLE;
|
|
xr_swapchain_destroy(&g_beam_swapchain[hand]);
|
|
}
|
|
if (g_view_space != XR_NULL_HANDLE)
|
|
xrDestroySpace(g_view_space);
|
|
g_view_space = XR_NULL_HANDLE;
|
|
if (g_action_set != XR_NULL_HANDLE)
|
|
xrDestroyActionSet(g_action_set);
|
|
g_action_set = XR_NULL_HANDLE;
|
|
g_aim_pose_action = XR_NULL_HANDLE;
|
|
g_select_click_action = XR_NULL_HANDLE;
|
|
g_menu_toggle_action = XR_NULL_HANDLE;
|
|
g_a_click_action = XR_NULL_HANDLE;
|
|
g_b_click_action = XR_NULL_HANDLE;
|
|
g_x_click_action = XR_NULL_HANDLE;
|
|
g_y_click_action = XR_NULL_HANDLE;
|
|
g_squeeze_action = XR_NULL_HANDLE;
|
|
g_thumbstick_click_action = XR_NULL_HANDLE;
|
|
g_thumbstick_action = XR_NULL_HANDLE;
|
|
|
|
g_instance = XR_NULL_HANDLE;
|
|
g_session = XR_NULL_HANDLE;
|
|
memset(g_prev_select, 0, sizeof(g_prev_select));
|
|
memset(g_prev_double_click, 0, sizeof(g_prev_double_click));
|
|
memset(g_key_action_prev, 0, sizeof(g_key_action_prev));
|
|
memset(g_raw_down, 0, sizeof(g_raw_down));
|
|
memset(g_raw_prev, 0, sizeof(g_raw_prev));
|
|
memset(g_stick_digital, 0, sizeof(g_stick_digital));
|
|
g_capture_active = false;
|
|
memset(g_game_prev_hit, 0, sizeof(g_game_prev_hit));
|
|
memset(g_game_touch_active, 0, sizeof(g_game_touch_active));
|
|
memset(g_menu_prev_hit, 0, sizeof(g_menu_prev_hit));
|
|
memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
|
|
memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit));
|
|
memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active));
|
|
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
|
|
g_prev_menu = false;
|
|
g_keyboard_drag.dragging = false;
|
|
g_keyboard_drag.hand = -1;
|
|
g_menu_drag.dragging = false;
|
|
g_menu_drag.hand = -1;
|
|
g_turn_ready = true;
|
|
g_last_move_ticks = 0;
|
|
g_pending_startup_recenter = true;
|
|
}
|