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 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 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 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}