Skip to main content

mittens_engine/engine/ecs/system/
input_xr_gamepad_system.rs

1use crate::engine::ecs::component::{
2    ControllerHand, InputXRComponent, InputXRGamepadComponent, TransformComponent, XrAxisControl,
3    XrButtonControl, XrHandPreference,
4};
5use crate::engine::ecs::system::{System, TransformSystem, XrGamepadState, XrSystem};
6use crate::engine::ecs::{ComponentId, EventSignal, IntentValue, SignalEmitter, World};
7use crate::engine::graphics::{CameraTarget, VisualWorld};
8use crate::engine::user_input::InputState;
9use crate::utils::math;
10use std::collections::{HashMap, HashSet};
11use std::sync::OnceLock;
12
13fn xr_input_event_log_enabled() -> bool {
14    static ENABLED: OnceLock<bool> = OnceLock::new();
15    *ENABLED.get_or_init(|| {
16        std::env::var("CAT_XR_INPUT_LOG")
17            .ok()
18            .map(|s| {
19                let s = s.trim().to_ascii_lowercase();
20                s == "1" || s == "true" || s == "on" || s == "yes"
21            })
22            .unwrap_or(false)
23    })
24}
25
26#[derive(Debug, Clone, Default)]
27struct ComponentEventState {
28    buttons: HashMap<(ControllerHand, XrButtonControl), bool>,
29    axes: HashMap<(ControllerHand, XrAxisControl), [f32; 2]>,
30}
31
32#[derive(Debug, Default)]
33pub struct InputXRGamepadSystem {
34    components: HashSet<ComponentId>,
35    previous: HashMap<ComponentId, ComponentEventState>,
36    xr_active_last_frame: bool,
37    logged_missing_owner: HashSet<ComponentId>,
38    logged_missing_locomotion_target: HashSet<ComponentId>,
39}
40
41impl InputXRGamepadSystem {
42    pub fn register_input_xr_gamepad(&mut self, component: ComponentId) {
43        self.components.insert(component);
44    }
45
46    pub fn remove_input_xr_gamepad(&mut self, component: ComponentId) {
47        self.components.remove(&component);
48        self.previous.remove(&component);
49        self.logged_missing_owner.remove(&component);
50        self.logged_missing_locomotion_target.remove(&component);
51    }
52
53    pub fn tick_with_queue(
54        &mut self,
55        world: &mut World,
56        visuals: &mut VisualWorld,
57        xr: &XrSystem,
58        emit: &mut dyn SignalEmitter,
59        dt_sec: f32,
60    ) {
61        let xr_state = *xr.xr_gamepad_state();
62        if !xr_state.active {
63            if self.xr_active_last_frame {
64                eprintln!("[input_xr_gamepad_system] XR gamepad input inactive");
65            }
66            self.xr_active_last_frame = false;
67            return;
68        }
69        if !self.xr_active_last_frame {
70            eprintln!("[input_xr_gamepad_system] XR gamepad input active");
71        }
72        self.xr_active_last_frame = true;
73
74        let components: Vec<_> = self.components.iter().copied().collect();
75        for cid in components {
76            let Some(cfg) = world
77                .get_component_by_id_as::<InputXRGamepadComponent>(cid)
78                .cloned()
79            else {
80                self.remove_input_xr_gamepad(cid);
81                continue;
82            };
83            if !cfg.enabled {
84                continue;
85            }
86
87            let Some(input_xr_owner) = nearest_ancestor_component::<InputXRComponent>(world, cid)
88            else {
89                if self.logged_missing_owner.insert(cid) {
90                    eprintln!(
91                        "[input_xr_gamepad_system] component {:?} has no owning InputXR ancestor; skipping XR input",
92                        cid
93                    );
94                }
95                continue;
96            };
97            self.logged_missing_owner.remove(&cid);
98
99            let prev = self.previous.get(&cid).cloned().unwrap_or_default();
100            let mut next = ComponentEventState::default();
101            emit_axis_events(cid, cfg.hand, &xr_state, &prev, &mut next, emit);
102            emit_button_events(cid, cfg.hand, &xr_state, &prev, &mut next, emit);
103            self.previous.insert(cid, next);
104
105            if cfg.locomotion {
106                let moved = apply_locomotion(
107                    world,
108                    visuals,
109                    input_xr_owner,
110                    &cfg,
111                    &xr_state,
112                    emit,
113                    dt_sec,
114                );
115                if moved {
116                    self.logged_missing_locomotion_target.remove(&cid);
117                } else if locomotion_target_transform(world, input_xr_owner).is_none()
118                    && self.logged_missing_locomotion_target.insert(cid)
119                {
120                    eprintln!(
121                        "[input_xr_gamepad_system] component {:?} has locomotion enabled but no transform ancestor above InputXR",
122                        cid
123                    );
124                }
125            }
126        }
127    }
128}
129
130impl System for InputXRGamepadSystem {
131    fn tick(
132        &mut self,
133        _world: &mut World,
134        _visuals: &mut VisualWorld,
135        _input: &InputState,
136        _dt_sec: f32,
137    ) {
138    }
139}
140
141fn emit_axis_events(
142    cid: ComponentId,
143    hand_pref: XrHandPreference,
144    xr: &XrGamepadState,
145    prev: &ComponentEventState,
146    next: &mut ComponentEventState,
147    emit: &mut dyn SignalEmitter,
148) {
149    for &(hand, control, value) in &[
150        (
151            ControllerHand::Left,
152            XrAxisControl::LeftStick,
153            xr.hands[0].thumbstick,
154        ),
155        (
156            ControllerHand::Right,
157            XrAxisControl::RightStick,
158            xr.hands[1].thumbstick,
159        ),
160        (
161            ControllerHand::Left,
162            XrAxisControl::LeftTrigger,
163            xr.hands[0].trigger_value.map(|v| [v, 0.0]),
164        ),
165        (
166            ControllerHand::Right,
167            XrAxisControl::RightTrigger,
168            xr.hands[1].trigger_value.map(|v| [v, 0.0]),
169        ),
170        (
171            ControllerHand::Left,
172            XrAxisControl::LeftGrip,
173            xr.hands[0].grip_value.map(|v| [v, 0.0]),
174        ),
175        (
176            ControllerHand::Right,
177            XrAxisControl::RightGrip,
178            xr.hands[1].grip_value.map(|v| [v, 0.0]),
179        ),
180    ] {
181        if !hand_matches(hand_pref, hand) {
182            continue;
183        }
184        let Some(value) = value else {
185            continue;
186        };
187        next.axes.insert((hand, control), value);
188        if prev.axes.get(&(hand, control)).copied() != Some(value) {
189            if xr_input_event_log_enabled() {
190                eprintln!(
191                    "[input_xr_gamepad_system] axis {:?} {:?} -> [{:.3}, {:.3}] on {:?}",
192                    hand, control, value[0], value[1], cid
193                );
194            }
195            emit.push_event(
196                cid,
197                EventSignal::XrAxisChanged {
198                    source_component: cid,
199                    hand,
200                    control,
201                    value,
202                },
203            );
204        }
205    }
206}
207
208fn emit_button_events(
209    cid: ComponentId,
210    hand_pref: XrHandPreference,
211    xr: &XrGamepadState,
212    prev: &ComponentEventState,
213    next: &mut ComponentEventState,
214    emit: &mut dyn SignalEmitter,
215) {
216    for &(hand, control, state) in &[
217        (
218            ControllerHand::Left,
219            XrButtonControl::LeftTrigger,
220            xr.hands[0].trigger_pressed,
221        ),
222        (
223            ControllerHand::Right,
224            XrButtonControl::RightTrigger,
225            xr.hands[1].trigger_pressed,
226        ),
227        (
228            ControllerHand::Left,
229            XrButtonControl::LeftGrip,
230            xr.hands[0].grip_pressed,
231        ),
232        (
233            ControllerHand::Right,
234            XrButtonControl::RightGrip,
235            xr.hands[1].grip_pressed,
236        ),
237        (
238            ControllerHand::Left,
239            XrButtonControl::ButtonX,
240            xr.hands[0].button_x,
241        ),
242        (
243            ControllerHand::Left,
244            XrButtonControl::ButtonY,
245            xr.hands[0].button_y,
246        ),
247        (
248            ControllerHand::Right,
249            XrButtonControl::ButtonA,
250            xr.hands[1].button_a,
251        ),
252        (
253            ControllerHand::Right,
254            XrButtonControl::ButtonB,
255            xr.hands[1].button_b,
256        ),
257    ] {
258        if !hand_matches(hand_pref, hand) {
259            continue;
260        }
261        let Some((down, value)) = state else {
262            continue;
263        };
264        next.buttons.insert((hand, control), down);
265        let was_down = prev.buttons.get(&(hand, control)).copied().unwrap_or(false);
266        if was_down != down {
267            if xr_input_event_log_enabled() {
268                eprintln!(
269                    "[input_xr_gamepad_system] button {:?} {:?} -> {} ({:.3}) on {:?}",
270                    hand,
271                    control,
272                    if down { "down" } else { "up" },
273                    value,
274                    cid
275                );
276            }
277            emit.push_event(
278                cid,
279                if down {
280                    EventSignal::XrButtonDown {
281                        source_component: cid,
282                        hand,
283                        control,
284                        value,
285                    }
286                } else {
287                    EventSignal::XrButtonUp {
288                        source_component: cid,
289                        hand,
290                        control,
291                        value,
292                    }
293                },
294            );
295        }
296        if was_down != down {
297            emit.push_event(
298                cid,
299                EventSignal::XrButtonChanged {
300                    source_component: cid,
301                    hand,
302                    control,
303                    value,
304                },
305            );
306        }
307    }
308}
309
310fn apply_locomotion(
311    world: &mut World,
312    visuals: &VisualWorld,
313    input_xr_owner: ComponentId,
314    cfg: &InputXRGamepadComponent,
315    xr: &XrGamepadState,
316    emit: &mut dyn SignalEmitter,
317    dt_sec: f32,
318) -> bool {
319    let Some((_, stick)) = resolve_locomotion_stick(cfg.hand, xr) else {
320        return false;
321    };
322
323    let mut dx = stick[0];
324    let mut dz = -stick[1];
325    let len = (dx * dx + dz * dz).sqrt();
326    if len <= cfg.deadzone || len <= f32::EPSILON {
327        return false;
328    }
329    let scaled = ((len - cfg.deadzone) / (1.0 - cfg.deadzone)).clamp(0.0, 1.0);
330    dx = dx / len * scaled;
331    dz = dz / len * scaled;
332
333    let Some(target_tcid) = locomotion_target_transform(world, input_xr_owner) else {
334        return false;
335    };
336
337    // OpenXR has already published the final world-space eye transforms for the
338    // active CameraXR this frame. Use that authoritative rendered orientation
339    // directly; reconstructing it from the ECS hierarchy can double AVC yaw.
340    let camera_world = visuals
341        .visual_camera(CameraTarget::Xr)
342        .and_then(|camera| camera.eyes.first())
343        .map(|eye| eye.transform.matrix_world)
344        .or_else(|| TransformSystem::world_model(world, target_tcid));
345    let (right, back) = camera_world
346        .and_then(horizontal_camera_basis)
347        .unwrap_or(([1.0, 0.0, 0.0], [0.0, 0.0, 1.0]));
348    let move_world = [
349        right[0] * dx + back[0] * dz,
350        0.0,
351        right[2] * dx + back[2] * dz,
352    ];
353
354    // The locomotion target stores a local translation. Convert the world-space
355    // direction back through its parent so rotated parent rigs don't skew it.
356    let move_local = world
357        .parent_of(target_tcid)
358        .and_then(|parent| TransformSystem::world_model(world, parent))
359        .map(|parent_world| {
360            math::quat_rotate_vec3(
361                math::quat_conjugate(math::mat_to_quat(parent_world)),
362                move_world,
363            )
364        })
365        .unwrap_or(move_world);
366    let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(target_tcid) else {
367        return false;
368    };
369    t.transform.translation[0] += move_local[0] * cfg.speed * dt_sec;
370    t.transform.translation[2] += move_local[2] * cfg.speed * dt_sec;
371    t.transform.recompute_model();
372
373    let transform = t.transform;
374    emit.push_intent_now(
375        target_tcid,
376        IntentValue::UpdateTransform {
377            component_ids: vec![target_tcid],
378            translation: transform.translation,
379            rotation_quat_xyzw: transform.rotation,
380            scale: transform.scale,
381        },
382    );
383    true
384}
385
386fn hand_matches(pref: XrHandPreference, hand: ControllerHand) -> bool {
387    match pref {
388        XrHandPreference::Default | XrHandPreference::Either => true,
389        XrHandPreference::Left => hand == ControllerHand::Left,
390        XrHandPreference::Right => hand == ControllerHand::Right,
391    }
392}
393
394fn nearest_ancestor_component<T: 'static>(
395    world: &World,
396    start: ComponentId,
397) -> Option<ComponentId> {
398    let mut cur = world.parent_of(start);
399    while let Some(cid) = cur {
400        if world.get_component_by_id_as::<T>(cid).is_some() {
401            return Some(cid);
402        }
403        cur = world.parent_of(cid);
404    }
405    None
406}
407
408fn locomotion_target_transform(world: &World, input_xr_owner: ComponentId) -> Option<ComponentId> {
409    let mut cur = world.parent_of(input_xr_owner);
410    while let Some(cid) = cur {
411        if world
412            .get_component_by_id_as::<TransformComponent>(cid)
413            .is_some()
414        {
415            return Some(cid);
416        }
417        cur = world.parent_of(cid);
418    }
419    None
420}
421
422fn resolve_locomotion_stick(
423    pref: XrHandPreference,
424    xr: &XrGamepadState,
425) -> Option<(ControllerHand, [f32; 2])> {
426    match pref {
427        XrHandPreference::Left => xr.hands[0].thumbstick.map(|v| (ControllerHand::Left, v)),
428        XrHandPreference::Right => xr.hands[1].thumbstick.map(|v| (ControllerHand::Right, v)),
429        XrHandPreference::Either | XrHandPreference::Default => xr.hands[0]
430            .thumbstick
431            .map(|v| (ControllerHand::Left, v))
432            .or_else(|| xr.hands[1].thumbstick.map(|v| (ControllerHand::Right, v))),
433    }
434}
435
436fn horizontal_camera_basis(matrix_world: [[f32; 4]; 4]) -> Option<([f32; 3], [f32; 3])> {
437    // Transform matrices are column-major: column 0 is camera-right and column
438    // 2 is camera-back. Project both onto the ground plane and normalize them.
439    let normalize_xz = |v: [f32; 3]| {
440        let len = (v[0] * v[0] + v[2] * v[2]).sqrt();
441        (len > 1e-6).then(|| [v[0] / len, 0.0, v[2] / len])
442    };
443    let right = normalize_xz([matrix_world[0][0], 0.0, matrix_world[0][2]])?;
444    let back = normalize_xz([matrix_world[2][0], 0.0, matrix_world[2][2]])?;
445    Some((right, back))
446}