bevy_tnua/
controller.rs

1use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
2use bevy::platform::collections::hash_map::Entry;
3use bevy::platform::collections::HashMap;
4use bevy::prelude::*;
5use bevy::time::Stopwatch;
6use bevy_tnua_physics_integration_layer::math::{
7    AdjustPrecision, AsF32, Float, Quaternion, Vector3,
8};
9
10use crate::basis_action_traits::{
11    BoxableAction, BoxableBasis, DynamicAction, DynamicBasis, TnuaAction, TnuaActionContext,
12    TnuaActionInitiationDirective, TnuaActionLifecycleDirective, TnuaActionLifecycleStatus,
13    TnuaBasisContext,
14};
15use crate::{
16    TnuaBasis, TnuaMotor, TnuaPipelineStages, TnuaProximitySensor, TnuaRigidBodyTracker,
17    TnuaSystemSet, TnuaToggle, TnuaUserControlsSystemSet,
18};
19
20/// The main for supporting Tnua character controller.
21///
22/// Will not work without a physics backend plugin (like `TnuaRapier2dPlugin` or
23/// `TnuaRapier3dPlugin`)
24///
25/// Make sure the schedule for this plugin, the physics backend plugin, and the physics backend
26/// itself are all using the same timestep. This usually means that the physics backend is in e.g.
27/// `FixedPostUpdate` and the Tnua plugins are at `PostUpdate`.
28///
29/// **DO NOT mix `Update` with `FixedUpdate`!** This will mess up Tnua's calculations, resulting in
30/// very unstable character motion.
31pub struct TnuaControllerPlugin {
32    schedule: InternedScheduleLabel,
33}
34
35impl TnuaControllerPlugin {
36    pub fn new(schedule: impl ScheduleLabel) -> Self {
37        Self {
38            schedule: schedule.intern(),
39        }
40    }
41}
42
43impl Plugin for TnuaControllerPlugin {
44    fn build(&self, app: &mut App) {
45        app.configure_sets(
46            self.schedule,
47            (
48                TnuaPipelineStages::Sensors,
49                TnuaPipelineStages::SubservientSensors,
50                TnuaUserControlsSystemSet,
51                TnuaPipelineStages::Logic,
52                TnuaPipelineStages::Motors,
53            )
54                .chain()
55                .in_set(TnuaSystemSet),
56        );
57        app.add_systems(
58            self.schedule,
59            apply_controller_system.in_set(TnuaPipelineStages::Logic),
60        );
61    }
62}
63
64struct FedEntry {
65    fed_this_frame: bool,
66    rescheduled_in: Option<Timer>,
67}
68
69/// The main component used for interaction with the controls and animation code.
70///
71/// Every frame, the game code should feed input this component on every controlled entity. What
72/// should be fed is:
73///
74/// * A basis - this is the main movement command - usually
75///   [`TnuaBuiltinWalk`](crate::builtins::TnuaBuiltinWalk), but there can be others. It is the
76///   game code's responsibility to ensure only one basis is fed at any given time, because basis
77///   can hold state and replacing the basis type restarts the state.
78///
79///   Refer to the documentation of [the implementors of
80///   `TnuaBasis`](crate::TnuaBasis#implementors) for more information.
81///
82/// * Zero or more actions - these are movements like jumping, dashing, crouching, etc. Multiple
83///   actions can be fed, but only one can be active at any given moment. Unlike basis, there is a
84///   smart mechanism for deciding which action to use and which to discard, so it is safe to feed
85///   many actions at the same frame.
86///
87///   Refer to the documentation of [the implementors of
88///   `TnuaAction`](crate::TnuaAction#implementors) for more information.
89///
90/// Without [`TnuaControllerPlugin`] this component will not do anything.
91#[derive(Component, Default)]
92#[require(TnuaMotor, TnuaRigidBodyTracker, TnuaProximitySensor)]
93pub struct TnuaController {
94    current_basis: Option<(&'static str, Box<dyn DynamicBasis>)>,
95    actions_being_fed: HashMap<&'static str, FedEntry>,
96    current_action: Option<(&'static str, Box<dyn DynamicAction>)>,
97    contender_action: Option<(&'static str, Box<dyn DynamicAction>, Stopwatch)>,
98    action_flow_status: TnuaActionFlowStatus,
99    up_direction: Option<Dir3>,
100}
101
102impl TnuaController {
103    /// Feed a basis - the main movement command - with [its default name](TnuaBasis::NAME).
104    pub fn basis<B: TnuaBasis>(&mut self, basis: B) {
105        self.named_basis(B::NAME, basis);
106    }
107
108    /// Feed a basis - the main movement command - with a custom name.
109    ///
110    /// This should only be used if the same basis type needs to be used with different names to
111    /// allow, for example, different animations. Otherwise prefer to use the default name with
112    /// [`basis`](Self::basis).
113    pub fn named_basis<B: TnuaBasis>(&mut self, name: &'static str, basis: B) {
114        if let Some((existing_name, existing_basis)) =
115            self.current_basis.as_mut().and_then(|(n, b)| {
116                let b = b.as_mut_any().downcast_mut::<BoxableBasis<B>>()?;
117                Some((n, b))
118            })
119        {
120            *existing_name = name;
121            existing_basis.input = basis;
122        } else {
123            self.current_basis = Some((name, Box::new(BoxableBasis::new(basis))));
124        }
125    }
126
127    /// Instruct the basis to pretend the user provided no input this frame.
128    ///
129    /// The exact meaning is defined in the basis' [`neutralize`](TnuaBasis::neutralize) method,
130    /// but generally it means that fields that typically come from a configuration will not be
131    /// touched, and only fields that are typically set by user input get nullified.
132    pub fn neutralize_basis(&mut self) {
133        if let Some((_, basis)) = self.current_basis.as_mut() {
134            basis.neutralize();
135        }
136    }
137
138    /// The name of the currently running basis.
139    ///
140    /// When using the basis with it's default name, prefer to match this against
141    /// [`TnuaBasis::NAME`] and not against a string literal.
142    pub fn basis_name(&self) -> Option<&'static str> {
143        self.current_basis
144            .as_ref()
145            .map(|(basis_name, _)| *basis_name)
146    }
147
148    /// A dynamic accessor to the currently running basis.
149    pub fn dynamic_basis(&self) -> Option<&dyn DynamicBasis> {
150        Some(self.current_basis.as_ref()?.1.as_ref())
151    }
152
153    /// The currently running basis, together with its state.
154    ///
155    /// This is mainly useful for animation. When multiple basis types are used in the game,
156    /// [`basis_name`](Self::basis_name) be used to determine the type of the current basis first,
157    /// to avoid having to try multiple downcasts.
158    pub fn concrete_basis<B: TnuaBasis>(&self) -> Option<(&B, &B::State)> {
159        let (_, basis) = self.current_basis.as_ref()?;
160        let boxable_basis: &BoxableBasis<B> = basis.as_any().downcast_ref()?;
161        Some((&boxable_basis.input, &boxable_basis.state))
162    }
163
164    /// The currently running basis, together with its state, as mutable.
165    /// Useful if you need to touch the state of a running action to respond to game events.
166    pub fn concrete_basis_mut<B: TnuaBasis>(&mut self) -> Option<(&B, &mut B::State)> {
167        let (_, basis) = self.current_basis.as_mut()?;
168        let boxable_basis: &mut BoxableBasis<B> = basis.as_mut_any().downcast_mut()?;
169        Some((&boxable_basis.input, &mut boxable_basis.state))
170    }
171
172    /// Feed an action with [its default name](TnuaBasis::NAME).
173    pub fn action<A: TnuaAction>(&mut self, action: A) {
174        self.named_action(A::NAME, action);
175    }
176
177    /// Feed an action with a custom name.
178    ///
179    /// This should only be used if the same action type needs to be used with different names to
180    /// allow, for example, different animations. Otherwise prefer to use the default name with
181    /// [`action`](Self::action).
182    pub fn named_action<A: TnuaAction>(&mut self, name: &'static str, action: A) {
183        match self.actions_being_fed.entry(name) {
184            Entry::Occupied(mut entry) => {
185                entry.get_mut().fed_this_frame = true;
186                if let Some((current_name, current_action)) = self.current_action.as_mut() {
187                    if *current_name == name {
188                        let Some(current_action) = current_action
189                            .as_mut_any()
190                            .downcast_mut::<BoxableAction<A>>()
191                        else {
192                            panic!("Multiple action types registered with same name {name:?}");
193                        };
194                        current_action.input = action;
195                    } else {
196                        // different action is running - will not override because button was
197                        // already pressed.
198                    }
199                } else if self.contender_action.is_none()
200                    && entry
201                        .get()
202                        .rescheduled_in
203                        .as_ref()
204                        .is_some_and(|timer| timer.finished())
205                {
206                    // no action is running - but this action is rescheduled and there is no
207                    // already-existing contender that would have taken priority
208                    self.contender_action =
209                        Some((name, Box::new(BoxableAction::new(action)), Stopwatch::new()));
210                } else {
211                    // no action is running - will not set because button was already pressed.
212                }
213            }
214            Entry::Vacant(entry) => {
215                entry.insert(FedEntry {
216                    fed_this_frame: true,
217                    rescheduled_in: None,
218                });
219                if let Some(contender_action) = self.contender_action.as_mut().and_then(
220                    |(contender_name, contender_action, _)| {
221                        if *contender_name == name {
222                            let Some(contender_action) = contender_action
223                                .as_mut_any()
224                                .downcast_mut::<BoxableAction<A>>()
225                            else {
226                                panic!("Multiple action types registered with same name {name:?}");
227                            };
228                            Some(contender_action)
229                        } else {
230                            None
231                        }
232                    },
233                ) {
234                    contender_action.input = action;
235                } else {
236                    self.contender_action =
237                        Some((name, Box::new(BoxableAction::new(action)), Stopwatch::new()));
238                }
239            }
240        }
241    }
242
243    /// Re-feed the same action that is currently active.
244    ///
245    /// This is useful when matching on [`action_name`](Self::action_name) and wanting to continue
246    /// feeding the **exact same** action with the **exact same** input without having to use
247    /// [`concrete_action`](Self::concrete_action).
248    pub fn prolong_action(&mut self) {
249        if let Some((current_name, _)) = self.current_action {
250            if let Some(fed_action) = self.actions_being_fed.get_mut(current_name) {
251                fed_action.fed_this_frame = true;
252            }
253        }
254    }
255
256    /// The name of the currently running action.
257    ///
258    /// When using an action with it's default name, prefer to match this against
259    /// [`TnuaAction::NAME`] and not against a string literal.
260    pub fn action_name(&self) -> Option<&'static str> {
261        self.current_action
262            .as_ref()
263            .map(|(action_name, _)| *action_name)
264    }
265
266    /// A dynamic accessor to the currently running action.
267    pub fn dynamic_action(&self) -> Option<&dyn DynamicAction> {
268        Some(self.current_action.as_ref()?.1.as_ref())
269    }
270
271    /// The currently running action, together with its state.
272    ///
273    /// This is mainly useful for animation. When multiple action types are used in the game,
274    /// [`action_name`](Self::action_name) be used to determine the type of the current action
275    /// first, to avoid having to try multiple downcasts.
276    pub fn concrete_action<A: TnuaAction>(&self) -> Option<(&A, &A::State)> {
277        let (_, action) = self.current_action.as_ref()?;
278        let boxable_action: &BoxableAction<A> = action.as_any().downcast_ref()?;
279        Some((&boxable_action.input, &boxable_action.state))
280    }
281
282    /// The currently running action, together with its state, as mutable.
283    /// Useful if you need to touch the state of a running action to respond to game events.
284    ///
285    /// If the action is replaced, the state will be lost. If you need to keep the state, you should
286    /// store it separately.
287    pub fn concrete_action_mut<A: TnuaAction>(&mut self) -> Option<(&A, &mut A::State)> {
288        let (_, action) = self.current_action.as_mut()?;
289        let boxable_action: &mut BoxableAction<A> = action.as_mut_any().downcast_mut()?;
290        Some((&boxable_action.input, &mut boxable_action.state))
291    }
292
293    /// Indicator for the state and flow of movement actions.
294    ///
295    /// Query this every frame to keep track of the actions. For air actions,
296    /// [`TnuaAirActionsTracker`](crate::control_helpers::TnuaAirActionsTracker) is easier to use
297    /// (and uses this behind the scenes)
298    ///
299    /// The benefits of this over querying [`action_name`](Self::action_name) every frame are:
300    ///
301    /// * `action_flow_status` can indicate when the same action has been fed again immediately
302    ///   after stopping or cancelled into itself.
303    /// * `action_flow_status` shows an [`ActionEnded`](TnuaActionFlowStatus::ActionEnded) when the
304    ///   action is no longer fed, even if the action is still active (termination sequence)
305    pub fn action_flow_status(&self) -> &TnuaActionFlowStatus {
306        &self.action_flow_status
307    }
308
309    /// Checks if the character is currently airborne.
310    ///
311    /// The check is done based on the basis, and is equivalent to getting the controller's
312    /// [`dynamic_basis`](Self::dynamic_basis) and checking its
313    /// [`is_airborne`](TnuaBasis::is_airborne) method.
314    pub fn is_airborne(&self) -> Result<bool, TnuaControllerHasNoBasis> {
315        match self.dynamic_basis() {
316            Some(basis) => Ok(basis.is_airborne()),
317            None => Err(TnuaControllerHasNoBasis),
318        }
319    }
320
321    /// Returns the direction considered as up.
322    ///
323    /// Note that the up direction is based on gravity, as reported by
324    /// [`TnuaRigidBodyTracker::gravity`], and that it'd typically be one frame behind since it
325    /// gets updated in the same system that applies the controller logic. If this is unacceptable,
326    /// consider using [`TnuaRigidBodyTracker::gravity`] directly or deducing the up direction via
327    /// different means.
328    pub fn up_direction(&self) -> Option<Dir3> {
329        self.up_direction
330    }
331}
332
333#[derive(thiserror::Error, Debug)]
334#[error("The Tnua controller does not have any basis set")]
335pub struct TnuaControllerHasNoBasis;
336
337/// The result of [`TnuaController::action_flow_status()`].
338#[derive(Debug, Default, Clone)]
339pub enum TnuaActionFlowStatus {
340    /// No action is going on.
341    #[default]
342    NoAction,
343
344    /// An action just started.
345    ActionStarted(&'static str),
346
347    /// An action was fed in a past frame and is still ongoing.
348    ActionOngoing(&'static str),
349
350    /// An action has stopped being fed.
351    ///
352    /// Note that the action may still have a termination sequence after this happens.
353    ActionEnded(&'static str),
354
355    /// An action has just been canceled into another action.
356    Cancelled {
357        old: &'static str,
358        new: &'static str,
359    },
360}
361
362impl TnuaActionFlowStatus {
363    /// The name of the ongoing action, if there is an ongoing action.
364    ///
365    /// Will also return a value if the action has just started.
366    pub fn ongoing(&self) -> Option<&'static str> {
367        match self {
368            TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionEnded(_) => None,
369            TnuaActionFlowStatus::ActionStarted(action_name)
370            | TnuaActionFlowStatus::ActionOngoing(action_name)
371            | TnuaActionFlowStatus::Cancelled {
372                old: _,
373                new: action_name,
374            } => Some(action_name),
375        }
376    }
377
378    /// The name of the action that has just started this frame.
379    ///
380    /// Will return `None` if there is no action, or if the ongoing action has started in a past
381    /// frame.
382    pub fn just_starting(&self) -> Option<&'static str> {
383        match self {
384            TnuaActionFlowStatus::NoAction
385            | TnuaActionFlowStatus::ActionOngoing(_)
386            | TnuaActionFlowStatus::ActionEnded(_) => None,
387            TnuaActionFlowStatus::ActionStarted(action_name)
388            | TnuaActionFlowStatus::Cancelled {
389                old: _,
390                new: action_name,
391            } => Some(action_name),
392        }
393    }
394}
395
396#[allow(clippy::type_complexity)]
397fn apply_controller_system(
398    time: Res<Time>,
399    mut query: Query<(
400        &mut TnuaController,
401        &TnuaRigidBodyTracker,
402        &mut TnuaProximitySensor,
403        &mut TnuaMotor,
404        Option<&TnuaToggle>,
405    )>,
406) {
407    let frame_duration = time.delta().as_secs_f64() as Float;
408    if frame_duration == 0.0 {
409        return;
410    }
411    for (mut controller, tracker, mut sensor, mut motor, tnua_toggle) in query.iter_mut() {
412        match tnua_toggle.copied().unwrap_or_default() {
413            TnuaToggle::Disabled => continue,
414            TnuaToggle::SenseOnly => {}
415            TnuaToggle::Enabled => {}
416        }
417
418        let controller = controller.as_mut();
419
420        let up_direction = Dir3::new(-tracker.gravity.f32()).ok();
421        controller.up_direction = up_direction;
422        // TODO: support the case where there is no up direction at all?
423        let up_direction = up_direction.unwrap_or(Dir3::Y);
424
425        match controller.action_flow_status {
426            TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionOngoing(_) => {}
427            TnuaActionFlowStatus::ActionEnded(_) => {
428                controller.action_flow_status = TnuaActionFlowStatus::NoAction;
429            }
430            TnuaActionFlowStatus::ActionStarted(action_name)
431            | TnuaActionFlowStatus::Cancelled {
432                old: _,
433                new: action_name,
434            } => {
435                controller.action_flow_status = TnuaActionFlowStatus::ActionOngoing(action_name);
436            }
437        }
438
439        if let Some((_, basis)) = controller.current_basis.as_mut() {
440            let basis = basis.as_mut();
441            basis.apply(
442                TnuaBasisContext {
443                    frame_duration,
444                    tracker,
445                    proximity_sensor: sensor.as_ref(),
446                    up_direction,
447                },
448                motor.as_mut(),
449            );
450            let sensor_cast_range_for_basis = basis.proximity_sensor_cast_range();
451
452            // To streamline TnuaActionContext creation
453            let proximity_sensor = sensor.as_ref();
454
455            let has_valid_contender = if let Some((_, contender_action, being_fed_for)) =
456                &mut controller.contender_action
457            {
458                let initiation_decision = contender_action.initiation_decision(
459                    TnuaActionContext {
460                        frame_duration,
461                        tracker,
462                        proximity_sensor,
463                        basis,
464                        up_direction,
465                    },
466                    being_fed_for,
467                );
468                being_fed_for.tick(time.delta());
469                match initiation_decision {
470                    TnuaActionInitiationDirective::Reject => {
471                        controller.contender_action = None;
472                        false
473                    }
474                    TnuaActionInitiationDirective::Delay => false,
475                    TnuaActionInitiationDirective::Allow => true,
476                }
477            } else {
478                false
479            };
480
481            if let Some((name, current_action)) = controller.current_action.as_mut() {
482                let lifecycle_status = if has_valid_contender {
483                    TnuaActionLifecycleStatus::CancelledInto
484                } else if controller
485                    .actions_being_fed
486                    .get(name)
487                    .map(|fed_entry| fed_entry.fed_this_frame)
488                    .unwrap_or(false)
489                {
490                    TnuaActionLifecycleStatus::StillFed
491                } else {
492                    TnuaActionLifecycleStatus::NoLongerFed
493                };
494
495                let directive = current_action.apply(
496                    TnuaActionContext {
497                        frame_duration,
498                        tracker,
499                        proximity_sensor,
500                        basis,
501                        up_direction,
502                    },
503                    lifecycle_status,
504                    motor.as_mut(),
505                );
506                if current_action.violates_coyote_time() {
507                    basis.violate_coyote_time();
508                }
509                let reschedule_action =
510                    |actions_being_fed: &mut HashMap<&'static str, FedEntry>,
511                     after_seconds: Float| {
512                        if let Some(fed_entry) = actions_being_fed.get_mut(name) {
513                            fed_entry.rescheduled_in =
514                                Some(Timer::from_seconds(after_seconds.f32(), TimerMode::Once));
515                        }
516                    };
517                match directive {
518                    TnuaActionLifecycleDirective::StillActive => {
519                        if !lifecycle_status.is_active()
520                            && matches!(
521                                controller.action_flow_status,
522                                TnuaActionFlowStatus::ActionOngoing(_)
523                            )
524                        {
525                            controller.action_flow_status = TnuaActionFlowStatus::ActionEnded(name);
526                        }
527                    }
528                    TnuaActionLifecycleDirective::Finished
529                    | TnuaActionLifecycleDirective::Reschedule { .. } => {
530                        if let TnuaActionLifecycleDirective::Reschedule { after_seconds } =
531                            directive
532                        {
533                            reschedule_action(&mut controller.actions_being_fed, after_seconds);
534                        }
535                        controller.current_action = if has_valid_contender {
536                            let (contender_name, mut contender_action, _) = controller.contender_action.take().expect("has_valid_contender can only be true if contender_action is Some");
537                            if let Some(contender_fed_entry) =
538                                controller.actions_being_fed.get_mut(contender_name)
539                            {
540                                contender_fed_entry.rescheduled_in = None;
541                            }
542                            let contender_directive = contender_action.apply(
543                                TnuaActionContext {
544                                    frame_duration,
545                                    tracker,
546                                    proximity_sensor,
547                                    basis,
548                                    up_direction,
549                                },
550                                TnuaActionLifecycleStatus::CancelledFrom,
551                                motor.as_mut(),
552                            );
553                            if contender_action.violates_coyote_time() {
554                                basis.violate_coyote_time();
555                            }
556                            match contender_directive {
557                                TnuaActionLifecycleDirective::StillActive => {
558                                    if matches!(
559                                        controller.action_flow_status,
560                                        TnuaActionFlowStatus::ActionOngoing(_)
561                                    ) {
562                                        controller.action_flow_status =
563                                            TnuaActionFlowStatus::Cancelled {
564                                                old: name,
565                                                new: contender_name,
566                                            };
567                                    } else {
568                                        controller.action_flow_status =
569                                            TnuaActionFlowStatus::ActionStarted(contender_name);
570                                    }
571                                    Some((contender_name, contender_action))
572                                }
573                                TnuaActionLifecycleDirective::Finished => {
574                                    if matches!(
575                                        controller.action_flow_status,
576                                        TnuaActionFlowStatus::ActionOngoing(_)
577                                    ) {
578                                        controller.action_flow_status =
579                                            TnuaActionFlowStatus::ActionEnded(name);
580                                    }
581                                    None
582                                }
583                                TnuaActionLifecycleDirective::Reschedule { after_seconds } => {
584                                    if matches!(
585                                        controller.action_flow_status,
586                                        TnuaActionFlowStatus::ActionOngoing(_)
587                                    ) {
588                                        controller.action_flow_status =
589                                            TnuaActionFlowStatus::ActionEnded(name);
590                                    }
591                                    reschedule_action(
592                                        &mut controller.actions_being_fed,
593                                        after_seconds,
594                                    );
595                                    None
596                                }
597                            }
598                        } else {
599                            controller.action_flow_status = TnuaActionFlowStatus::ActionEnded(name);
600                            None
601                        };
602                    }
603                }
604            } else if has_valid_contender {
605                let (contender_name, mut contender_action, _) = controller
606                    .contender_action
607                    .take()
608                    .expect("has_valid_contender can only be true if contender_action is Some");
609                contender_action.apply(
610                    TnuaActionContext {
611                        frame_duration,
612                        tracker,
613                        proximity_sensor,
614                        basis,
615                        up_direction,
616                    },
617                    TnuaActionLifecycleStatus::Initiated,
618                    motor.as_mut(),
619                );
620                if contender_action.violates_coyote_time() {
621                    basis.violate_coyote_time();
622                }
623                controller.action_flow_status = TnuaActionFlowStatus::ActionStarted(contender_name);
624                controller.current_action = Some((contender_name, contender_action));
625            }
626
627            let sensor_case_range_for_action =
628                if let Some((_, current_action)) = &controller.current_action {
629                    current_action.proximity_sensor_cast_range()
630                } else {
631                    0.0
632                };
633
634            sensor.cast_range = sensor_cast_range_for_basis.max(sensor_case_range_for_action);
635            sensor.cast_direction = -up_direction;
636            // TODO: Maybe add the horizontal rotation as well somehow?
637            sensor.cast_shape_rotation =
638                Quaternion::from_rotation_arc(Vector3::Y, up_direction.adjust_precision())
639        }
640
641        // Cycle actions_being_fed
642        controller.actions_being_fed.retain(|_, fed_entry| {
643            if fed_entry.fed_this_frame {
644                fed_entry.fed_this_frame = false;
645                if let Some(rescheduled_in) = &mut fed_entry.rescheduled_in {
646                    rescheduled_in.tick(time.delta());
647                }
648                true
649            } else {
650                false
651            }
652        });
653
654        if let Some((contender_name, ..)) = controller.contender_action {
655            if !controller.actions_being_fed.contains_key(contender_name) {
656                controller.contender_action = None;
657            }
658        }
659    }
660}