Add free view and a separate cyberspace action set
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.
This commit is contained in:
ml
2026-09-20 07:24:44 +02:00
parent c24b7f15cb
commit d8324d409e
6 changed files with 531 additions and 37 deletions
+309 -21
View File
@@ -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).
@@ -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);
@@ -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",
@@ -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
@@ -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?