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 <noreply@anthropic.com>
This commit is contained in:
Stefan MaierandClaude Opus 5.5 committed 2026-10-13 21:45:40 +02:00
1 parent c85cf8ece1
commit e18c2b37d6
7 files changed
+127 -48

No files matched your search

+3
View File
@@ -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
+11 -5
View File
@@ -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
+11 -6
View File
@@ -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"),
+8 -3
View File
@@ -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,
+64 -22
View File
@@ -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)
{
+1 -1
View File
@@ -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
+29 -11
View File
@@ -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);