bevy_input_sequence/
plugin.rs

1use bevy::{
2    app::{App, Plugin, Update},
3    diagnostic::FrameCount,
4    ecs::{
5        entity::Entity,
6        intern::Interned,
7        prelude::{In, IntoScheduleConfigs, Messages, RemovedComponents},
8        query::Added,
9        schedule::{ScheduleLabel, SystemSet},
10        system::{Commands, Local, Query, Res, ResMut},
11    },
12    input::{
13        gamepad::{Gamepad, GamepadButton, GamepadEvent},
14        keyboard::KeyCode,
15        ButtonInput,
16    },
17    log::warn,
18    time::Time,
19};
20use std::collections::{HashMap, VecDeque};
21
22use crate::{
23    cache::{ButtonSequenceCache, KeySequenceCache},
24    chord::{is_modifier, KeyChordQueue},
25    frame_time::FrameTime,
26    input_sequence::{ButtonSequence, InputSequence, KeySequence},
27    KeyChord, Modifiers,
28};
29use trie_rs::inc_search::{Answer, IncSearch};
30
31/// ButtonInput sequence plugin.
32pub struct InputSequencePlugin {
33    #[allow(clippy::type_complexity)]
34    schedules: Vec<(Interned<dyn ScheduleLabel>, Option<Interned<dyn SystemSet>>)>,
35    match_key: Option<bool>,
36    match_button: Option<bool>,
37}
38
39impl Default for InputSequencePlugin {
40    fn default() -> Self {
41        InputSequencePlugin {
42            schedules: vec![(Interned(Box::leak(Box::new(Update))), None)],
43            match_key: None,
44            match_button: None,
45        }
46    }
47}
48
49impl Plugin for InputSequencePlugin {
50    fn build(&self, app: &mut App) {
51        if self
52            .match_key
53            .unwrap_or(app.world().get_resource::<ButtonInput<KeyCode>>().is_some())
54        {
55            app
56                // Add key sequence.
57                .init_resource::<KeySequenceCache>()
58                .init_resource::<KeyChordQueue>();
59
60            for (schedule, set) in &self.schedules {
61                if let Some(set) = set {
62                    app.add_systems(
63                        *schedule,
64                        (
65                            detect_key_removals,
66                            detect_key_additions,
67                            key_sequence_matcher,
68                        )
69                            .chain()
70                            .in_set(*set),
71                    );
72                } else {
73                    app.add_systems(
74                        *schedule,
75                        (
76                            detect_key_removals,
77                            detect_key_additions,
78                            key_sequence_matcher,
79                        )
80                            .chain(),
81                    );
82                }
83            }
84        } else {
85            warn!("No key sequence matcher added; consider adding DefaultPlugins.");
86        }
87
88        if self
89            .match_button
90            .unwrap_or(app.world().contains_resource::<Messages<GamepadEvent>>())
91        {
92            // app
93            //     .register_type::<InputSequence<GamepadButton, In<Entity>>>()
94            //     ;
95            // Add button sequences.
96            app.init_resource::<ButtonSequenceCache>();
97
98            for (schedule, set) in &self.schedules {
99                if let Some(set) = set {
100                    app.add_systems(
101                        *schedule,
102                        (
103                            detect_button_removals,
104                            detect_button_additions,
105                            button_sequence_matcher,
106                        )
107                            .chain()
108                            .in_set(*set),
109                    );
110                } else {
111                    app.add_systems(
112                        *schedule,
113                        (
114                            detect_button_removals,
115                            detect_button_additions,
116                            button_sequence_matcher,
117                        )
118                            .chain(),
119                    );
120                }
121            }
122        } else {
123            // Only warn if not specified.
124            if self.match_button.is_none() {
125                warn!("No button sequence matcher added; consider adding DefaultPlugins.");
126            }
127        }
128    }
129}
130
131impl InputSequencePlugin {
132    /// Constructs an empty input sequence plugin with no default schedules.
133    pub fn empty() -> Self {
134        Self {
135            schedules: vec![],
136            match_key: None,
137            match_button: None,
138        }
139    }
140    /// Run the executor in a specific `Schedule`.
141    pub fn run_in(mut self, schedule: impl ScheduleLabel) -> Self {
142        self.schedules
143            .push((Interned(Box::leak(Box::new(schedule))), None));
144        self
145    }
146
147    /// Run the executor in a specific `Schedule` and `SystemSet`.
148    pub fn run_in_set(mut self, schedule: impl ScheduleLabel, set: impl SystemSet) -> Self {
149        self.schedules.push((
150            Interned(Box::leak(Box::new(schedule))),
151            Some(Interned(Box::leak(Box::new(set)))),
152        ));
153        self
154    }
155
156    /// Run systems to match keys. By default will match keys if resource
157    /// `ButtonInput<KeyCode>` exists.
158    pub fn match_key(mut self, yes: bool) -> Self {
159        self.match_key = Some(yes);
160        self
161    }
162
163    /// Run systems to match button. By default will match keys if resource
164    /// `ButtonInput<GamepadButton>` exists.
165    pub fn match_button(mut self, yes: bool) -> Self {
166        self.match_button = Some(yes);
167        self
168    }
169}
170
171fn detect_key_additions(
172    sequences: Query<&InputSequence<KeyChord, ()>, Added<InputSequence<KeyChord, ()>>>,
173    mut cache: ResMut<KeySequenceCache>,
174) {
175    if sequences.iter().next().is_some() {
176        cache.reset();
177    }
178}
179
180#[allow(clippy::type_complexity)]
181fn detect_button_additions(
182    sequences: Query<
183        &InputSequence<GamepadButton, In<Entity>>,
184        Added<InputSequence<GamepadButton, In<Entity>>>,
185    >,
186    mut cache: ResMut<ButtonSequenceCache>,
187) {
188    if sequences.iter().next().is_some() {
189        cache.reset();
190    }
191}
192
193fn detect_key_removals(
194    mut cache: ResMut<KeySequenceCache>,
195    mut removals: RemovedComponents<InputSequence<KeyChord, ()>>,
196) {
197    if removals.read().next().is_some() {
198        cache.reset();
199    }
200}
201
202fn detect_button_removals(
203    mut cache: ResMut<ButtonSequenceCache>,
204    mut removals: RemovedComponents<InputSequence<GamepadButton, In<Entity>>>,
205) {
206    if removals.read().next().is_some() {
207        cache.reset();
208    }
209}
210
211#[allow(clippy::too_many_arguments)]
212fn button_sequence_matcher(
213    sequences: Query<&ButtonSequence>,
214    time: Res<Time>,
215    mut last_times: Local<HashMap<Entity, VecDeque<FrameTime>>>,
216    mut cache: ResMut<ButtonSequenceCache>,
217    frame_count: Res<FrameCount>,
218    mut commands: Commands,
219    gamepads: Query<(Entity, &Gamepad)>,
220) {
221    let now = FrameTime {
222        frame: frame_count.0,
223        time: time.elapsed_secs(),
224    };
225    for (id, gamepad) in &gamepads {
226        for button in gamepad.get_just_pressed() {
227            let last_times = match last_times.get_mut(&id) {
228                Some(x) => x,
229                None => {
230                    last_times.insert(id, VecDeque::new());
231                    last_times.get_mut(&id).unwrap()
232                }
233            };
234
235            last_times.push_back(now.clone());
236            let start = &last_times[0];
237            let mut search = cache.recall(id, sequences.iter().by_ref());
238            for seq in inc_consume_input(&mut search, std::iter::once(*button)) {
239                if seq
240                    .time_limit
241                    .as_ref()
242                    .map(|limit| (&now - start).has_timedout(limit))
243                    .unwrap_or(false)
244                {
245                    // Sequence timed out.
246                } else {
247                    commands.run_system_with(seq.system_id, id);
248                }
249            }
250            let prefix_len = search.prefix_len();
251            let l = last_times.len();
252            let _ = last_times.drain(0..l - prefix_len);
253            let position = search.into();
254            cache.store(id, position);
255        }
256    }
257}
258
259#[allow(clippy::too_many_arguments)]
260fn key_sequence_matcher(
261    sequences: Query<&KeySequence>,
262    time: Res<Time>,
263    keys: Res<ButtonInput<KeyCode>>,
264    mut last_times: Local<VecDeque<FrameTime>>,
265    mut cache: ResMut<KeySequenceCache>,
266    frame_count: Res<FrameCount>,
267    mut commands: Commands,
268    mut keychord_queue: ResMut<KeyChordQueue>,
269) {
270    let mods = Modifiers::from(&keys);
271    let now = FrameTime {
272        frame: frame_count.0,
273        time: time.elapsed_secs(),
274    };
275    let maybe_start = last_times.front().cloned();
276    let mut input = keychord_queue
277        .drain(..)
278        .chain(
279            keys.get_just_pressed()
280                .filter(|k| !is_modifier(**k))
281                .map(|k| {
282                    let chord = KeyChord(mods, *k);
283                    last_times.push_back(now.clone());
284                    chord
285                }),
286        )
287        .peekable();
288    if input.peek().is_none() {
289        return;
290    }
291
292    let mut search = cache.recall(sequences.iter());
293
294    // eprintln!("maybe_start {maybe_start:?} now {now:?}");
295    for seq in inc_consume_input(&mut search, input) {
296        if let Some(ref start) = maybe_start {
297            if seq
298                .time_limit
299                .as_ref()
300                .map(|limit| (&now - start).has_timedout(limit))
301                .unwrap_or(false)
302            {
303                // Sequence timed out.
304                continue;
305            }
306        }
307        commands.run_system(seq.system_id);
308    }
309    let prefix_len = search.prefix_len();
310    let l = last_times.len();
311    let _ = last_times.drain(0..l.saturating_sub(prefix_len));
312    let position = search.into();
313    cache.store(position);
314}
315
316/// Incrementally consume the input.
317fn inc_consume_input<'a, 'b, K, V>(
318    search: &'b mut IncSearch<'a, K, V>,
319    input: impl Iterator<Item = K> + 'b,
320) -> impl Iterator<Item = &'a V> + 'b
321where
322    K: Clone + Eq + Ord,
323    'a: 'b,
324{
325    input.filter_map(move |k| {
326        match search.query(&k) {
327            Some(Answer::Match) => {
328                let result = Some(search.value().unwrap());
329                search.reset();
330                result
331            }
332            Some(Answer::PrefixAndMatch) => Some(search.value().unwrap()),
333            Some(Answer::Prefix) => None,
334            None => {
335                search.reset();
336                // This could be the start of a new sequence.
337                //
338                // Let's check it.
339                match search.query(&k) {
340                    Some(Answer::Match) => {
341                        let result = Some(search.value().unwrap());
342                        search.reset();
343                        result
344                    }
345                    Some(Answer::PrefixAndMatch) => Some(search.value().unwrap()),
346                    Some(Answer::Prefix) => None,
347                    None => {
348                        // This may not be necessary.
349                        search.reset();
350                        None
351                    }
352                }
353            }
354        }
355    })
356}