Basically if we look at NodeConfig
as it is today:
pub struct NodeConfig<T> {
pub(crate) node: T,
pub(crate) graph_info: GraphInfo,
pub(crate) conditions: Vec<BoxedCondition>,
}
We want to not allow/include GraphInfo
for observer configs, and just want to support run conditions. Maybe we'll support observer ordering in the future but for now it is controversial, and its not hard to change it to support it in the future if we follow the design below.
So, we have a T
there, which means we know what kind of node we're handling. We can bound T
with a new trait that lets us also specify the kind of metadata to store:
pub trait NodeType {
type Metadata; // <-- Used in NodeConfig
type GroupMetadata; // <-- Used in NodeConfigs (plural)
}
pub struct NodeConfig<T: NodeType> {
pub(crate) node: T,
pub(crate) metadata: T::Metadata,
pub(crate) conditions: Vec<BoxedCondition>,
}
And to maintain compatibility, we implement NodeType
for our two pre-existing node types:
impl NodeType for ScheduleSystem {
type Metadata = GraphInfo;
type GroupMetadata = Chain;
}
impl NodeType for InternedSystemSet {
type Metadata = GraphInfo;
type GroupMetadata = Chain;
}
So they still get to have NodeConfig
hold their GraphInfo
stuff. NodeConfigs
(plural) is a simple change as well:
pub enum NodeConfigs<T: NodeType> { // <-- add the bound here
NodeConfig(NodeConfig<T>),
Configs {
configs: Vec<NodeConfigs<T>>,
collective_conditions: Vec<BoxedCondition>,
metadata: T::GroupMetadata, // <-- use the associated type here
},
}
Now anytime we have functions that need to operate on the GraphInfo
, like adding system sets, ordering, etc. we can just use a bound T: NodeType<Metadata = GraphInfo>
.
The IntoSystemConfigs
and IntoSystemSetConfigs
traits are basically the same, except they operate on different node types. Since we have a NodeType
trait now, we can combine them into a single trait with a T: NodeType
bound:
pub trait IntoNodeConfigs<T: NodeType, Marker>: Sized {
fn into_configs(self) -> NodeConfigs<T>;
// For the helper functions like in_set, before, after, etc.
// we just add a bound `T: NodeType<Metadata = GraphInfo>` to
// the function.
fn in_set(self, set: impl SystemSet) -> NodeConfigs<T>
where
T: NodeType<Metadata = NodeInfo>
{
// ...
}
// Except distributive_run_if and run_if, since conditions are always
// around regardless of node type. So they can be left unchanged.
fn distributive_run_if<M>(self, condition: impl Condition<M> + Clone) -> NodeConfigs<T> {
// ...
}
fn run_if<M>(self, condition: impl Condition<M>) -> NodeConfigs<T> {
// ...
}
// For chain and chain_ignore_deferred, we instead bound with
// `T: NodeType<GroupMetadata = Chain>`
fn chain(self) -> NodeConfigs<T>
where
T: NodeType<GroupMetadata = Chain>
{
// ...
}
fn chain_ignore_deferred(self) -> NodeConfigs<T>
where
T: NodeType<GroupMetadata = Chain>
{
// ...
}
}
Now for add_systems
and configure_sets
, they use the IntoNodeConfigs
trait but specify their node type:
impl App {
pub fn add_systems<M>(
&mut self,
schedule: impl ScheduleLabel,
systems: impl IntoNodeConfigs<ScheduleSystem, M>,
) -> &mut Self;
pub fn configure_sets<M>( // <-- configure sets does get a marker generic now, NBD though
&mut self,
schedule: impl ScheduleLabel,
sets: impl IntoNodeConfigs<InternedSystemSet, M>,
) -> &mut Self;
}
Now with all that done, adding observer configuration support is incredibly easy. First add a new node type:
pub type ObserveSystem<E, B> = Box<dyn ObserverSystem<E, B>>;
impl<E: 'static, B: Bundle> Nodetype for ObserveSystem<E, B> {
type Metadata = (); // <------- No metadata for now, maybe we add some later.
type GroupMetadata = (); // <--/
}
And also implement IntoNodeConfigs
for observer systems. We can mostly copy the impl for IntoNodeConfigs<ScheduleSystem>
and change a few things:
impl<E, B, S, Marker> IntoNodeConfigs<ObserveSystem<E, B>, (Infallible, Marker)> for S
where
E: 'static,
B: Bundle,
S: IntoObserverSystem<E, B, Marker, ()>,
{
fn into_configs(self) -> NodeConfigs<ObserveSystem<E, B>> {
// ...
}
}
impl<E, B, S, Marker> IntoNodeConfigs<ObserveSystem<E, B>, (Fallible, Marker)> for S
where
E: 'static,
B: Bundle,
S: IntoObserverSystem<E, B, Marker, Result>,
{
fn into_configs(self) -> NodeConfigs<ObserveSystem<E, B>> {
// ...
}
}
Then, change the function signature of add_observer
to accept configs instead. Maybe we should also pluralize the function name?
impl App {
pub fn add_observers<E: Event, B: Bundle, M>(
&mut self,
observers: impl IntoNodeConfigs<ObserveSystem<E, B>, M>,
) -> &mut Self;
}
Collective conditions can't be easily cloned and pushed alongside every observer system, because conditons aren't required to be Clone
able! So I'll leave someone else to figure that out. It should be possible to just push them into a hash map and reference them in each observer container, probably.