|
| 1 | +//! Provides an API to add and remove plans associated with a specific entity. This is especially |
| 2 | +//! useful for managing plans for people that you want to cancel when the person dies. |
| 3 | +//! |
| 4 | +//! Keep in mind that every plan you want to be managed through this API *must* be added through |
| 5 | +//! this API. Thus, if you have a recurring plan that schedules its next run when it fires, it |
| 6 | +//! needs to schedule that next run through this API. |
| 7 | +//! |
| 8 | +//! To cancel a person's plans when they die, you will want to listen for the |
| 9 | +//! `PropertyChangeEvent<Person, Alive>`: |
| 10 | +//! |
| 11 | +//! ```rust |
| 12 | +//! context.subscribe_to_event::<PropertyChangeEvent<Person, Alive>>( |
| 13 | +//! |context, event| context.cancel_plans_for_entity(event.entity_id), |
| 14 | +//! ); |
| 15 | +//! ``` |
| 16 | +
|
| 17 | +use ixa::entity::entity_store::get_registered_entity_count; |
| 18 | +use ixa::plan::PlanId; |
| 19 | +use ixa::prelude::*; |
| 20 | +use ixa::{ContextBase, ExecutionPhase, HashMap}; |
| 21 | +use std::any::Any; |
| 22 | +use std::cell::OnceCell; |
| 23 | + |
| 24 | +define_data_plugin!(PlanIndexPlugin, EntityPlanIndexStore, |_context| { |
| 25 | + EntityPlanIndexStore::new() |
| 26 | +}); |
| 27 | + |
| 28 | +/// A lightweight data plugin container to store the entity plan indexes. |
| 29 | +/// This is essentially an implementation of `EntityStore` in "userland". |
| 30 | +struct EntityPlanIndexStore { |
| 31 | + /// The indexes each stored at their corresponding `E::id()`. |
| 32 | + indexes: Vec<OnceCell<Box<dyn Any>>>, |
| 33 | +} |
| 34 | + |
| 35 | +impl EntityPlanIndexStore { |
| 36 | + /// Creates a new [`EntityPlanIndexStore`], allocating the exact number of slots as there are |
| 37 | + /// registered [`Entity`]s. |
| 38 | + pub fn new() -> Self { |
| 39 | + let num_items = get_registered_entity_count(); |
| 40 | + Self { |
| 41 | + indexes: (0..num_items).map(|_| OnceCell::new()).collect(), |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + fn get_plan_index<E: Entity>(&self) -> &EntityPlanIndex<E> { |
| 46 | + let index = E::id(); |
| 47 | + let record = self.indexes.get(index).unwrap_or_else(|| { |
| 48 | + panic!( |
| 49 | + "No registered entity found with index = {index:?}. You must use the `define_entity!` macro to create an entity." |
| 50 | + ) |
| 51 | + }); |
| 52 | + let plan_index = record.get_or_init(|| { |
| 53 | + Box::new(EntityPlanIndex::<E>::default()) as Box<dyn Any> |
| 54 | + }); |
| 55 | + plan_index.downcast_ref::<EntityPlanIndex<E>>().expect( |
| 56 | + "TypeID does not match registered item type. You must use the `define_registered_item!` macro to create a registered item.", |
| 57 | + ) |
| 58 | + } |
| 59 | + |
| 60 | + fn get_plan_index_mut<E: Entity>(&mut self) -> &mut EntityPlanIndex<E> { |
| 61 | + let index = E::id(); |
| 62 | + let record = self.indexes.get_mut(index).unwrap_or_else(|| { |
| 63 | + panic!( |
| 64 | + "No registered entity found with index = {index:?}. You must use the `define_entity!` macro to create an entity." |
| 65 | + ) |
| 66 | + }); |
| 67 | + let _ = record.get_or_init(|| { |
| 68 | + Box::new(EntityPlanIndex::<E>::default()) as Box<dyn Any> |
| 69 | + }); |
| 70 | + let plan_index = record.get_mut().unwrap(); |
| 71 | + plan_index.as_mut().downcast_mut::<EntityPlanIndex<E>>().expect( |
| 72 | + "TypeID does not match registered item type. You must use the `define_registered_item!` macro to create a registered item.", |
| 73 | + ) |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +type EntityPlanIndex<E> = HashMap<EntityId<E>, Vec<PlanId>>; |
| 78 | + |
| 79 | +pub trait PlanIndexContextExt: ContextBase { |
| 80 | + /// Adds a plan for the given entity, recording in the index the fact that this plan ID is |
| 81 | + /// associated with this entity. |
| 82 | + fn add_plan_for_entity<E: Entity>( |
| 83 | + &mut self, |
| 84 | + entity_id: EntityId<E>, |
| 85 | + time: f64, |
| 86 | + callback: impl FnOnce(&mut Context) + 'static |
| 87 | + ) -> PlanId { |
| 88 | + self.add_plan_for_entity_with_phase(entity_id, time, callback, ExecutionPhase::Normal) |
| 89 | + } |
| 90 | + |
| 91 | + /// Adds a plan for the given entity to be executed at the given execution phase, recording in |
| 92 | + /// the index the fact that this plan ID is associated with this entity. |
| 93 | + fn add_plan_for_entity_with_phase<E: Entity>( |
| 94 | + &mut self, |
| 95 | + entity_id: EntityId<E>, |
| 96 | + time: f64, |
| 97 | + callback: impl FnOnce(&mut Context) + 'static, |
| 98 | + phase: ExecutionPhase, |
| 99 | + ) -> PlanId { |
| 100 | + // Here is the fundamental problem with trying to implement this in client code. |
| 101 | + // We need to know the plan ID of the plan that is currently being handled in order to |
| 102 | + // maintain the plan index, but we don't know the plan ID until _after_ we add the plan. |
| 103 | + // Our solution is to just keep growing the index. No harm comes from trying to cancel |
| 104 | + // a plan that has already been executed. |
| 105 | + let new_plan_id = self.add_plan_with_phase( |
| 106 | + time, |
| 107 | + callback, |
| 108 | + // |context| { |
| 109 | + // // Remove the plan ID from the index when the plan fires. |
| 110 | + // let plan_index_data = self.get_data_mut(PlanIndexPlugin); |
| 111 | + // let plan_index = plan_index_data.get_plan_index_mut::<E>(); |
| 112 | + // plan_index.entry(entity_id).and_modify(|v| v.retain(|id| id != new_plan_id)); |
| 113 | + // callback(context) |
| 114 | + // } |
| 115 | + phase |
| 116 | + ); |
| 117 | + let plan_index_data = self.get_data_mut(PlanIndexPlugin); |
| 118 | + let plan_index = plan_index_data.get_plan_index_mut::<E>(); |
| 119 | + plan_index.entry(entity_id).or_default().push(new_plan_id); |
| 120 | + new_plan_id |
| 121 | + } |
| 122 | + |
| 123 | + /// Fetches a copy of the vector of plan IDs scheduled for this entity. |
| 124 | + fn get_plans_for_entity<E: Entity>(&self, entity_id: EntityId<E>) -> Vec<PlanId> { |
| 125 | + let plan_index_data = self.get_data(PlanIndexPlugin); |
| 126 | + let plan_index = plan_index_data.get_plan_index::<E>(); |
| 127 | + plan_index.get(&entity_id).cloned().unwrap_or_default() |
| 128 | + } |
| 129 | + |
| 130 | + fn cancel_plans_for_entity<E: Entity>(&mut self, entity_id: EntityId<E>) -> bool { |
| 131 | + let plan_index_data = self.get_data_mut(PlanIndexPlugin); |
| 132 | + let plan_index = plan_index_data.get_plan_index_mut::<E>(); |
| 133 | + |
| 134 | + if let Some(plans) = plan_index.remove(&entity_id) { |
| 135 | + // Replace with `Context::cancel_plans_unchecked` when it lands so we avoid |
| 136 | + for plan_id in plans { |
| 137 | + self.cancel_plan(&plan_id); |
| 138 | + } |
| 139 | + true |
| 140 | + } else { |
| 141 | + false |
| 142 | + } |
| 143 | + } |
| 144 | +} |
0 commit comments