diff --git a/CONFIG.md b/CONFIG.md index faab213..0511c0e 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -79,7 +79,7 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`. "mouse_double_click": "right_b", "stand": "right_thumbstick_up", "prone": "right_thumbstick_down", - "toggle_freelook": "right_thumbstick_click", + "map_view": "right_thumbstick_click", "fire_weapon": "right_trigger" } } @@ -217,10 +217,10 @@ Which way the player is virtually facing, in degrees. ## Player Controls (`player_controls`) -Maps each of 45 in-game actions to a physical controller input: 9 -movement/click actions plus 36 for the engine's own keyboard hotkeys and -firing (posture, save/load, weapon select, MFD panels, etc. - see the hotkey -table below). Some actions are bound by default (see the default-input +Maps each of 57 in-game actions to a physical controller input: 9 +movement/click actions, 2 free-view actions, 11 cyberspace actions, and 35 +for the engine's own keyboard hotkeys and firing (posture, save/load, weapon +select, MFD panels, etc. - see the hotkey table below). Some actions are bound by default (see the default-input columns); the rest are unbound and can be bound on demand. Assign them in the **Controller Options** menu tab: tap an action's binding button, then press the button or stick direction to use ("Press a button…"); tap it again, or @@ -258,6 +258,8 @@ wait 8 seconds, to cancel without changing it. | `jump` | `left_thumbstick_click` | Jump | | `mouse_click` | `right_a` | Click - operates the keyboard/game quad's cursor, whichever the pointing hand's ray is over | | `mouse_double_click` | `right_b` | Double-click - game quad only (e.g. using an inventory item); has no effect over the menu or keyboard | +| `free_left_thumbstick_view` | `left_squeeze` | Hold to look around with the left thumbstick (see below) | +| `free_right_thumbstick_view` | `none` | Hold to look around with the right thumbstick (see below) | Movement actions bound to a thumbstick direction are suppressed while the VR menu is open, since the same physical stick already snap-turns/smooth- @@ -265,6 +267,45 @@ moves the player in that state (see `player_position` above) - this is the one case where a binding's effect depends on whether the menu is open, and it only ever affects the 8 thumbstick-direction inputs below. +**Free view** (`free_left_thumbstick_view`, `free_right_thumbstick_view`) is +the engine's mouse look driven by a thumbstick. While the bound input is held, +that stick looks around instead of doing its normal job: left/right turns the +view, up/down looks up and down, in proportion to how far the stick is pushed. +The stick's own direction bindings (move, strafe, turn, stand/prone) are +suspended while it is held, and the cursor stays centred in the view. Release +the input to return to normal. It has no effect while the VR menu is open. + +### Cyberspace actions + +Cyberspace has its own set of movement actions, active only while the player +is in cyberspace. While in cyberspace the normal movement actions +(`move_*`, `strafe_*`, `turn_*`, `jump`, `stand`, `crouch`, `prone`) are +inactive, and the cyberspace actions are inactive everywhere else. Each set +has its own bindings, so one input can be bound in both (for example the left +stick up is `move_forward` on foot and `cyber_climb` in cyberspace); +the Controller Options tab only reports a conflict between actions that can +be active at the same time. Free view, clicking and firing work in both. + +Pitch and bank (`cyber_climb`, `cyber_dive`, `cyber_bank_*` and the four +diagonals) are proportional when bound to a thumbstick direction: pushing the +stick further turns faster, from 20% of full speed just past the trigger point +to 60% at full deflection. Bound to a button they run at full speed. Thrust and +roll are always full speed. + +| Action key | Default input | Keyboard equivalent | Effect | +|---|---|---|---| +| `cyber_thrust` | `right_thumbstick_up` | `s` | Thrust forward | +| `cyber_climb` | `left_thumbstick_up` | `w` | Pitch up | +| `cyber_dive` | `left_thumbstick_down` | `x` | Pitch down | +| `cyber_bank_left` | `left_thumbstick_left` | `a` | Bank left | +| `cyber_bank_right` | `left_thumbstick_right` | `d` | Bank right | +| `cyber_roll_left` | `right_thumbstick_left` | `e` | Roll left | +| `cyber_roll_right` | `right_thumbstick_right` | `q` | Roll right | +| `cyber_climb_left` | `none` | Keypad 7 | Pitch up and bank left | +| `cyber_climb_right` | `none` | Keypad 9 | Pitch up and bank right | +| `cyber_dive_left` | `none` | Keypad 1 | Pitch down and bank left | +| `cyber_dive_right` | `none` | Keypad 3 | Pitch down and bank right | + `mouse_click` additionally behaves differently depending on the menu: while the VR menu is open, its "click" is always whichever hand's trigger is used to interact with the menu (so you can never lock yourself out of @@ -277,8 +318,8 @@ bind the `trigger` identifier (see below) for either hand to click. ### Hotkey actions The engine's own keyboard hotkeys (`HotKeyLookup[]` in `Prefs.c`) plus -firing, as 36 bindable actions. `stand` (`right_thumbstick_up`), `prone` -(`right_thumbstick_down`), `toggle_freelook` (`right_thumbstick_click`) and +firing, as 35 bindable actions. `stand` (`right_thumbstick_up`), `prone` +(`right_thumbstick_down`), `map_view` (`right_thumbstick_click`) and `fire_weapon` (`right_trigger`) are bound by default; the rest are unbound (`none`). Each fires once per press, like pressing the key on a real keyboard (there is no hold behavior). @@ -288,7 +329,6 @@ keyboard (there is no hold behavior). | `stand` | `t` | Stand up | | `crouch` | `g` | Crouch | | `prone` | `b` | Go prone | -| `toggle_freelook` | `f` | Toggle mouse-look | | `pause` | `p` | Pause the game | | `full_view` | Ctrl+F | Switch to the fullscreen view loop | | `normal_view` | Ctrl+D | Switch to the normal gameplay view | @@ -317,6 +357,7 @@ keyboard (there is no hold behavior). **Not exposed** (other `HotKeyLookup[]` entries): - `keypad 0`-`keypad 9`: a controller button per digit is not useful; use the on-screen keyboard. +- `toggle_freelook` (F): the free-view actions cover mouse look. - The 4 `cheat_*` entries: debug cheats. - `mac_help` (disabled) and `audiolog_cancel` (its build flag is not defined): they are not real hotkeys in this build. diff --git a/android/app/src/main/cpp/xr_input.c b/android/app/src/main/cpp/xr_input.c index b953864..c49ac29 100644 --- a/android/app/src/main/cpp/xr_input.c +++ b/android/app/src/main/cpp/xr_input.c @@ -94,9 +94,10 @@ static const char *kRawInputNames[QS_RAW_COUNT] = { "right_thumbstick_right", "trigger", "squeeze", "none", }; -// Remappable game actions: movement, click and jump, 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 +// 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: @@ -121,7 +122,6 @@ typedef enum { QS_ACTION_STAND, QS_ACTION_CROUCH, QS_ACTION_PRONE, - QS_ACTION_TOGGLE_FREELOOK, QS_ACTION_PAUSE, QS_ACTION_FULL_VIEW, QS_ACTION_NORMAL_VIEW, @@ -154,6 +154,19 @@ typedef enum { 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; @@ -170,7 +183,6 @@ static const char *kActionNames[QS_ACTION_COUNT] = { "stand", "crouch", "prone", - "toggle_freelook", "pause", "full_view", "normal_view", @@ -203,14 +215,28 @@ static const char *kActionNames[QS_ACTION_COUNT] = { "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 toggles free -// look; right A/B click/double-click; right trigger fires. Everything else is -// QS_RAW_NONE. +// 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 @@ -224,11 +250,10 @@ static QsRawInput s_binding[QS_ACTION_COUNT] = { QS_RAW_RIGHT_THUMBSTICK_UP, // stand QS_RAW_NONE, // crouch QS_RAW_RIGHT_THUMBSTICK_DOWN, // prone - QS_RAW_RIGHT_THUMBSTICK_CLICK, // toggle_freelook QS_RAW_NONE, // pause QS_RAW_NONE, // full_view QS_RAW_NONE, // normal_view - QS_RAW_NONE, // map_view + QS_RAW_RIGHT_THUMBSTICK_CLICK, // map_view QS_RAW_NONE, // save_game QS_RAW_NONE, // load_game QS_RAW_NONE, // select_grenade @@ -257,6 +282,19 @@ static QsRawInput s_binding[QS_ACTION_COUNT] = { 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 @@ -282,11 +320,10 @@ static const QsKeyAction kKeyActions[] = { {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"/"toggle_freelook"/"pause" - DOWN('t'/'g'/'b'/'f'/'p'). + // "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_TOGGLE_FREELOOK, SDL_SCANCODE_F, SDLK_f, KMOD_NONE, true, 'f'}, {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. @@ -335,6 +372,19 @@ static const QsKeyAction kKeyActions[] = { // 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])) @@ -393,6 +443,8 @@ 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 @@ -723,6 +775,8 @@ static void xr_input_update_raw_states(void) { 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; @@ -768,11 +822,10 @@ void xr_input_reset_player_controls(void) { QS_RAW_RIGHT_THUMBSTICK_UP, // stand QS_RAW_NONE, // crouch QS_RAW_RIGHT_THUMBSTICK_DOWN, // prone - QS_RAW_RIGHT_THUMBSTICK_CLICK, // toggle_freelook QS_RAW_NONE, // pause QS_RAW_NONE, // full_view QS_RAW_NONE, // normal_view - QS_RAW_NONE, // map_view + QS_RAW_RIGHT_THUMBSTICK_CLICK, // map_view QS_RAW_NONE, // save_game QS_RAW_NONE, // load_game QS_RAW_NONE, // select_grenade @@ -801,6 +854,19 @@ void xr_input_reset_player_controls(void) { 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; @@ -831,6 +897,39 @@ 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. @@ -849,9 +948,10 @@ static bool xr_input_binding_conflicts(QsRawInput binding, QsRawInput matched) { // 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 may -// respond to a given raw input - see xr_input_binding_conflicts() above) -// by clearing any other action that would otherwise still fire from it, +// 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) @@ -867,7 +967,9 @@ static void xr_input_process_capture(void) { QsRawInput matched = (QsRawInput)i; int displacedAction = -1; for (int a = 0; a < QS_ACTION_COUNT; a++) { - if (a != (int)g_capture_action && xr_input_binding_conflicts(s_binding[a], matched)) { + 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 @@ -1671,6 +1773,162 @@ static bool xr_input_push_key(const QsKeyAction *keyAction, bool down) { 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; @@ -1718,13 +1976,32 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { // 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]; - wantDown[i] = raw != QS_RAW_NONE && !xr_input_action_suppressed_by_capture(keyAction->action) && - !(xr_input_raw_is_thumbstick_direction(raw) && menuVisible) && g_raw_down[raw]; + 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]; @@ -1781,6 +2058,10 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { } 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++) { @@ -2096,7 +2377,9 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { py = 0; else if (py >= gameHeight) py = gameHeight - 1; - xr_mouse_move(px, py); + // 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); @@ -2144,6 +2427,11 @@ bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad) { } 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). diff --git a/android/app/src/main/java/de/ladkau/questshock/MenuOverlay.java b/android/app/src/main/java/de/ladkau/questshock/MenuOverlay.java index 807303d..ccb125c 100644 --- a/android/app/src/main/java/de/ladkau/questshock/MenuOverlay.java +++ b/android/app/src/main/java/de/ladkau/questshock/MenuOverlay.java @@ -103,7 +103,7 @@ public class MenuOverlay extends OverlayPanel { private static final String[] ACTION_KEYS = { "move_forward", "move_back", "strafe_left", "strafe_right", "turn_left", "turn_right", "jump", "mouse_click", "mouse_double_click", - "stand", "crouch", "prone", "toggle_freelook", "pause", + "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", @@ -112,7 +112,14 @@ public class MenuOverlay extends OverlayPanel { "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", + "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", + }; + // Shown first in the Controller Options list, in this order. + private static final String[] FIRST_ROW_ACTIONS = { + "fire_weapon", "free_left_thumbstick_view", "free_right_thumbstick_view", }; private static final String CONTROLLER_PROFILES_DIR = QuestShockActivity.GAME_DIR + "/controller_profiles"; // Shown (with its own explanatory note - see Strings.java's @@ -599,12 +606,21 @@ public class MenuOverlay extends OverlayPanel { controlRows = new LinearLayout[ACTION_KEYS.length]; controlActionLabels = new TextView[ACTION_KEYS.length]; controlBindingButtons = new Button[ACTION_KEYS.length]; - // Rows are created in display order (Fire Weapon first, then the - // rest in ACTION_KEYS order); every array below stays indexed by + // Rows are created in display order (FIRST_ROW_ACTIONS first, then + // the rest in ACTION_KEYS order); every array below stays indexed by // the real action index, which must keep matching xr_input.c. - int fireIndex = Arrays.asList(ACTION_KEYS).indexOf("fire_weapon"); + int[] displayOrder = new int[ACTION_KEYS.length]; + int filled = 0; + for (String key : FIRST_ROW_ACTIONS) { + displayOrder[filled++] = Arrays.asList(ACTION_KEYS).indexOf(key); + } + for (int k = 0; k < ACTION_KEYS.length; k++) { + if (!Arrays.asList(FIRST_ROW_ACTIONS).contains(ACTION_KEYS[k])) { + displayOrder[filled++] = k; + } + } for (int d = 0; d < ACTION_KEYS.length; d++) { - final int i = d == 0 ? fireIndex : (d - 1 < fireIndex ? d - 1 : d); + final int i = displayOrder[d]; final int actionIndex = i; LinearLayout row = new LinearLayout(activity); row.setOrientation(LinearLayout.HORIZONTAL); diff --git a/android/app/src/main/java/de/ladkau/questshock/Strings.java b/android/app/src/main/java/de/ladkau/questshock/Strings.java index fa29c2a..34bb098 100644 --- a/android/app/src/main/java/de/ladkau/questshock/Strings.java +++ b/android/app/src/main/java/de/ladkau/questshock/Strings.java @@ -79,8 +79,6 @@ final class Strings { put("action_stand", "Stand", "Debout", "Stehen"); put("action_crouch", "Crouch", "Accroupi", "Ducken"); put("action_prone", "Prone", "Couché", "Liegen"); - put("action_toggle_freelook", "Toggle Free Look", "Basculer la vue libre", - "Freies Umsehen umschalten"); put("action_pause", "Pause Game", "Mettre en pause", "Spiel pausieren"); put("action_full_view", "Fullscreen View", "Vue plein écran", "Vollbildansicht"); put("action_normal_view", "Normal View", "Vue normale", "Normale Ansicht"); @@ -121,6 +119,34 @@ final class Strings { put("action_mfd_right_4", "Right MFD: Target", "MFD droit : cible", "Rechtes MFD: Ziel"); put("action_mfd_right_5", "Right MFD: Info", "MFD droit : info", "Rechtes MFD: Info"); put("action_fire_weapon", "Fire Weapon", "Tirer", "Waffe abfeuern"); + put("action_free_left_thumbstick_view", "Free View (Left Stick)", + "Vue libre (stick gauche)", "Freie Ansicht (linker Stick)"); + put("action_free_right_thumbstick_view", "Free View (Right Stick)", + "Vue libre (stick droit)", "Freie Ansicht (rechter Stick)"); + + // Cyberspace actions (active only in cyberspace, see xr_input.c). + put("action_cyber_thrust", "Cyberspace: Thrust", "Cyberespace : poussée", + "Cyberspace: Schub"); + put("action_cyber_climb", "Cyberspace: Climb", "Cyberespace : monter", + "Cyberspace: Steigen"); + put("action_cyber_dive", "Cyberspace: Dive", "Cyberespace : plonger", + "Cyberspace: Sinken"); + put("action_cyber_bank_left", "Cyberspace: Bank Left", "Cyberespace : virage à gauche", + "Cyberspace: Links kippen"); + put("action_cyber_bank_right", "Cyberspace: Bank Right", "Cyberespace : virage à droite", + "Cyberspace: Rechts kippen"); + put("action_cyber_roll_left", "Cyberspace: Roll Left", "Cyberespace : roulis gauche", + "Cyberspace: Links rollen"); + put("action_cyber_roll_right", "Cyberspace: Roll Right", "Cyberespace : roulis droit", + "Cyberspace: Rechts rollen"); + put("action_cyber_climb_left", "Cyberspace: Climb Left", "Cyberespace : monter à gauche", + "Cyberspace: Links steigen"); + put("action_cyber_climb_right", "Cyberspace: Climb Right", "Cyberespace : monter à droite", + "Cyberspace: Rechts steigen"); + put("action_cyber_dive_left", "Cyberspace: Dive Left", "Cyberespace : plonger à gauche", + "Cyberspace: Links sinken"); + put("action_cyber_dive_right", "Cyberspace: Dive Right", "Cyberespace : plonger à droite", + "Cyberspace: Rechts sinken"); put("capture_prompt", "Press a button…", "Appuyez sur un bouton…", "Taste drücken…"); put("reset_to_defaults", "Reset to Defaults", "Réinitialiser", "Auf Standard zurücksetzen"); put("generic_profile_note", diff --git a/android/engine-patches/24-android-cyberspace-query.patch b/android/engine-patches/24-android-cyberspace-query.patch new file mode 100644 index 0000000..b949b24 --- /dev/null +++ b/android/engine-patches/24-android-cyberspace-query.patch @@ -0,0 +1,13 @@ +--- a/src/GameSrc/mouselook.c ++++ b/src/GameSrc/mouselook.c +@@ -121,3 +121,10 @@ + get_mouselook_vel(&mvelx, &mvely); + } + } ++ ++#ifdef __ANDROID__ ++// Whether the player is currently in cyberspace; read by the Android ++// controller input code (android/app/src/main/cpp/xr_input.c) to switch ++// between the normal and cyberspace action sets. ++int qs_in_cyberspace(void) { return global_fullmap != NULL && global_fullmap->cyber; } ++#endif diff --git a/android/engine-patches/25-android-cyberspace-analog-scale.patch b/android/engine-patches/25-android-cyberspace-analog-scale.patch new file mode 100644 index 0000000..329361e --- /dev/null +++ b/android/engine-patches/25-android-cyberspace-analog-scale.patch @@ -0,0 +1,110 @@ +--- a/src/GameSrc/movekeys.c ++++ b/src/GameSrc/movekeys.c +@@ -44,6 +44,12 @@ + extern bool gKeypadOverride; + + uchar parse_motion_key(ushort code, short *cnum, short *cval); ++#ifdef __ANDROID__ ++// Set every frame by android/app/src/main/cpp/xr_input.c. ++float qs_cyber_pitch_scale = 1.0f; ++float qs_cyber_bank_scale = 1.0f; ++#endif ++ + uchar parse_motion_key_cyber(ushort code, short *cnum, short *cval); + void init_motion_polling(void); + +@@ -173,6 +179,20 @@ + *cnum = -1; + *cval = 0; + ++#ifdef __ANDROID__ ++ // Pitch and bank strength, scaled by the controller thumbstick's ++ // deflection (see android/app/src/main/cpp/xr_input.c); 1.0 for a key. ++ short pitchMax = (short)(CONTROL_MAX_VAL * qs_cyber_pitch_scale + 0.5f); ++ short bankMax = (short)(CONTROL_MAX_VAL * qs_cyber_bank_scale + 0.5f); ++ if (pitchMax < 1) ++ pitchMax = 1; ++ if (bankMax < 1) ++ bankMax = 1; ++#else ++ short pitchMax = CONTROL_MAX_VAL; ++ short bankMax = CONTROL_MAX_VAL; ++#endif ++ + while (MoveCyberKeybinds[i].code != 255) { + if (code == MoveCyberKeybinds[i].code) { + move = MoveCyberKeybinds[i].move; +@@ -189,22 +209,22 @@ + + case M_CLIMB: + *cnum = CONTROL_YVEL; +- *cval = -CONTROL_MAX_VAL; ++ *cval = -pitchMax; + break; + + case M_BANKLEFT: + *cnum = CONTROL_XYROT; +- *cval = -CONTROL_MAX_VAL; ++ *cval = -bankMax; + break; + + case M_BANKRIGHT: + *cnum = CONTROL_XYROT; +- *cval = CONTROL_MAX_VAL; ++ *cval = bankMax; + break; + + case M_DIVE: + *cnum = CONTROL_YVEL; +- *cval = CONTROL_MAX_VAL; ++ *cval = pitchMax; + break; + + case M_ROLLRIGHT: +@@ -219,38 +239,38 @@ + + case M_CLIMBLEFT: + *cnum = CONTROL_YVEL; +- *cval = -CONTROL_MAX_VAL; ++ *cval = -pitchMax; + if (abs(poll_controls[*cnum]) < abs(*cval)) + poll_controls[*cnum] = *cval; + *cnum = CONTROL_XYROT; +- *cval = -CONTROL_MAX_VAL; ++ *cval = -bankMax; + break; + + case M_CLIMBRIGHT: + *cnum = CONTROL_YVEL; +- *cval = -CONTROL_MAX_VAL; ++ *cval = -pitchMax; + if (abs(poll_controls[*cnum]) < abs(*cval)) + poll_controls[*cnum] = *cval; + *cnum = CONTROL_XYROT; +- *cval = CONTROL_MAX_VAL; ++ *cval = bankMax; + break; + + case M_DIVERIGHT: + *cnum = CONTROL_YVEL; +- *cval = CONTROL_MAX_VAL; ++ *cval = pitchMax; + if (abs(poll_controls[*cnum]) < abs(*cval)) + poll_controls[*cnum] = *cval; + *cnum = CONTROL_XYROT; +- *cval = CONTROL_MAX_VAL; ++ *cval = bankMax; + break; + + case M_DIVELEFT: + *cnum = CONTROL_YVEL; +- *cval = CONTROL_MAX_VAL; ++ *cval = pitchMax; + if (abs(poll_controls[*cnum]) < abs(*cval)) + poll_controls[*cnum] = *cval; + *cnum = CONTROL_XYROT; +- *cval = -CONTROL_MAX_VAL; ++ *cval = -bankMax; + break; + default: + // Unhandled movement. What I gonna do?