use std::{marker::PhantomData, ops::Deref, usize}; use bevy_app::prelude::*; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_sequential_actions::*; #[derive(Deref, DerefMut)] struct TestApp(App); impl TestApp { fn new() -> Self { let mut app = App::new(); app.init_resource::() .add_plugins(SequentialActionsPlugin) .add_systems(Update, (countdown, countup)); Self(app) } fn spawn_agent(&mut self) -> Entity { self.world_mut().spawn(SequentialActions).id() } fn actions(&mut self, agent: Entity) -> impl ModifyActions + use<'_> { self.world_mut().actions(agent) } fn hooks(&self) -> &Hooks { self.world().resource::() } fn hooks_mut(&mut self) -> Mut<'_, Hooks> { self.world_mut().resource_mut::() } fn entity(&self, entity: Entity) -> EntityRef<'_> { self.world().entity(entity) } fn get_entity(&self, entity: Entity) -> Option> { self.world().get_entity(entity).ok() } fn current_action(&self, agent: Entity) -> &CurrentAction { self.world().get::(agent).unwrap() } fn action_queue(&self, agent: Entity) -> &ActionQueue { self.world().get::(agent).unwrap() } fn despawn(&mut self, entity: Entity) -> bool { self.world_mut().despawn(entity) } fn reset(&mut self) -> &mut Self { self.world_mut().clear_entities(); self.world_mut().resource_mut::().clear(); self } } #[derive(Debug, Default, Resource, Deref, DerefMut)] struct Hooks(Vec); #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Hook { Add(Name, Entity), Start(Name, Entity), Stop(Name, Option, StopReason), Remove(Name, Option), Drop(Name, Option, DropReason), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Name { Countdown, Countup, Despawn, GoodAdd, BadAdd, } impl Name { fn on_add(self, agent: Entity, world: &mut World) { world.resource_mut::().push(Hook::Add(self, agent)); } fn on_start(self, agent: Entity, world: &mut World) { world.resource_mut::().push(Hook::Start(self, agent)); } fn on_stop(self, agent: Option, world: &mut World, reason: StopReason) { world .resource_mut::() .push(Hook::Stop(self, agent, reason)); } fn on_remove(self, agent: Option, world: &mut World) { world .resource_mut::() .push(Hook::Remove(self, agent)); } fn on_drop(self, agent: Option, world: &mut World, reason: DropReason) { world .resource_mut::() .push(Hook::Drop(self, agent, reason)); } } struct CountdownAction { count: i32, current: Option, } impl CountdownAction { const fn new(count: i32) -> Self { Self { count, current: None, } } } impl Action for CountdownAction { fn is_finished(&self, agent: Entity, world: &World) -> bool { world.get::(agent).unwrap().0 <= 0 } fn on_add(&mut self, agent: Entity, world: &mut World) { Name::Countdown.on_add(agent, world); } fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { Name::Countdown.on_start(agent, world); let count = self.current.take().unwrap_or(self.count); world.entity_mut(agent).insert(Countdown(count)); self.is_finished(agent, world) } fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { Name::Countdown.on_stop(agent, world, reason); let Some(agent) = agent else { return }; let countdown = world.entity_mut(agent).take::(); if reason == StopReason::Paused { self.current = Some(countdown.unwrap().0); } } fn on_remove(&mut self, agent: Option, world: &mut World) { Name::Countdown.on_remove(agent, world); } fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { Name::Countdown.on_drop(agent, world, reason); } } #[derive(Component)] struct Countdown(i32); fn countdown(mut countdown_q: Query<&mut Countdown>) { for mut countdown in &mut countdown_q { countdown.0 -= 1; } } struct CountupAction { count: i32, current: Option, } impl CountupAction { const fn new(count: i32) -> Self { Self { count, current: None, } } } impl Action for CountupAction { fn is_finished(&self, agent: Entity, world: &World) -> bool { world.get::(agent).unwrap().0 >= self.count } fn on_add(&mut self, agent: Entity, world: &mut World) { Name::Countup.on_add(agent, world); } fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { Name::Countup.on_start(agent, world); let count = self.current.take().unwrap_or_default(); world.entity_mut(agent).insert(Countup(count)); self.is_finished(agent, world) } fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { Name::Countup.on_stop(agent, world, reason); let Some(agent) = agent else { return }; let countup = world.entity_mut(agent).take::(); if reason == StopReason::Paused { self.current = Some(countup.unwrap().0); } } fn on_remove(&mut self, agent: Option, world: &mut World) { Name::Countup.on_remove(agent, world); } fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { Name::Countup.on_drop(agent, world, reason); } } #[derive(Component)] struct Countup(i32); fn countup(mut countup_q: Query<&mut Countup>) { for mut countup in &mut countup_q { countup.0 += 1; } } #[test] fn spawn_and_remove() { let mut app = TestApp::new(); let a = app.world_mut().spawn(SequentialActions).id(); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), true); app.world_mut().entity_mut(a).remove::(); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), true); app.world_mut() .entity_mut(a) .remove_with_requires::(); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), false); } #[test] fn add() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).start(false).add(CountdownAction::new(0)); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 1); assert_eq!( app.hooks().deref().clone(), vec![Hook::Add(Name::Countdown, a)] ); app.actions(a).clear().add(CountdownAction::new(0)); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 0); app.actions(a).clear().add(CountdownAction::new(1)); assert!(app.current_action(a).is_some()); assert_eq!(app.action_queue(a).len(), 0); app.reset().actions(a).add(CountdownAction::new(1)); assert!(app.get_entity(a).is_none()); assert_eq!(app.hooks().deref().clone(), vec![]); } #[test] fn add_many() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a) .start(false) .add((CountdownAction::new(0), CountupAction::new(0))); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 2); assert_eq!( app.hooks().deref().clone(), vec![Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a)] ); app.actions(a) .clear() .add((CountdownAction::new(0), CountupAction::new(0))); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 0); app.actions(a) .add(actions![CountdownAction::new(1), CountupAction::new(1)]); assert!(app.current_action(a).is_some()); assert_eq!(app.action_queue(a).len(), 1); app.actions(a).add([]); assert!(app.current_action(a).is_some()); assert_eq!(app.action_queue(a).len(), 1); app.reset() .actions(a) .add(actions![CountdownAction::new(1), CountupAction::new(1)]); assert!(app.get_entity(a).is_none()); assert_eq!(app.hooks().deref().clone(), vec![]); } #[test] fn finish() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(CountdownAction::new(0)); assert!(app.current_action(a).is_none()); assert!(app.action_queue(a).is_empty()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Finished), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done) ] ); let a = app.reset().spawn_agent(); app.actions(a).add(CountdownAction::new(1)); app.update(); assert!(app.current_action(a).is_none()); assert!(app.action_queue(a).is_empty()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Finished), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done) ] ); let a = app.reset().spawn_agent(); app.actions(a) .add((CountdownAction::new(0), CountupAction::new(0))); assert!(app.current_action(a).is_none()); assert!(app.action_queue(a).is_empty()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Finished), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done), Hook::Start(Name::Countup, a), Hook::Stop(Name::Countup, Some(a), StopReason::Finished), Hook::Remove(Name::Countup, Some(a)), Hook::Drop(Name::Countup, Some(a), DropReason::Done) ] ); let a = app.reset().spawn_agent(); app.actions(a) .add((CountdownAction::new(1), CountupAction::new(1))); app.update(); assert!(app.current_action(a).is_some()); assert_eq!(app.action_queue(a).len(), 0); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Finished), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done), Hook::Start(Name::Countup, a) ] ); } #[test] fn cancel() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(CountdownAction::new(1)).cancel(); assert!(app.current_action(a).is_none()); assert!(app.action_queue(a).is_empty()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Canceled), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done) ] ); app.reset().actions(a).cancel(); } #[test] fn pause() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(CountdownAction::new(1)).pause(); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 1); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Paused) ] ); app.reset().actions(a).pause(); } #[test] fn next() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(CountdownAction::new(1)).next(); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 0); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Canceled), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done) ] ); app.reset().actions(a).next(); } #[test] fn skip() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a) .start(false) .add(CountdownAction::new(1)) .skip(0); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 1); assert_eq!( app.hooks().deref().clone(), vec![Hook::Add(Name::Countdown, a)] ); app.actions(a).add(CountupAction::new(1)).skip(1); assert!(app.current_action(a).is_some()); assert_eq!(app.action_queue(a).len(), 0); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Countdown, a), Hook::Remove(Name::Countup, Some(a)), Hook::Drop(Name::Countup, Some(a), DropReason::Skipped) ] ); app.actions(a).clear(); app.hooks_mut().clear(); app.actions(a) .start(false) .add(( CountdownAction::new(1), CountupAction::new(1), CountupAction::new(1), )) .skip(2); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 1); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Add(Name::Countup, a), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Skipped), Hook::Remove(Name::Countup, Some(a)), Hook::Drop(Name::Countup, Some(a), DropReason::Skipped), ] ); app.reset().actions(a).skip(usize::MAX); } #[test] fn clear() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a) .add((CountdownAction::new(1), CountupAction::new(1))) .clear(); assert!(app.current_action(a).is_none()); assert_eq!(app.action_queue(a).len(), 0); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, Some(a), StopReason::Canceled), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Cleared), Hook::Remove(Name::Countup, Some(a)), Hook::Drop(Name::Countup, Some(a), DropReason::Cleared) ] ); app.reset().actions(a).clear(); } #[test] fn order() { #[derive(Default)] struct MarkerAction(PhantomData); impl Action for MarkerAction { fn is_finished(&self, _agent: Entity, _world: &World) -> bool { true } fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { world.entity_mut(agent).insert(M::default()); false } fn on_stop(&mut self, agent: Option, world: &mut World, _reason: StopReason) { world.entity_mut(agent.unwrap()).remove::(); } } #[derive(Default, Component)] struct A; #[derive(Default, Component)] struct B; #[derive(Default, Component)] struct C; let mut app = TestApp::new(); let a = app.spawn_agent(); // Back app.actions(a).add(( MarkerAction::::default(), MarkerAction::::default(), MarkerAction::::default(), )); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), false); app.update(); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), false); app.update(); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), true); // Front app.actions(a) .clear() .start(false) .add(|_a, _w: &mut World| false) .order(AddOrder::Front) .add(( MarkerAction::::default(), MarkerAction::::default(), MarkerAction::::default(), )) .execute(); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), false); app.update(); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), true); assert_eq!(app.entity(a).contains::(), false); app.update(); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), false); assert_eq!(app.entity(a).contains::(), true); } #[test] fn pause_resume() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(CountdownAction::new(10)); assert_eq!(app.entity(a).get::().unwrap().0, 10); app.update(); assert_eq!(app.entity(a).get::().unwrap().0, 9); app.actions(a) .pause() .order(AddOrder::Front) .add(CountdownAction::new(1)); assert_eq!(app.entity(a).get::().unwrap().0, 1); app.update(); assert_eq!(app.entity(a).get::().unwrap().0, 9); } #[test] fn despawn() { let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a) .add((CountdownAction::new(10), CountupAction::new(10))); app.update(); app.despawn(a); assert!(app.get_entity(a).is_none()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Countdown, a), Hook::Stop(Name::Countdown, None, StopReason::Canceled), Hook::Remove(Name::Countdown, None), Hook::Drop(Name::Countdown, None, DropReason::Done), Hook::Remove(Name::Countup, None), Hook::Drop(Name::Countup, None, DropReason::Cleared) ] ); } #[test] fn despawn_action() { struct DespawnAction; impl Action for DespawnAction { fn is_finished(&self, _agent: Entity, _world: &World) -> bool { true } fn on_add(&mut self, agent: Entity, world: &mut World) { Name::Despawn.on_add(agent, world); } fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { Name::Despawn.on_start(agent, world); world.despawn(agent); B } fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { Name::Despawn.on_stop(agent, world, reason); } fn on_remove(&mut self, agent: Option, world: &mut World) { Name::Despawn.on_remove(agent, world); } fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { Name::Despawn.on_drop(agent, world, reason); } } let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(DespawnAction::); assert!(app.get_entity(a).is_none()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Despawn, a), Hook::Start(Name::Despawn, a), Hook::Stop(Name::Despawn, None, StopReason::Finished), Hook::Remove(Name::Despawn, None), Hook::Drop(Name::Despawn, None, DropReason::Done) ] ); let a = app.reset().spawn_agent(); app.actions(a).add(DespawnAction::); assert!(app.get_entity(a).is_none()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Despawn, a), Hook::Start(Name::Despawn, a), Hook::Stop(Name::Despawn, None, StopReason::Canceled), Hook::Remove(Name::Despawn, None), Hook::Drop(Name::Despawn, None, DropReason::Done) ] ); let a = app.reset().spawn_agent(); app.actions(a).add(( DespawnAction::, CountdownAction::new(1), CountupAction::new(1), )); assert!(app.get_entity(a).is_none()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Despawn, a), Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Despawn, a), // After despawn, the bevy ecs on_remove component hook is triggered Hook::Remove(Name::Countdown, None), Hook::Drop(Name::Countdown, None, DropReason::Cleared), Hook::Remove(Name::Countup, None), Hook::Drop(Name::Countup, None, DropReason::Cleared), // Back to DespawnAction Hook::Stop(Name::Despawn, None, StopReason::Finished), Hook::Remove(Name::Despawn, None), Hook::Drop(Name::Despawn, None, DropReason::Done) ] ); let a = app.reset().spawn_agent(); app.actions(a).add(( DespawnAction::, CountdownAction::new(1), CountupAction::new(1), )); assert!(app.get_entity(a).is_none()); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::Despawn, a), Hook::Add(Name::Countdown, a), Hook::Add(Name::Countup, a), Hook::Start(Name::Despawn, a), // After despawn, the bevy ecs on_remove component hook is triggered Hook::Remove(Name::Countdown, None), Hook::Drop(Name::Countdown, None, DropReason::Cleared), Hook::Remove(Name::Countup, None), Hook::Drop(Name::Countup, None, DropReason::Cleared), // Back to DespawnAction Hook::Stop(Name::Despawn, None, StopReason::Canceled), Hook::Remove(Name::Despawn, None), Hook::Drop(Name::Despawn, None, DropReason::Done) ] ); } #[test] fn good_add_action() { struct GoodAddAction; impl Action for GoodAddAction { fn is_finished(&self, _agent: Entity, _world: &World) -> bool { true } fn on_add(&mut self, agent: Entity, world: &mut World) { Name::GoodAdd.on_add(agent, world); } fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { Name::GoodAdd.on_start(agent, world); world .actions(agent) .start(false) .add(CountdownAction::new(1)); true } fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { Name::GoodAdd.on_stop(agent, world, reason); } fn on_remove(&mut self, agent: Option, world: &mut World) { Name::GoodAdd.on_remove(agent, world); } fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { Name::GoodAdd.on_drop(agent, world, reason); } } let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(GoodAddAction); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::GoodAdd, a), Hook::Start(Name::GoodAdd, a), Hook::Add(Name::Countdown, a), Hook::Stop(Name::GoodAdd, Some(a), StopReason::Finished), Hook::Remove(Name::GoodAdd, Some(a)), Hook::Drop(Name::GoodAdd, Some(a), DropReason::Done), Hook::Start(Name::Countdown, a) ] ); } #[test] fn bad_add_action() { struct BadAddAction; impl Action for BadAddAction { fn is_finished(&self, _agent: Entity, _world: &World) -> bool { true } fn on_add(&mut self, agent: Entity, world: &mut World) { Name::BadAdd.on_add(agent, world); } fn on_start(&mut self, agent: Entity, world: &mut World) -> bool { Name::BadAdd.on_start(agent, world); true } fn on_stop(&mut self, agent: Option, world: &mut World, reason: StopReason) { Name::BadAdd.on_stop(agent, world, reason); world.actions(agent.unwrap()).add(CountdownAction::new(1)); } fn on_remove(&mut self, agent: Option, world: &mut World) { Name::BadAdd.on_remove(agent, world); } fn on_drop(self: Box, agent: Option, world: &mut World, reason: DropReason) { Name::BadAdd.on_drop(agent, world, reason); } } let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(BadAddAction); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Add(Name::BadAdd, a), Hook::Start(Name::BadAdd, a), Hook::Stop(Name::BadAdd, Some(a), StopReason::Finished), Hook::Add(Name::Countdown, a), Hook::Start(Name::Countdown, a), Hook::Remove(Name::BadAdd, Some(a)), Hook::Drop(Name::BadAdd, Some(a), DropReason::Done) ] ); app.hooks_mut().clear(); app.update(); assert_eq!( app.hooks().deref().clone(), vec![ Hook::Stop(Name::Countdown, Some(a), StopReason::Finished), Hook::Remove(Name::Countdown, Some(a)), Hook::Drop(Name::Countdown, Some(a), DropReason::Done) ] ); } #[test] #[should_panic] #[cfg(debug_assertions)] fn forever_action() { struct ForeverAction; impl Action for ForeverAction { fn is_finished(&self, _agent: Entity, _world: &World) -> bool { true } fn on_start(&mut self, _agent: Entity, _world: &mut World) -> bool { true } fn on_stop(&mut self, _agent: Option, _world: &mut World, _reason: StopReason) {} fn on_drop(self: Box, agent: Option, world: &mut World, _reason: DropReason) { world .actions(agent.unwrap()) .start(false) .add(self as BoxedAction); } } let mut app = TestApp::new(); let a = app.spawn_agent(); app.actions(a).add(ForeverAction); }