Skip to main content

siglus_scene_vm/runtime/
input.rs

1//! Runtime input state (winit-agnostic).
2//!
3//! Siglus scripts query input via numeric forms (INPUT/MOUSE/KEYLIST) and helper
4//! key objects. The original engine stores per-key state in fixed tables.
5//!
6//! Runtime input model:
7//! - A fixed 0..=255 virtual-key table (down + edge "stock" flags)
8//! - Mouse position
9//! - Mouse wheel delta since last read / frame
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum VmKey {
13    Escape,
14    Enter,
15    Space,
16    Backspace,
17    Delete,
18    Tab,
19    Shift,
20    Control,
21    Meta,
22    Alt,
23    Home,
24    End,
25    ArrowUp,
26    ArrowDown,
27    ArrowLeft,
28    ArrowRight,
29    /// Function keys (F1..F12).
30    F(u8),
31    /// Digit keys 0..9.
32    Digit(u8),
33    /// Latin letter keys A..Z.
34    Letter(char),
35    /// Any other unmapped physical key.
36    Other(u32),
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum VmMouseButton {
41    Left,
42    Right,
43    Middle,
44    Other(u8),
45}
46
47#[derive(Debug, Clone, Copy)]
48struct KeyState {
49    down: bool,
50    down_stock: bool,
51    up_stock: bool,
52    // Mirrors tona3 C_input_state::BUTTON::down_up_stock:
53    // 0 = no down/up sequence pending; 1 = down happened; 2 = down+up completed.
54    // NEXT/frame clears only state 2, so state 1 survives until the matching up event.
55    down_up_stock: u8,
56    flick_stock: bool,
57    flick_angle: f32,
58    flick_pixel: f32,
59    flick_mm: f32,
60    flick_start: Option<(i32, i32)>,
61}
62
63impl KeyState {
64    const fn new() -> Self {
65        Self {
66            down: false,
67            down_stock: false,
68            up_stock: false,
69            down_up_stock: 0,
70            flick_stock: false,
71            flick_angle: 0.0,
72            flick_pixel: 0.0,
73            flick_mm: 0.0,
74            flick_start: None,
75        }
76    }
77
78    fn clear_all(&mut self) {
79        self.down = false;
80        self.down_stock = false;
81        self.up_stock = false;
82        self.down_up_stock = 0;
83        self.flick_stock = false;
84        self.flick_angle = 0.0;
85        self.flick_pixel = 0.0;
86        self.flick_mm = 0.0;
87        self.flick_start = None;
88    }
89
90    fn clear_stocks(&mut self) {
91        self.down_stock = false;
92        self.up_stock = false;
93        // tona3 BUTTON::frame() only clears completed down-up stock.
94        // The intermediate state (1) must persist across NEXT/frame until set_up().
95        if self.down_up_stock == 2 {
96            self.down_up_stock = 0;
97        }
98        self.flick_stock = false;
99    }
100
101    fn use_stocks(&mut self) {
102        self.down_stock = false;
103        self.up_stock = false;
104        self.down_up_stock = 0;
105        self.flick_stock = false;
106    }
107}
108
109// Contemporary SiglusEngine uses the WinMM JOYINFOEX button state as one of
110// its raw joypad input backends. JOYINFOEX::dwButtons is a 32-bit button mask
111// (JOY_BUTTON1..JOY_BUTTON32), so the script-visible raw joypad key domain must
112// not truncate the backend at the 20 controls used by one particular game UI.
113pub const JOYPAD_KEY_COUNT: usize = 32;
114
115#[derive(Debug, Clone)]
116pub struct InputState {
117    keys: [KeyState; 256],
118    // Newer Siglus builds expose the raw joypad button domain. Keep the complete
119    // 32-button WinMM domain; individual games are free to iterate only the
120    // subset they assign. The script-visible copy follows the same BUTTON
121    // stock semantics as keyboard keys: held state plus down/up/down-up edges.
122    joypad_keys: [KeyState; JOYPAD_KEY_COUNT],
123
124    pub mouse_x: i32,
125    pub mouse_y: i32,
126    mouse_position_valid: bool,
127
128    wheel_delta: i32,
129
130    /// Last key-down event since start.
131    pub last_key_down: Option<VmKey>,
132    /// Last mouse-down event since start.
133    pub last_mouse_down: Option<VmMouseButton>,
134
135    /// Newer Siglus builds expose whether UI navigation is currently driven by
136    /// a joypad. Keyboard/mouse activity switches this off; a platform gamepad
137    /// bridge can switch it on through `note_joypad_activity`.
138    joypad_mode_active: bool,
139}
140
141impl Default for InputState {
142    fn default() -> Self {
143        Self {
144            keys: [KeyState::new(); 256],
145            joypad_keys: [KeyState::new(); JOYPAD_KEY_COUNT],
146            mouse_x: -1,
147            mouse_y: -1,
148            mouse_position_valid: false,
149            wheel_delta: 0,
150            last_key_down: None,
151            last_mouse_down: None,
152            joypad_mode_active: false,
153        }
154    }
155}
156
157impl InputState {
158    /// Returns the input-family state queried by the newer SYSCOM opcode 333.
159    pub fn joypad_mode_active(&self) -> bool {
160        self.joypad_mode_active
161    }
162
163    /// Called by a platform gamepad backend when a navigation-capable joypad
164    /// input becomes the active UI device.
165    pub fn note_joypad_activity(&mut self) {
166        self.joypad_mode_active = true;
167    }
168
169    /// Returns whether a newer-engine joypad key is currently held.
170    pub fn joypad_is_down(&self, key_no: usize) -> bool {
171        self.joypad_keys.get(key_no).is_some_and(|st| st.down)
172    }
173
174    /// Returns the non-consuming down edge stock for a joypad key.
175    pub fn joypad_down_stock(&self, key_no: usize) -> bool {
176        self.joypad_keys.get(key_no).is_some_and(|st| st.down_stock)
177    }
178
179    /// Returns the non-consuming up edge stock for a joypad key.
180    pub fn joypad_up_stock(&self, key_no: usize) -> bool {
181        self.joypad_keys.get(key_no).is_some_and(|st| st.up_stock)
182    }
183
184    /// Returns the non-consuming completed down/up stock for a joypad key.
185    pub fn joypad_down_up_stock(&self, key_no: usize) -> bool {
186        self.joypad_keys
187            .get(key_no)
188            .is_some_and(|st| st.down_up_stock == 2)
189    }
190
191    /// Platform bridge entry point for a joypad-key press.  Keeping this in
192    /// InputState means the VM semantics are complete even on platforms where
193    /// no gamepad backend has been wired yet.
194    pub fn on_joypad_key_down(&mut self, key_no: usize) {
195        let Some(st) = self.joypad_keys.get_mut(key_no) else {
196            return;
197        };
198        self.joypad_mode_active = true;
199        if !st.down {
200            st.down = true;
201            st.down_stock = true;
202        }
203        if st.down_up_stock == 0 {
204            st.down_up_stock = 1;
205        }
206        if st.down_stock && st.up_stock {
207            st.down_up_stock = 2;
208        }
209    }
210
211    /// Platform bridge entry point for a joypad-key release.
212    pub fn on_joypad_key_up(&mut self, key_no: usize) {
213        if key_no >= JOYPAD_KEY_COUNT {
214            return;
215        }
216        self.joypad_mode_active = true;
217        let st = &mut self.joypad_keys[key_no];
218        if st.down {
219            st.down = false;
220            st.up_stock = true;
221            if st.down_up_stock == 1 {
222                st.down_up_stock = 2;
223            }
224        }
225    }
226
227    fn note_keyboard_mouse_activity(&mut self) {
228        self.joypad_mode_active = false;
229    }
230
231    /// Drop transient input edges after returning from a native configuration
232    /// UI. Held state is retained, matching `use_current`.
233    pub fn resync_after_native_ui(&mut self) {
234        self.use_current();
235    }
236
237    // ---------------------------------------------------------------------
238    // Virtual key helpers
239    // ---------------------------------------------------------------------
240
241    /// Returns true if the given virtual-key is currently held down.
242    pub fn vk_is_down(&self, vk: u8) -> bool {
243        self.keys[vk as usize].down
244    }
245
246    /// Returns true if the key transitioned to down since the last `next_frame`.
247    pub fn vk_down_stock(&self, vk: u8) -> bool {
248        self.keys[vk as usize].down_stock
249    }
250
251    /// Returns true if the key transitioned to up since the last `next_frame`.
252    pub fn vk_up_stock(&self, vk: u8) -> bool {
253        self.keys[vk as usize].up_stock
254    }
255
256    /// Returns true if a down+up pair happened since the last `next_frame`.
257    pub fn vk_down_up_stock(&self, vk: u8) -> bool {
258        self.keys[vk as usize].down_up_stock == 2
259    }
260
261    /// Consume one key-down edge while preserving the held state.
262    ///
263    /// Matches `C_input_state::BUTTON::use_down_stock()`: consuming DOWN also
264    /// invalidates any in-progress DOWN_UP sequence so the later release cannot
265    /// be observed as a second logical input.
266    pub fn use_vk_down_stock(&mut self, vk: u8) -> bool {
267        let st = &mut self.keys[vk as usize];
268        if !st.down_stock {
269            return false;
270        }
271        st.down_stock = false;
272        st.down_up_stock = 0;
273        true
274    }
275
276    /// Consume one key-up edge. Matches tona3 `use_up_stock()`.
277    pub fn use_vk_up_stock(&mut self, vk: u8) -> bool {
278        let st = &mut self.keys[vk as usize];
279        if !st.up_stock {
280            return false;
281        }
282        st.up_stock = false;
283        st.down_up_stock = 0;
284        true
285    }
286
287    /// Consume a completed key down/up pair. Matches tona3
288    /// `C_input_state::BUTTON::use_down_up_stock()`.
289    pub fn use_vk_down_up_stock(&mut self, vk: u8) -> bool {
290        let st = &mut self.keys[vk as usize];
291        if st.down_up_stock != 2 {
292            return false;
293        }
294        st.down_stock = false;
295        st.up_stock = false;
296        st.down_up_stock = 0;
297        true
298    }
299
300    /// Returns true if a flick was detected since the last `next_frame`.
301    pub fn vk_flick_stock(&self, vk: u8) -> bool {
302        self.keys[vk as usize].flick_stock
303    }
304
305    /// Returns flick angle (radians) for the last flick on this key.
306    pub fn vk_flick_angle(&self, vk: u8) -> f32 {
307        self.keys[vk as usize].flick_angle
308    }
309
310    /// Returns flick distance in pixels for the last flick on this key.
311    pub fn vk_flick_pixel(&self, vk: u8) -> f32 {
312        self.keys[vk as usize].flick_pixel
313    }
314
315    /// Returns flick distance in millimeters for the last flick on this key.
316    pub fn vk_flick_mm(&self, vk: u8) -> f32 {
317        self.keys[vk as usize].flick_mm
318    }
319
320    fn vk_set_down(&mut self, vk: u8) {
321        let st = &mut self.keys[vk as usize];
322        if !st.down {
323            st.down = true;
324            st.down_stock = true;
325        }
326        if st.down_up_stock == 0 {
327            st.down_up_stock = 1;
328        }
329        // If both edges happen within the same frame, mark the completed down-up stock.
330        if st.down_stock && st.up_stock {
331            st.down_up_stock = 2;
332        }
333    }
334
335    fn vk_set_up(&mut self, vk: u8) {
336        let st = &mut self.keys[vk as usize];
337        if st.down {
338            st.down = false;
339            st.up_stock = true;
340            if st.down_up_stock == 1 {
341                st.down_up_stock = 2;
342            }
343        }
344    }
345
346    fn vk_set_flick_start(&mut self, vk: u8) {
347        if !is_mouse_vk(vk) {
348            return;
349        }
350        let st = &mut self.keys[vk as usize];
351        st.flick_stock = false;
352        st.flick_start = Some((self.mouse_x, self.mouse_y));
353    }
354
355    fn vk_set_flick_end(&mut self, vk: u8) {
356        if !is_mouse_vk(vk) {
357            return;
358        }
359        let st = &mut self.keys[vk as usize];
360        let Some((sx, sy)) = st.flick_start.take() else {
361            return;
362        };
363        let dx = (self.mouse_x - sx) as f32;
364        let dy = (self.mouse_y - sy) as f32;
365        let dist = (dx * dx + dy * dy).sqrt();
366        if dist < FLICK_MIN_PIXEL {
367            return;
368        }
369        // Note: the original engine seems to treat angle as atan2(dx, dy).
370        st.flick_angle = dx.atan2(dy);
371        st.flick_pixel = dist;
372        st.flick_mm = dist * MM_PER_PX;
373        st.flick_stock = true;
374    }
375
376    /// Clears all keys (including held-down state) and all edge stocks.
377    pub fn clear_all(&mut self) {
378        for st in &mut self.keys {
379            st.clear_all();
380        }
381        for st in &mut self.joypad_keys {
382            st.clear_all();
383        }
384        self.wheel_delta = 0;
385        self.last_key_down = None;
386        self.last_mouse_down = None;
387        self.joypad_mode_active = false;
388    }
389
390    pub fn has_mouse_position(&self) -> bool {
391        self.mouse_position_valid
392    }
393
394    /// Clears only keyboard-visible state and leaves mouse state intact.
395    pub fn clear_keyboard(&mut self) {
396        for (idx, st) in self.keys.iter_mut().enumerate() {
397            if matches!(idx as u8, 0x01 | 0x02 | 0x04) {
398                continue;
399            }
400            st.clear_all();
401        }
402        self.last_key_down = None;
403    }
404
405    /// Clears only mouse-visible state and leaves keyboard state intact.
406    pub fn clear_mouse(&mut self) {
407        for vk in [0x01usize, 0x02usize, 0x04usize] {
408            self.keys[vk].clear_all();
409        }
410        self.wheel_delta = 0;
411        self.last_mouse_down = None;
412    }
413
414    /// Consumes current input edges while preserving held-down state.
415    ///
416    /// Mirrors tona3 C_input_state::use(): clear down/up/down_up/flick stocks
417    /// for mouse and keyboard plus wheel, but do not release held keys.
418    pub fn use_current(&mut self) {
419        for st in &mut self.keys {
420            st.use_stocks();
421        }
422        for st in &mut self.joypad_keys {
423            st.use_stocks();
424        }
425        self.wheel_delta = 0;
426        self.last_key_down = None;
427        self.last_mouse_down = None;
428    }
429
430    /// Advances to the next frame: clears edge stocks but keeps held-down state.
431    pub fn next_frame(&mut self) {
432        for st in &mut self.keys {
433            st.clear_stocks();
434        }
435        for st in &mut self.joypad_keys {
436            st.clear_stocks();
437        }
438        self.wheel_delta = 0;
439    }
440
441    /// Advances only keyboard state to the next frame.
442    pub fn next_keyboard_frame(&mut self) {
443        for (idx, st) in self.keys.iter_mut().enumerate() {
444            if matches!(idx as u8, 0x01 | 0x02 | 0x04) {
445                continue;
446            }
447            st.clear_stocks();
448        }
449        self.last_key_down = None;
450    }
451
452    /// Advances only mouse state to the next frame.
453    pub fn next_mouse_frame(&mut self) {
454        for vk in [0x01usize, 0x02usize, 0x04usize] {
455            self.keys[vk].clear_stocks();
456        }
457        self.wheel_delta = 0;
458        self.last_mouse_down = None;
459    }
460
461    // ---------------------------------------------------------------------
462    // Wheel
463    // ---------------------------------------------------------------------
464
465    pub fn on_mouse_wheel(&mut self, delta_y: i32) {
466        self.note_keyboard_mouse_activity();
467        self.wheel_delta = self.wheel_delta.saturating_add(delta_y);
468    }
469
470    /// Reads and clears the accumulated wheel delta.
471    pub fn take_wheel_delta(&mut self) -> i32 {
472        let v = self.wheel_delta;
473        self.wheel_delta = 0;
474        v
475    }
476
477    // ---------------------------------------------------------------------
478    // Bridge from platform key/mouse events
479    // ---------------------------------------------------------------------
480
481    pub fn is_key_down(&self, k: VmKey) -> bool {
482        vmkey_to_vk(k)
483            .map(|vk| self.vk_is_down(vk))
484            .unwrap_or(false)
485    }
486
487    pub fn is_mouse_down(&self, b: VmMouseButton) -> bool {
488        match b {
489            VmMouseButton::Left => self.vk_is_down(0x01),
490            VmMouseButton::Right => self.vk_is_down(0x02),
491            VmMouseButton::Middle => self.vk_is_down(0x04),
492            VmMouseButton::Other(_) => false,
493        }
494    }
495
496    pub fn on_key_down(&mut self, k: VmKey) {
497        self.note_keyboard_mouse_activity();
498        if let Some(vk) = vmkey_to_vk(k) {
499            self.vk_set_down(vk);
500        }
501        self.last_key_down = Some(k);
502    }
503
504    pub fn on_key_up(&mut self, k: VmKey) {
505        if let Some(vk) = vmkey_to_vk(k) {
506            self.vk_set_up(vk);
507        }
508    }
509
510    pub fn on_mouse_down(&mut self, b: VmMouseButton) {
511        self.note_keyboard_mouse_activity();
512        match b {
513            VmMouseButton::Left => {
514                self.vk_set_down(0x01);
515                self.vk_set_flick_start(0x01);
516            }
517            VmMouseButton::Right => {
518                self.vk_set_down(0x02);
519                self.vk_set_flick_start(0x02);
520            }
521            VmMouseButton::Middle => {
522                self.vk_set_down(0x04);
523                self.vk_set_flick_start(0x04);
524            }
525            VmMouseButton::Other(_) => {}
526        }
527        self.last_mouse_down = Some(b);
528    }
529
530    pub fn on_mouse_up(&mut self, b: VmMouseButton) {
531        match b {
532            VmMouseButton::Left => {
533                self.vk_set_flick_end(0x01);
534                self.vk_set_up(0x01);
535            }
536            VmMouseButton::Right => {
537                self.vk_set_flick_end(0x02);
538                self.vk_set_up(0x02);
539            }
540            VmMouseButton::Middle => {
541                self.vk_set_flick_end(0x04);
542                self.vk_set_up(0x04);
543            }
544            VmMouseButton::Other(_) => {}
545        }
546    }
547
548    pub fn on_mouse_move(&mut self, x: i32, y: i32) {
549        self.note_keyboard_mouse_activity();
550        self.mouse_x = x;
551        self.mouse_y = y;
552        self.mouse_position_valid = true;
553    }
554
555    /// Returns a direction bitmask based on arrow keys.
556    ///
557    /// Bit layout:
558    ///   1=left, 2=right, 4=up, 8=down
559    pub fn dir_mask(&self) -> i64 {
560        let mut m = 0;
561        if self.vk_is_down(0x25) {
562            m |= 1;
563        }
564        if self.vk_is_down(0x27) {
565            m |= 2;
566        }
567        if self.vk_is_down(0x26) {
568            m |= 4;
569        }
570        if self.vk_is_down(0x28) {
571            m |= 8;
572        }
573        m
574    }
575}
576
577pub(crate) fn vmkey_to_vk_code(k: VmKey) -> Option<u8> {
578    vmkey_to_vk(k)
579}
580
581fn vmkey_to_vk(k: VmKey) -> Option<u8> {
582    match k {
583        VmKey::Escape => Some(0x1B),
584        VmKey::Enter => Some(0x0D),
585        VmKey::Space => Some(0x20),
586        VmKey::Backspace => Some(0x08),
587        VmKey::Delete => Some(0x2E),
588        VmKey::Tab => Some(0x09),
589        VmKey::Shift => Some(0x10),
590        VmKey::Control => Some(0x11),
591        VmKey::Meta => Some(0x5B),
592        VmKey::Alt => Some(0x12),
593        VmKey::Home => Some(0x24),
594        VmKey::End => Some(0x23),
595
596        VmKey::ArrowLeft => Some(0x25),
597        VmKey::ArrowUp => Some(0x26),
598        VmKey::ArrowRight => Some(0x27),
599        VmKey::ArrowDown => Some(0x28),
600
601        VmKey::F(n @ 1..=12) => Some(0x6F + n), // F1=0x70
602        VmKey::Digit(n @ 0..=9) => Some(0x30 + n),
603        VmKey::Letter(c) => {
604            let uc = c.to_ascii_uppercase();
605            if uc.is_ascii_uppercase() {
606                Some(uc as u8)
607            } else {
608                None
609            }
610        }
611        VmKey::Other(_) => None,
612        _ => None,
613    }
614}
615
616fn is_mouse_vk(vk: u8) -> bool {
617    matches!(vk, 0x01 | 0x02 | 0x04)
618}
619
620const FLICK_MIN_PIXEL: f32 = 30.0;
621const MM_PER_PX: f32 = 25.4 / 96.0;
622
623#[cfg(test)]
624mod joypad_mode_tests {
625    use super::{InputState, VmKey};
626
627    #[test]
628    fn keyboard_activity_leaves_joypad_mode() {
629        let mut input = InputState::default();
630        input.note_joypad_activity();
631        assert!(input.joypad_mode_active());
632        input.on_key_down(VmKey::Enter);
633        assert!(!input.joypad_mode_active());
634    }
635
636    #[test]
637    fn native_ui_resync_consumes_edges_but_keeps_mode() {
638        let mut input = InputState::default();
639        input.note_joypad_activity();
640        input.on_key_down(VmKey::Enter);
641        input.note_joypad_activity();
642        input.resync_after_native_ui();
643        assert!(input.joypad_mode_active());
644        assert!(!input.vk_down_stock(0x0d));
645        assert!(input.vk_is_down(0x0d));
646    }
647
648    #[test]
649    fn joypad_key_uses_button_stock_lifecycle() {
650        let mut input = InputState::default();
651        input.on_joypad_key_down(0);
652        assert!(input.joypad_mode_active());
653        assert!(input.joypad_is_down(0));
654        assert!(input.joypad_down_stock(0));
655        assert!(!input.joypad_down_up_stock(0));
656
657        input.next_frame();
658        assert!(input.joypad_is_down(0));
659        assert!(!input.joypad_down_stock(0));
660
661        input.on_joypad_key_up(0);
662        assert!(!input.joypad_is_down(0));
663        assert!(input.joypad_up_stock(0));
664        assert!(input.joypad_down_up_stock(0));
665    }
666
667    #[test]
668    fn consuming_down_prevents_release_from_becoming_down_up() {
669        let mut input = InputState::default();
670        input.on_mouse_down(super::VmMouseButton::Left);
671        assert!(input.vk_down_stock(0x01));
672        assert!(input.use_vk_down_stock(0x01));
673        assert!(!input.vk_down_stock(0x01));
674
675        input.on_mouse_up(super::VmMouseButton::Left);
676        assert!(input.vk_up_stock(0x01));
677        assert!(!input.vk_down_up_stock(0x01));
678    }
679
680    #[test]
681    fn consuming_up_or_down_up_matches_tona_button_stock_semantics() {
682        let mut input = InputState::default();
683        input.on_key_down(VmKey::Enter);
684        input.on_key_up(VmKey::Enter);
685        assert!(input.vk_down_up_stock(0x0d));
686        assert!(input.use_vk_up_stock(0x0d));
687        assert!(!input.vk_up_stock(0x0d));
688        assert!(!input.vk_down_up_stock(0x0d));
689
690        input.on_key_down(VmKey::Enter);
691        input.on_key_up(VmKey::Enter);
692        assert!(input.use_vk_down_up_stock(0x0d));
693        assert!(!input.vk_down_stock(0x0d));
694        assert!(!input.vk_up_stock(0x0d));
695        assert!(!input.vk_down_up_stock(0x0d));
696    }
697
698    #[test]
699    fn joypad_keeps_complete_32_button_backend_domain() {
700        let mut input = InputState::default();
701
702        // WinMM JOYINFOEX::dwButtons exposes JOY_BUTTON1..JOY_BUTTON32.
703        // The last backend button therefore maps to script key index 31.
704        input.on_joypad_key_down(31);
705        assert!(input.joypad_is_down(31));
706        assert!(input.joypad_down_stock(31));
707
708        input.on_joypad_key_up(31);
709        assert!(!input.joypad_is_down(31));
710        assert!(input.joypad_up_stock(31));
711
712        // Index 32 is outside the actual backend domain and must not alias or
713        // grow the table implicitly.
714        input.on_joypad_key_down(32);
715        assert!(!input.joypad_is_down(32));
716    }
717}