Skip to content

Commit 79182d3

Browse files
committed
feat: "plan index" that tracks an entity's plans so that the plans can be canceled in bulk, e.g., when a person dies
1 parent fd7153f commit 79182d3

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod population_loader;
88
pub mod rate_fns;
99
pub mod reports;
1010
pub mod settings;
11+
pub mod plan_index;
1112

1213
pub use model::initialize_model;
1314
pub use parameters::ContextParametersExt;

src/plan_index.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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

Comments
 (0)