pub mod input_reader;
mod instance;
pub mod time;
mod trigger_tracker;
use core::{
any::TypeId,
cmp::{Ordering, Reverse},
marker::PhantomData,
};
#[cfg(test)]
use bevy::ecs::system::SystemState;
use bevy::{
ecs::{
component::ComponentId,
entity_disabling::Disabled,
schedule::ScheduleLabel,
system::{ParamBuilder, QueryParamBuilder},
world::{FilteredEntityMut, FilteredEntityRef},
},
prelude::*,
};
use log::{debug, trace};
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
use crate::{
action::fns::ActionFns,
binding::FirstActivation,
condition::fns::{ConditionFns, ConditionRegistry},
context::{input_reader::PendingBindings, trigger_tracker::TriggerTracker},
modifier::fns::{ModifierFns, ModifierRegistry},
prelude::*,
};
use input_reader::InputReader;
use instance::ContextInstances;
pub trait InputContextAppExt {
fn add_input_context<C: Component>(&mut self) -> &mut Self {
self.add_input_context_to::<PreUpdate, C>()
}
fn add_input_context_to<S: ScheduleLabel + Default, C: Component>(&mut self) -> &mut Self;
}
impl InputContextAppExt for App {
fn add_input_context_to<S: ScheduleLabel + Default, C: Component>(&mut self) -> &mut Self {
debug!(
"registering `{}` for `{}`",
ShortName::of::<C>(),
ShortName::of::<S>(),
);
let actions_id = self.world_mut().register_component::<Actions<C>>();
let activity_id = self.world_mut().register_component::<ContextActivity<C>>();
let mut registry = self.world_mut().resource_mut::<ContextRegistry>();
if let Some(contexts) = registry
.iter_mut()
.find(|c| c.schedule_id == TypeId::of::<S>())
{
debug_assert!(
!contexts.actions_ids.contains(&actions_id),
"context `{}` shouldn't be added more than once",
ShortName::of::<C>()
);
contexts.actions_ids.push(actions_id);
contexts.activity_ids.push(activity_id);
} else {
let mut contexts = ScheduleContexts::new::<S>();
contexts.actions_ids.push(actions_id);
contexts.activity_ids.push(activity_id);
registry.push(contexts);
}
let _ = self.try_register_required_components::<C, ContextPriority<C>>();
let _ = self.try_register_required_components::<C, ContextActivity<C>>();
self.add_observer(register::<C, S>)
.add_observer(unregister::<C, S>)
.add_observer(deactivate::<C>)
.add_observer(reset_action::<C>);
self
}
}
#[derive(Resource, Default, Deref, DerefMut)]
pub(crate) struct ContextRegistry(Vec<ScheduleContexts>);
pub(crate) struct ScheduleContexts {
schedule_id: TypeId,
actions_ids: Vec<ComponentId>,
activity_ids: Vec<ComponentId>,
setup: fn(&Self, &mut App, &ConditionRegistry, &ModifierRegistry),
}
impl ScheduleContexts {
#[must_use]
fn new<S: ScheduleLabel + Default>() -> Self {
Self {
schedule_id: TypeId::of::<S>(),
actions_ids: Default::default(),
activity_ids: Default::default(),
setup: Self::setup_typed::<S>,
}
}
pub(crate) fn setup(
&self,
app: &mut App,
conditions: &ConditionRegistry,
modifiers: &ModifierRegistry,
) {
(self.setup)(self, app, conditions, modifiers);
}
pub(crate) fn setup_typed<S: ScheduleLabel + Default>(
&self,
app: &mut App,
conditions: &ConditionRegistry,
modifiers: &ModifierRegistry,
) {
debug!("setting up systems for `{}`", ShortName::of::<S>());
let update_fn = (
ParamBuilder,
ParamBuilder,
ParamBuilder,
ParamBuilder,
QueryParamBuilder::new(|builder| {
builder
.data::<Option<&GamepadDevice>>()
.optional(|builder| {
for &id in &self.activity_ids {
builder.mut_id(id);
}
for &id in &self.actions_ids {
builder.mut_id(id);
}
});
}),
ParamBuilder,
ParamBuilder,
ParamBuilder,
QueryParamBuilder::new(|builder| {
builder.optional(|builder| {
for &id in &**conditions {
builder.mut_id(id);
}
for &id in &**modifiers {
builder.mut_id(id);
}
});
}),
)
.build_state(app.world_mut())
.build_system(update::<S>);
let trigger_fn = (
ParamBuilder,
ParamBuilder,
QueryParamBuilder::new(|builder| {
builder.optional(|builder| {
for &id in &self.activity_ids {
builder.mut_id(id);
}
for &id in &self.actions_ids {
builder.ref_id(id);
}
});
}),
ParamBuilder,
)
.build_state(app.world_mut())
.build_system(apply::<S>);
app.init_resource::<ContextInstances<S>>()
.configure_sets(
S::default(),
(EnhancedInputSystems::Update, EnhancedInputSystems::Apply).chain(),
)
.add_systems(
S::default(),
(
update_fn.in_set(EnhancedInputSystems::Update),
trigger_fn.in_set(EnhancedInputSystems::Apply),
),
);
}
}
fn register<C: Component, S: ScheduleLabel>(
insert: On<Insert, ContextPriority<C>>,
mut instances: ResMut<ContextInstances<S>>,
contexts: Query<&ContextPriority<C>, Allow<Disabled>>,
) {
let priority = **contexts.get(insert.entity).unwrap();
debug!(
"registering `{}` to `{}` with priority {priority}",
ShortName::of::<C>(),
insert.entity
);
instances.add::<C>(insert.entity, priority);
}
fn unregister<C: Component, S: ScheduleLabel>(
replace: On<Replace, ContextPriority<C>>,
mut instances: ResMut<ContextInstances<S>>,
) {
debug!(
"unregistering `{}` from `{}`",
ShortName::of::<C>(),
replace.entity,
);
instances.remove::<C>(replace.entity);
}
fn deactivate<C: Component>(
insert: On<Insert, ContextActivity<C>>,
mut pending: ResMut<PendingBindings>,
contexts: Query<(&ContextActivity<C>, &Actions<C>)>,
actions: Query<(&ActionSettings, &Bindings)>,
bindings: Query<&Binding>,
) {
let Ok((&active, context_actions)) = contexts.get(insert.entity) else {
return;
};
debug!(
"setting activity of `{}` to `{}`",
ShortName::of::<C>(),
*active,
);
if !*active {
for (settings, action_bindings) in actions.iter_many(context_actions) {
if settings.require_reset {
pending.extend(bindings.iter_many(action_bindings).copied());
}
}
}
}
pub(crate) fn reset_action<C: Component>(
remove: On<Remove, ActionOf<C>>,
mut commands: Commands,
mut pending: ResMut<PendingBindings>,
mut actions: Query<(
&ActionOf<C>,
&ActionSettings,
&ActionFns,
Option<&Bindings>,
&mut ActionValue,
&mut ActionState,
&mut ActionEvents,
&mut ActionTime,
)>,
bindings: Query<&Binding>,
) {
let Ok((action_of, settings, fns, action_bindings, mut value, mut state, mut events, mut time)) =
actions.get_mut(remove.entity)
else {
trace!("ignoring reset for `{}`", remove.entity);
return;
};
*time = Default::default();
events.set_if_neq(ActionEvents::new(*state, ActionState::None));
state.set_if_neq(Default::default());
value.set_if_neq(ActionValue::zero(value.dim()));
fns.trigger(
&mut commands,
**action_of,
remove.entity,
*state,
*events,
*value,
*time,
);
if let Some(action_bindings) = action_bindings
&& settings.require_reset
{
pending.extend(bindings.iter_many(action_bindings).copied());
}
}
#[derive(Component)]
pub struct ExternallyMocked;
#[allow(clippy::too_many_arguments)]
fn update<S: ScheduleLabel>(
mut consume_buffer: Local<Vec<Binding>>, time: ContextTime,
mut reader: InputReader,
instances: Res<ContextInstances<S>>,
mut contexts: Query<FilteredEntityMut>,
mut actions: Query<
(
Entity,
&Name,
&ActionSettings,
Option<&Bindings>,
Option<&ModifierFns>,
Option<&ConditionFns>,
&mut ActionMock,
),
Without<ExternallyMocked>,
>,
mut actions_data: Query<(
&'static mut ActionValue,
&'static mut ActionState,
&'static mut ActionEvents,
&'static mut ActionTime,
)>,
mut bindings: Query<
(
Entity,
&Binding,
&mut FirstActivation,
Option<&ModifierFns>,
Option<&ConditionFns>,
),
Without<ActionSettings>,
>,
mut conds_and_mods: Query<FilteredEntityMut>,
) {
reader.clear_consumed::<S>();
for instance in &**instances {
let Ok(mut context) = contexts.get_mut(instance.entity) else {
trace!(
"skipping updating `{}` on disabled `{}`",
instance.name, instance.entity
);
continue;
};
let gamepad = context.get::<GamepadDevice>().copied().unwrap_or_default();
let context_active = instance.is_active(&context.as_readonly());
let Some(mut context_actions) = instance.actions_mut(&mut context) else {
continue;
};
let mods_count = |action: &Entity| {
let Ok((.., action_bindings, _, _, _)) = actions.get(*action) else {
return Reverse(0);
};
let value = bindings
.iter_many(action_bindings.into_iter().flatten())
.map(|(_, b, ..)| b.mod_keys_count())
.max()
.unwrap_or(0);
Reverse(value)
};
if !context_actions.is_sorted_by_key(mods_count) {
context_actions.sort_by_cached_key(mods_count);
}
trace!("updating `{}` on `{}`", instance.name, instance.entity);
reader.set_gamepad(gamepad);
let mut actions_iter = actions.iter_many_mut(&*context_actions);
while let Some((
action,
action_name,
action_settings,
action_bindings,
modifiers,
conditions,
mut mock,
)) = actions_iter.fetch_next()
{
let action_name = ShortName(action_name);
let (new_state, new_value) = if !context_active {
trace!("skipping updating `{action_name}` due to inactive context");
let dim = actions_data.get(action).map(|(v, ..)| v.dim()).unwrap();
(ActionState::None, ActionValue::zero(dim))
} else if mock.enabled {
trace!("updating `{action_name}` from `{mock:?}`");
let expired = match &mut mock.span {
MockSpan::Updates(ticks) => {
*ticks = ticks.saturating_sub(1);
*ticks == 0
}
MockSpan::Duration(duration) => {
*duration = duration.saturating_sub(time.delta());
trace!("reducing mock duration by {:?}", time.delta());
duration.is_zero()
}
MockSpan::Manual => false,
};
let new_state = mock.state;
let new_value = mock.value;
if expired {
mock.enabled = false;
}
(new_state, new_value)
} else {
trace!("updating `{action_name}` from input");
let dim = actions_data.get(action).map(|(v, ..)| v.dim()).unwrap();
let actions_data = actions_data.as_readonly();
let mut tracker = TriggerTracker::new(ActionValue::zero(dim));
let mut bindings_iter =
bindings.iter_many_mut(action_bindings.into_iter().flatten());
while let Some((
binding_entity,
&binding,
mut first_activation,
modifiers,
conditions,
)) = bindings_iter.fetch_next()
{
let new_value = reader.value(binding);
if action_settings.require_reset && **first_activation {
if new_value.as_bool() {
reader.consume::<S>(binding);
continue;
} else {
**first_activation = false;
}
}
let mut binding_entity = conds_and_mods.get_mut(binding_entity).unwrap();
let mut current_tracker = TriggerTracker::new(new_value);
trace!("reading value `{new_value:?}`");
if let Some(modifiers) = modifiers {
current_tracker.apply_modifiers(
&mut binding_entity,
&actions_data,
&time,
modifiers,
);
}
if let Some(conditions) = conditions {
current_tracker.apply_conditions(
&mut binding_entity,
&actions_data,
&time,
conditions,
);
}
let current_state = current_tracker.state();
if current_state == ActionState::None {
continue;
}
match current_state.cmp(&tracker.state()) {
Ordering::Less => (),
Ordering::Equal => {
tracker.combine(current_tracker, action_settings.accumulation);
if action_settings.consume_input {
consume_buffer.push(binding);
}
}
Ordering::Greater => {
tracker.overwrite(current_tracker);
if action_settings.consume_input {
consume_buffer.clear();
consume_buffer.push(binding);
}
}
}
}
let mut action = conds_and_mods.get_mut(action).unwrap();
if let Some(modifiers) = modifiers {
tracker.apply_modifiers(&mut action, &actions_data, &time, modifiers);
}
if let Some(conditions) = conditions {
tracker.apply_conditions(&mut action, &actions_data, &time, conditions);
}
let new_state = tracker.state();
let new_value = tracker.value().convert(dim);
if action_settings.consume_input {
if new_state != ActionState::None {
for &binding in &consume_buffer {
reader.consume::<S>(binding);
}
}
consume_buffer.clear();
}
(new_state, new_value)
};
trace!("evaluated to `{new_state:?}` with `{new_value:?}`");
let (mut value, mut state, mut events, mut action_time) =
actions_data.get_mut(action).unwrap();
action_time.update(time.delta_secs(), *state);
events.set_if_neq(ActionEvents::new(*state, new_state));
state.set_if_neq(new_state);
value.set_if_neq(new_value);
}
}
}
pub type ActionsQuery<'w, 's> = Query<
'w,
's,
(
&'static ActionValue,
&'static ActionState,
&'static ActionEvents,
&'static ActionTime,
),
>;
fn apply<S: ScheduleLabel>(
mut commands: Commands,
instances: Res<ContextInstances<S>>,
contexts: Query<FilteredEntityRef, Without<ActionFns>>,
mut actions: Query<EntityMut, With<ActionFns>>,
) {
for instance in &**instances {
let Ok(context) = contexts.get(instance.entity) else {
trace!(
"skipping triggering for `{}` on disabled `{}`",
instance.name, instance.entity,
);
continue;
};
let Some(context_actions) = instance.actions(&context) else {
continue;
};
trace!(
"running triggers for `{}` on `{}`",
instance.name, instance.entity,
);
let mut actions_iter = actions.iter_many_mut(context_actions);
while let Some(mut action) = actions_iter.fetch_next() {
let fns = *action.get::<ActionFns>().unwrap();
let value = *action.get::<ActionValue>().unwrap();
fns.store_value(&mut action, value);
let state = *action.get::<ActionState>().unwrap();
let events = *action.get::<ActionEvents>().unwrap();
let time = *action.get::<ActionTime>().unwrap();
fns.trigger(
&mut commands,
context.id(),
action.id(),
state,
events,
value,
time,
);
}
}
}
#[derive(Component, Reflect, Deref)]
#[component(immutable)]
pub struct ContextActivity<C> {
#[deref]
active: bool,
#[reflect(ignore)]
marker: PhantomData<C>,
}
impl<C> ContextActivity<C> {
pub const ACTIVE: Self = Self::new(true);
pub const INACTIVE: Self = Self::new(false);
#[must_use]
pub const fn new(active: bool) -> Self {
Self {
active,
marker: PhantomData,
}
}
#[must_use]
pub const fn toggled(self) -> Self {
if self.active {
Self::INACTIVE
} else {
Self::ACTIVE
}
}
}
impl<C> Default for ContextActivity<C> {
fn default() -> Self {
Self::ACTIVE
}
}
impl<C> Clone for ContextActivity<C> {
fn clone(&self) -> Self {
*self
}
}
impl<C> Copy for ContextActivity<C> {}
#[derive(Component, Reflect, Deref)]
#[component(immutable)]
pub struct ContextPriority<C> {
#[deref]
value: usize,
#[reflect(ignore)]
marker: PhantomData<C>,
}
impl<C> ContextPriority<C> {
pub const fn new(value: usize) -> Self {
Self {
value,
marker: PhantomData,
}
}
}
impl<C> Default for ContextPriority<C> {
fn default() -> Self {
Self::new(0)
}
}
impl<C> Clone for ContextPriority<C> {
fn clone(&self) -> Self {
*self
}
}
impl<C> Copy for ContextPriority<C> {}
#[derive(Component, Reflect, Debug, Default, Hash, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
pub enum GamepadDevice {
#[default]
Any,
Single(Entity),
None,
}
impl From<Entity> for GamepadDevice {
fn from(value: Entity) -> Self {
Self::Single(value)
}
}
impl From<Option<Entity>> for GamepadDevice {
fn from(value: Option<Entity>) -> Self {
match value {
Some(entity) => GamepadDevice::Single(entity),
None => GamepadDevice::None,
}
}
}
#[cfg(test)]
pub(crate) fn init_world<'w, 's>() -> (World, SystemState<(ContextTime<'w>, ActionsQuery<'w, 's>)>)
{
let mut world = World::new();
world.init_resource::<Time>();
world.init_resource::<Time<Real>>();
let state = SystemState::<(ContextTime, ActionsQuery)>::new(&mut world);
(world, state)
}