Skip to content

input: report the release edge of custom bindings - #416

Open
schlegp wants to merge 1 commit into
Supreeeme:mainfrom
schlegp:fix/digital-action-release-edge
Open

input: report the release edge of custom bindings#416
schlegp wants to merge 1 commit into
Supreeeme:mainfrom
schlegp:fix/digital-action-release-edge

Conversation

@schlegp

@schlegp schlegp commented Aug 31, 2026

Copy link
Copy Markdown

Was investigating why throwing items in Half-Life: Alyx only worked with the grip grab and not the trigger/pinch grab.

Alyx binds its grab action (/actions/interact/in/use) to two inputs at once. In bindings_touch.json the grip is a trigger mode click, which xrizer binds natively, and the trigger is a button mode click, which becomes a threshold binding on trigger/value.

GetDigitalActionData only adopted the custom binding's state while it was held. Releasing the trigger failed that condition, so it fell back to reporting the natively bound grip instead. The action correctly read as no longer held, but bChanged was false and last_change_time came from whenever the grip last moved — tracing it, the timestamp reported on a trigger release was literally the previous grip release's. Alyx appears to act on that release edge, so it never threw the object, it just let go of it. Grip grabs worked because there the native binding was the one that changed.

Both candidates are now ranked: a pressed binding wins, and between two unpressed ones the one that changed during this sync wins. Applied the same rule in the two places that pick among an action's custom bindings and between hands, which previously fell back to whichever candidate came first.

Includes a regression test using the same binding shape (one native click, one threshold binding). Against the old code it fails with bChanged: false.

Fixes throwing in Alyx (Issue #281). Haven't checked other games as I don't have other similar ones to play.

Comment thread src/input/tests.rs
Comment on lines +1092 to +1097
/// Half-Life: Alyx binds its grab action this way - grip as a `trigger` mode click, which becomes a
/// native binding, and trigger as a `button` mode click, which becomes a threshold on
/// trigger/value. Releasing the threshold binding used to fall back to reporting the native
/// binding's state: the action correctly read as no longer held, but `bChanged` was false and
/// `fUpdateTime`'s companion `last_change_time` came from whenever the *grip* last moved. Alyx
/// never saw the release edge, so held objects dropped straight down instead of being thrown.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can go in the commit message instead of this comment.

Comment thread src/input.rs
}

/// Ranking used to decide which of an action's bindings should represent it. Highest wins.
fn binding_rank(state: &xr::ActionState<bool>) -> (bool, bool, i64) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be simpler if this just took two action states and returned the "highest" one, which should probably just be which of the last_change_times is the latest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants