From e18c2b37d699c4bcd0bf5ecec7830df258abc437 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:45:40 +0200 Subject: [PATCH] Push to talk on any button, never sent by a grip reaching for the weapon Push to talk becomes an action of the BUTTONS page (vr.button_talk), on an eighth button there, the free hand's grip (its default): it can trade places with any other action (the move stick's click, say, so that a player holding a weapon in both hands can talk). The free hand's grip as a button is pressed once held 0.15 s, at the threshold halo_vr_two_hand takes the foregrip at, and not at all while it holds the weapon's foregrip or the Warthog's wheel: taken in that time, the press is theirs until the grip opens. Before, the runtime's grip button (which closes before the foregrip's threshold) could open the microphone for a frame or two as the hand took the weapon. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++ port/android/VR_ROADMAP.md | 16 ++++--- port/linux/include/halo_vr.h | 17 ++++--- port/linux/src/port_config.c | 11 +++-- port/linux/src/vr.c | 86 +++++++++++++++++++++++++++--------- port/linux/src/xinput_sdl.c | 2 +- source/interface/ui_widget.c | 40 ++++++++++++----- 7 files changed, 127 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index fd39fded..260a9ad3 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,9 @@ saves and game data stay. More about the Steam Frame: *Quit the game* is on its last page. - In the menus, point with a controller and pull the trigger to click. - The controller layout is on the VR options' *Controller buttons* page. +- **Voice chat** in network games: hold the grip of the hand without the + weapon to talk (not while it holds the weapon or the Warthog's wheel). + The *BUTTONS* page can put push to talk on another button. ## Problems and questions diff --git a/port/android/VR_ROADMAP.md b/port/android/VR_ROADMAP.md index 69602106..d0e85df1 100644 --- a/port/android/VR_ROADMAP.md +++ b/port/android/VR_ROADMAP.md @@ -25,6 +25,8 @@ named where code is taken over. | Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard | | Multiplayer | Up to date with OpenCE (network version 28, 2026-10-12): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list | | Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person | +| Voice chat | Push to talk on any button (`vr.button_talk`, BUTTONS page; the free hand's grip by default, sending nothing while it reaches for the foregrip or the wheel), open mic, off (2026-10-13) | +| Builds | The Android build's assembly conversion (`tools/android_asm_convert.py`) keeps string data as it is: strings and tables starting with "_" or holding \" lost a "_" (Opus's pulse cache among them: the Steam Frame's crashes in online games while talking), 2026-10-13 | | Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) | | Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) | | Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) | @@ -150,11 +152,15 @@ COMFORT (turning, the vignettes), BUTTONS (the actions' buttons, Buttons remapped (BUTTONS page; `vr.c` `controls_remapped`): - Jump, the flashlight (held: switch grenades), action/reload, switch - weapon, crouch, zoom and melee each go on one of seven buttons: A, B, - X, Y, the move stick's click, the turning stick's, the weapon hand's - grip. Moving an action to a button swaps it with the action there, so - none is ever left without one. -- The triggers, the free hand's grip, the menu button, the Steam Frame's + weapon, crouch, zoom, melee and push to talk each go on one of eight + buttons: A, B, X, Y, the move stick's click, the turning stick's, the + weapon hand's grip, the free hand's grip. Moving an action to a button + swaps it with the action there, so none is ever left without one. +- The free hand's grip as a button (`free_grip_down`): pressed once held + 0.15 s at the foregrip's threshold, and not at all while it holds the + weapon's foregrip or the Warthog's wheel (taken in that time, the press + is theirs until the grip opens). +- The triggers, the menu button, the Steam Frame's bumpers, D-pad and View button keep theirs; the menus keep the buttons as they are. - Applied after the left-handed mirroring, on the gamepad's sides: the diff --git a/port/linux/include/halo_vr.h b/port/linux/include/halo_vr.h index feadea81..c685c957 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -169,7 +169,8 @@ enum int halo_vr_body_mode(void); /* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */ int halo_vr_melee(void); -/* the free hand's grip as voice chat's push to talk (vr.c) */ +/* voice chat's push to talk held: its button (vr.button_talk; the free +hand's grip by default) */ int halo_vr_push_to_talk_held(void); /* the weight of the melee attack under way, from the swing's speed (0..1; 0 with none): the game scales melee damage by the player's run instead, @@ -262,13 +263,13 @@ int halo_vr_controller_layout(void); vr.handedness) */ int halo_vr_weapon_hand(void); /* The game's actions on the controllers' buttons (vr.button_*; the VR -options' BUTTONS page): each action on one of seven buttons, named as +options' BUTTONS page): each action on one of eight buttons, named as right-handed (A and B on the right controller, X and Y, the Quest's, on the left; the move stick's click, the turning stick's, the weapon hand's -grip); left-handed with vr.mirror_controls, their mirror images. The -actions are numbered by the button each has by default. The triggers, the -free hand's grip, the menu button and the Steam Frame's bumpers, D-pad and -View button keep theirs; the menus keep the buttons as they are. */ +grip, the free hand's); left-handed with vr.mirror_controls, their mirror +images. The actions are numbered by the button each has by default. The +triggers, the menu button and the Steam Frame's bumpers, D-pad and View +button keep theirs; the menus keep the buttons as they are. */ enum { HALO_VR_INPUT_A = 0, @@ -278,6 +279,9 @@ enum HALO_VR_INPUT_MOVE_STICK, HALO_VR_INPUT_TURN_STICK, HALO_VR_INPUT_GRIP, + /* (pressed held a moment, if it took neither the weapon's foregrip nor + the Warthog's wheel: vr.c's free_grip_down) */ + HALO_VR_INPUT_FREE_GRIP, HALO_VR_INPUT_COUNT, HALO_VR_ACTION_JUMP = HALO_VR_INPUT_A, @@ -287,6 +291,7 @@ enum HALO_VR_ACTION_CROUCH = HALO_VR_INPUT_MOVE_STICK, HALO_VR_ACTION_ZOOM = HALO_VR_INPUT_TURN_STICK, HALO_VR_ACTION_MELEE = HALO_VR_INPUT_GRIP, + HALO_VR_ACTION_TALK = HALO_VR_INPUT_FREE_GRIP, /* voice chat's push to talk */ HALO_VR_ACTION_COUNT = HALO_VR_INPUT_COUNT, }; /* an action's setting ("vr.button_jump"), a button's value in it ("a"), diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 23b3fada..11cc00d2 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -252,7 +252,9 @@ static const struct config_setting config_settings[] = "page swaps them): \"a\", \"b\", \"x\", \"y\" (A and B on the right\n" "controller, X and Y the Quest's on the left; the Steam Frame has all\n" "four on the right), \"move_stick\" and \"turn_stick\" (their clicks),\n" - "\"grip\" (the weapon hand's), named as right-handed: left-handed with\n" + "\"grip\" (the weapon hand's), \"free_grip\" (the free hand's, held a\n" + "moment, when it took neither the weapon's foregrip nor the Warthog's\n" + "wheel), named as right-handed: left-handed with\n" "mirror_controls, their mirror images. Jump," }, { "vr.button_light", _config_string, "\"b\"", "HALO_VR_BUTTON_LIGHT", _environment_value, _platform_android, "the flashlight (held: switch grenades)," }, @@ -267,8 +269,11 @@ static const struct config_setting config_settings[] = _platform_android, "zoom" }, { "vr.button_melee", _config_string, "\"grip\"", "HALO_VR_BUTTON_MELEE", _environment_value, _platform_android, - "and melee. The triggers, the free hand's grip and the menu button keep\n" - "theirs, as do the menus." }, + "melee" }, + { "vr.button_talk", _config_string, "\"free_grip\"", "HALO_VR_BUTTON_TALK", _environment_value, + _platform_android, + "and voice chat's push to talk (audio.voice_chat). The triggers and the\n" + "menu button keep theirs, as do the menus." }, { "vr.laser", _config_boolean, "false", "HALO_VR_LASER", _environment_value, _platform_android, "A laser from the weapon to what it aims at (with aim_with = \"controller\")." }, { "vr.crosshair", _config_boolean, "true", "HALO_VR_CROSSHAIR", _environment_value, _platform_android, diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 56273e6f..b63082ee 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -22,11 +22,11 @@ HALO_VR_ACTION_* and HALO_VR_INPUT_*), by number */ static const char *const vr_action_settings[HALO_VR_ACTION_COUNT] = { "vr.button_jump", "vr.button_light", "vr.button_action", "vr.button_weapon", "vr.button_crouch", - "vr.button_zoom", "vr.button_melee", + "vr.button_zoom", "vr.button_melee", "vr.button_talk", }; static const char *const vr_input_values[HALO_VR_INPUT_COUNT] = { - "a", "b", "x", "y", "move_stick", "turn_stick", "grip", + "a", "b", "x", "y", "move_stick", "turn_stick", "grip", "free_grip", }; const char *halo_vr_action_setting(int action) @@ -230,6 +230,10 @@ static struct int two_hand_latched; /* the grip is held (on the weapon or not) */ int two_hand_valid; float two_hand_reach; /* metres from the foregrip a closing grip takes the weapon */ + /* the free hand's grip as a button (free_grip_down): closed since (0 + open), and whether it took the weapon or the wheel since */ + unsigned long long free_grip_since_us; + int free_grip_taken; /* the grenade in the free hand (grenade_update, vr.grenade_throw_speed): its trigger held, the hand's last positions (LOCAL), and the throw as it let go */ @@ -1354,19 +1358,6 @@ int halo_vr_finger_curls(short local_player_index, int hand, float curls[4]) return HALO_VR_FINGERS_CURLED | ((state->buttons & HALO_XR_BUTTON_TOUCH_KNOWN) ? HALO_VR_FINGERS_TOUCH : 0); } -/* Voice chat's push to talk in VR (network_voice.c, through -halo_push_to_talk_held): the free hand's grip, while it holds neither the -weapon's foregrip nor the Warthog's wheel; the grip has no button of the -game's of its own. */ -int halo_vr_push_to_talk_held(void) -{ - int hand = free_hand(); - - if (!vr.active || !vr.frame.focused || !(vr.frame.hands[hand].buttons & HALO_XR_BUTTON_SQUEEZE)) - return 0; - return !vr.two_hand_gripping && !vr.wheel_held[hand]; -} - int halo_vr_melee(void) { return vr.active && now_us() < vr.melee_until_us; @@ -3406,28 +3397,63 @@ holding a Warthog's wheel press nothing. */ static const int input_hands[HALO_VR_INPUT_COUNT] = { HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT, HALO_XR_HAND_LEFT, HALO_XR_HAND_LEFT, HALO_XR_HAND_LEFT, - HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT, + HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT, HALO_XR_HAND_LEFT, }; static const int input_buttons[HALO_VR_INPUT_COUNT] = { HALO_XR_BUTTON_A, HALO_XR_BUTTON_B, HALO_XR_BUTTON_A, HALO_XR_BUTTON_B, HALO_XR_BUTTON_THUMBSTICK, - HALO_XR_BUTTON_THUMBSTICK, HALO_XR_BUTTON_SQUEEZE, + HALO_XR_BUTTON_THUMBSTICK, HALO_XR_BUTTON_SQUEEZE, HALO_XR_BUTTON_SQUEEZE, }; +/* The free hand's grip as a button (HALO_VR_INPUT_FREE_GRIP; push to talk +by default), the hands mirrored: pressed once closed for +FREE_GRIP_DELAY_US, at halo_vr_two_hand's threshold, unless it took the +weapon's foregrip or the Warthog's wheel (which both decide sooner) since +it closed; that press then is theirs until it opens. So a grip reaching +for the weapon presses nothing, not even for a moment. */ +#define FREE_GRIP_DELAY_US 150000ULL + +static int free_grip_down(const struct halo_xr_hand *free) +{ + unsigned long long now = now_us(); + + if (free->squeeze <= (vr.free_grip_since_us ? 0.35f : 0.6f)) + { + vr.free_grip_since_us = 0; + vr.free_grip_taken = 0; + return 0; + } + if (!vr.free_grip_since_us) + vr.free_grip_since_us = now; + if (vr.two_hand_gripping || vr.wheel_held[free_hand()]) + vr.free_grip_taken = 1; + return !vr.free_grip_taken && now - vr.free_grip_since_us >= FREE_GRIP_DELAY_US; +} + +/* the buttons' presses (HALO_VR_INPUT_*), the hands mirrored */ +static void controls_down(const struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int down[HALO_VR_INPUT_COUNT]) +{ + int input; + + for (input = 0; input < HALO_VR_INPUT_COUNT; input++) + down[input] = (hands[input_hands[input]].buttons & input_buttons[input]) != 0; + if (vr.wheel_hands) + down[HALO_VR_INPUT_GRIP] = 0; + down[HALO_VR_INPUT_FREE_GRIP] = free_grip_down(&hands[HALO_XR_HAND_LEFT]); +} + static void controls_remapped(struct halo_xr_hand hands[HALO_XR_HAND_COUNT]) { int down[HALO_VR_INPUT_COUNT]; int input, action; - for (input = 0; input < HALO_VR_INPUT_COUNT; input++) - down[input] = (hands[input_hands[input]].buttons & input_buttons[input]) != 0; - if (vr.wheel_hands) - down[HALO_VR_INPUT_GRIP] = 0; + controls_down(hands, down); for (input = 0; input < HALO_VR_INPUT_COUNT; input++) hands[input_hands[input]].buttons &= ~input_buttons[input]; for (action = 0; action < HALO_VR_ACTION_COUNT; action++) { - if (down[vr.action_inputs[action]]) + /* (talking is no button of the game's: halo_vr_push_to_talk_held) */ + if (action != HALO_VR_ACTION_TALK && down[vr.action_inputs[action]]) hands[input_hands[action]].buttons |= input_buttons[action]; } } @@ -3441,6 +3467,22 @@ static void controls_in_play(struct halo_xr_hand hands[HALO_XR_HAND_COUNT]) controls_remapped(hands); } +/* Voice chat's push to talk in VR (network_voice.c, through +halo_push_to_talk_held): its button (vr.button_talk), in the menus too */ +int halo_vr_push_to_talk_held(void) +{ + struct halo_xr_hand hands[HALO_XR_HAND_COUNT]; + int down[HALO_VR_INPUT_COUNT]; + + if (!vr.active || !vr.frame.focused) + return 0; + memcpy(hands, vr.frame.hands, sizeof(hands)); + if (vr.left_handed) + controllers_mirrored(hands); + controls_down(hands, down); + return down[vr.action_inputs[HALO_VR_ACTION_TALK]]; +} + /* the controllers, for the gamepad the game reads (xinput_sdl.c) */ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus) { diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 5779fc04..6850f0d1 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -605,7 +605,7 @@ int halo_push_to_talk_held(void) int slot; #ifdef HALO_ANDROID - /* (port: in VR the free hand's grip too, halo_vr_push_to_talk_held) */ + /* (port: in VR its button too, vr.button_talk: halo_vr_push_to_talk_held) */ if (halo_vr_active() && halo_vr_push_to_talk_held()) return 1; #endif diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 4ab5d349..ec049b6b 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -7519,8 +7519,8 @@ static short const vr_option_pages[NUMBER_OF_VR_OPTION_PAGES][VR_OPTIONS_PER_PAG { _vr_option_button_first + HALO_VR_ACTION_JUMP, _vr_option_button_first + HALO_VR_ACTION_LIGHT, _vr_option_button_first + HALO_VR_ACTION_ACTION, _vr_option_button_first + HALO_VR_ACTION_WEAPON, _vr_option_button_first + HALO_VR_ACTION_CROUCH, _vr_option_button_first + HALO_VR_ACTION_ZOOM, - _vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_buttons_reset, _vr_option_controls, - NONE, NONE, NONE }, + _vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_button_first + HALO_VR_ACTION_TALK, + _vr_option_buttons_reset, _vr_option_controls, NONE, NONE }, { _vr_option_graphics, _vr_option_foveation, _vr_option_refresh_rate, _vr_option_resolution, _vr_option_quit, NONE, NONE, NONE, NONE, NONE, NONE, NONE }, }; @@ -7582,7 +7582,7 @@ options' rows. On the Quest A and B are on the right, X and Y on the left, and mirrored the other way round; on the Steam Frame all four are on the right, and mirrored the left D-pad takes them (down, left, right, up). The sticks' clicks change sides with them; the grip is the weapon -hand's. */ +hand's, the free grip the other hand's. */ static char const *ui_vr_input_name( short input, boolean full, @@ -7604,7 +7604,8 @@ static char const *ui_vr_input_name( name = full ? (side ? "Right stick click" : "Left stick click") : "Stick click"; break; case HALO_VR_INPUT_GRIP: - side = (short)(ui_vr_left_handed() ? 0 : 1); + case HALO_VR_INPUT_FREE_GRIP: + side = (short)((input == HALO_VR_INPUT_FREE_GRIP) != (ui_vr_left_handed() == 0)); name = full ? (side ? "Right grip" : "Left grip") : "Grip"; break; default: @@ -7631,10 +7632,11 @@ options' rows name them, and in short (the Steam Frame's stick lines) */ static char const *const vr_action_names[HALO_VR_ACTION_COUNT] = { "Jump", "Light; held: grenades", "Action, reload", "Switch weapon", "Crouch", "Zoom", "Melee", + "Push to talk", }; static char const *const vr_action_short_names[HALO_VR_ACTION_COUNT] = { - "jump", "light", "action", "weapon", "crouch", "zoom", "melee", + "jump", "light", "action", "weapon", "crouch", "zoom", "melee", "talk", }; /* an action moved to the next button or the one before: the action that @@ -7723,9 +7725,10 @@ static void ui_vr_options_vignette_text( /* vr.body's values, and as the panel shows them */ static char const *const vr_bodies[] = { "full", "arms", "hands", "off" }; static char const *const vr_body_names[] = { "Full body", "Arms and hands", "Hands only", "Weapon only" }; -/* audio.voice_chat's (network_voice.c); push to talk is the free hand's grip */ +/* audio.voice_chat's (network_voice.c); push to talk's button is on the +BUTTONS page (vr.button_talk) */ static char const *const vr_voice_chats[] = { "push_to_talk", "open_mic", "off" }; -static char const *const vr_voice_chat_names[] = { "Push to talk (grip)", "Open mic", "Off" }; +static char const *const vr_voice_chat_names[] = { "Push to talk", "Open mic", "Off" }; static short ui_vr_options_voice_chat( void) @@ -8291,6 +8294,23 @@ static void ui_vr_controls_add_action( } } +/* the free hand's grip: the weapon in both hands at its foregrip, else the +action on it (vr.button_talk's push to talk by default) */ +static void ui_vr_controls_add_free_grip( + struct ui_vr_controls_column columns[2], + short free_column) +{ + static char line[32]; + short action = ui_vr_input_action(HALO_VR_INPUT_FREE_GRIP); + + if (action != HALO_VR_ACTION_TALK) + snprintf(line, sizeof(line), "Both hands; else %s", vr_action_short_names[action]); + else + snprintf(line, sizeof(line), "%s", ui_vr_options_voice_chat() == 0 ? "Both hands; else talk" : + "Weapon in both hands"); + ui_vr_controls_add(&columns[free_column], "Grip", line); +} + static void ui_vr_controls_column_render( short x, short y, @@ -8375,8 +8395,7 @@ static void ui_vr_controls_render( ui_vr_controls_add(&columns[gun], "Trigger", "Fire"); ui_vr_controls_add(&columns[free_column], "Trigger", "Grenade: hold, throw"); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_GRIP), TRUE); - ui_vr_controls_add(&columns[free_column], "Grip", ui_vr_options_voice_chat() == 0 ? - "Both hands; else talk" : "Weapon in both hands"); + ui_vr_controls_add_free_grip(columns, free_column); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_A), TRUE); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_B), TRUE); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_X), TRUE); @@ -8429,8 +8448,7 @@ static void ui_vr_controls_render( ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_GRIP), FALSE); ui_vr_controls_add(&columns[free_column], "Trigger", "Grenade: hold it,"); ui_vr_controls_add(&columns[free_column], "", "let go as you throw"); - ui_vr_controls_add(&columns[free_column], "Grip", ui_vr_options_voice_chat() == 0 ? - "Both hands; else talk" : "Weapon in both hands"); + ui_vr_controls_add_free_grip(columns, free_column); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_A), FALSE); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_B), FALSE); ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_X), FALSE);