use std::{
any::TypeId,
future::ready,
pin::Pin,
task::{Poll, Waker},
time::{Duration, Instant},
};
use bevy_ecs::message::{MessageCursor, Messages};
use bevy_log::debug;
use bevy_mod_scripting_bindings::InteropError;
use bevy_mod_scripting_script::ScriptAttachment;
use bevy_platform::collections::HashMap;
use super::*;
#[derive(SystemParam)]
pub struct StateMachine<'w, 's, T: Send + Sync + 'static, P: IntoScriptPluginParams> {
events: ResMut<'w, Messages<ForPlugin<T, P>>>,
cursor: Local<'s, MessageCursor<ForPlugin<T, P>>>,
}
impl<'w, 's, T: Send + Sync + 'static, P: IntoScriptPluginParams> StateMachine<'w, 's, T, P> {
pub fn machines_outstanding(&self) -> usize {
self.events.len()
}
pub fn drain(&mut self) -> impl Iterator<Item = T> {
self.events.drain().map(ForPlugin::inner)
}
pub fn intercept(&mut self) -> impl Iterator<Item = &mut T> {
*self.cursor = self.events.get_cursor();
self.cursor
.read_mut(&mut self.events)
.map(|p| p.event_mut())
}
pub fn iter_cloned(&self) -> Vec<T>
where
T: Clone,
{
let mut cursor = self.events.get_cursor();
cursor
.read(&self.events)
.cloned()
.map(ForPlugin::inner)
.collect()
}
pub fn write_batch(&mut self, batch: impl IntoIterator<Item = T>) {
self.events
.write_batch(batch.into_iter().map(ForPlugin::new));
}
}
#[derive(Resource)]
pub struct ActiveMachines<P: IntoScriptPluginParams> {
machines: VecDeque<ScriptMachine<P>>,
on_state_listeners: HashMap<
TypeId,
Vec<
Arc<
dyn Fn(
&mut dyn MachineState<P>,
&mut World,
&mut Context,
) -> Result<(), ScriptError>
+ Send
+ Sync,
>,
>,
>,
pub(crate) budget: Option<Duration>,
}
impl<P: IntoScriptPluginParams> Default for ActiveMachines<P> {
fn default() -> Self {
Self {
machines: Default::default(),
on_state_listeners: Default::default(),
budget: Default::default(),
}
}
}
pub trait TransitionListener<State>: 'static + Send + Sync {
fn on_enter(
&self,
state: &mut State,
world: &mut World,
context: &mut Context,
) -> Result<(), ScriptError>;
fn erased<P: IntoScriptPluginParams>(
self,
) -> Box<
dyn Fn(&mut dyn MachineState<P>, &mut World, &mut Context) -> Result<(), ScriptError>
+ Send
+ Sync,
>
where
Self: Sized,
State: 'static,
{
Box::new(move |state, world, context| {
let typed = (state as &mut dyn Any).downcast_mut::<State>();
typed
.ok_or(ScriptError::new_boxed_without_type_info(
format!(
"could not downcast script machine state to: '{}'. Could not execute transition listener",
std::any::type_name::<State>()
)
.into(),
))
.and_then(|typed| self.on_enter(typed, world, context))
})
}
}
impl<P: IntoScriptPluginParams> ActiveMachines<P> {
pub fn current_machine(&self) -> Option<&ScriptMachine<P>> {
self.machines.front()
}
pub fn push_listener<S: 'static>(&mut self, listener: impl TransitionListener<S> + 'static) {
let erased = listener.erased::<P>();
self.on_state_listeners
.entry(std::any::TypeId::of::<S>())
.or_default()
.push(erased.into());
}
pub fn tick_machines(&mut self, world: &mut World) {
let start = Instant::now();
let end = start + self.budget.unwrap_or(Duration::from_secs(99999));
while !self.machines.is_empty() && Instant::now() < end {
if let Some(mut next) = self.machines.pop_front() {
let final_state = next.tick(world, &self.on_state_listeners);
match final_state {
Some(Ok(_)) => {
}
Some(Err(err)) => {
_ = world
.write_message(ScriptErrorEvent::new(err.with_language(P::LANGUAGE)));
}
None => {
self.machines.push_front(next);
}
}
}
}
}
pub fn queue_machine(&mut self, context: Context, state: impl MachineState<P>) {
self.machines.push_back(ScriptMachine {
context,
internal_state: MachineExecutionState::Initialized(Box::new(state)),
});
}
pub fn active_machines(&self) -> usize {
self.machines.len()
}
}
pub struct ScriptMachine<P> {
pub context: Context,
internal_state: MachineExecutionState<P>,
}
enum MachineExecutionState<P> {
Initialized(Box<dyn MachineState<P>>),
Running(
Pin<Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync>>,
),
Finished,
}
impl<P: IntoScriptPluginParams> ScriptMachine<P> {
pub fn tick(
&mut self,
world: &mut World,
listeners: &HashMap<
TypeId,
Vec<
Arc<
dyn Fn(
&mut dyn MachineState<P>,
&mut World,
&mut Context,
) -> Result<(), ScriptError>
+ Send
+ Sync,
>,
>,
>,
) -> Option<Result<Box<dyn MachineState<P>>, ScriptError>> {
match &mut self.internal_state {
MachineExecutionState::Initialized(machine_state) => {
debug!(
"State '{}' entered. For script: {}",
machine_state.state_name(),
self.context.attachment,
);
if let Some(listeners) = listeners.get(&machine_state.as_ref().type_id()) {
for on_entered in listeners {
if let Err(err) =
(on_entered)(machine_state.as_mut(), world, &mut self.context)
{
_ = world.write_message(ScriptErrorEvent::new(
err.with_context(self.context.attachment.to_string())
.with_context(machine_state.state_name())
.with_language(P::LANGUAGE),
))
}
}
}
let next = machine_state.poll_next(&self.context, world);
self.internal_state = MachineExecutionState::Running(next.into());
return self.tick(world, listeners);
}
MachineExecutionState::Running(future) => {
let waker = Waker::noop();
let mut cx = std::task::Context::from_waker(waker);
if let Poll::Ready(res) = Future::poll(future.as_mut(), &mut cx) {
match res {
Ok(next) => {
if next.is_final() {
debug!(
"Reached final state '{}'. For script {}",
next.state_name(),
&self.context.attachment
);
self.internal_state = MachineExecutionState::Finished;
return Some(Ok(next));
} else {
self.internal_state = MachineExecutionState::Initialized(next)
}
}
res => {
debug!(
"Error in progressing to next state. For script {}",
&self.context.attachment
);
self.internal_state = MachineExecutionState::Finished;
return Some(res);
}
}
}
}
MachineExecutionState::Finished => {
return Some(Err(ScriptError::new_boxed_without_type_info(
String::from("cannot tick machine twice").into(),
)
.with_context(self.context.attachment.to_string())
.with_language(P::LANGUAGE)));
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct Context {
pub attachment: ScriptAttachment,
pub blackboard: SmallVec<[(&'static str, Arc<dyn Any + Send + Sync + 'static>); 1]>,
}
impl Context {
pub fn insert(&mut self, key: &'static str, val: impl Any + Send + Sync + 'static) {
self.blackboard.push((key, Arc::new(val)));
}
pub fn get_first_typed<T: Any + Clone>(&self, key: &'static str) -> Option<T> {
self.blackboard
.iter()
.find_map(|(k, v)| (*k == key).then_some(v.downcast_ref().cloned()))
.flatten()
}
}
pub trait MachineState<P>: Send + Sync + 'static + Any {
fn state_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
fn poll_next(
&mut self,
ctxt: &Context,
world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync>;
fn is_final(&self) -> bool {
false
}
}
#[derive(Clone, Copy)]
pub struct LoadingCompleted;
#[derive(Clone, Copy)]
pub struct UnloadingCompleted;
#[derive(Clone)]
pub struct LoadingInitialized {
pub source: Handle<ScriptAsset>,
pub content: Box<[u8]>,
}
pub struct ReloadingInitialized<P: IntoScriptPluginParams> {
pub source: Handle<ScriptAsset>,
pub content: Box<[u8]>,
pub existing_context: Arc<Mutex<P::C>>,
}
#[derive(Clone)]
pub struct UnloadingInitialized<P: IntoScriptPluginParams> {
pub existing_context: Arc<Mutex<P::C>>,
}
impl<P: IntoScriptPluginParams> Clone for ReloadingInitialized<P> {
fn clone(&self) -> Self {
Self {
source: self.source.clone(),
content: self.content.clone(),
existing_context: self.existing_context.clone(),
}
}
}
impl<P: IntoScriptPluginParams> std::fmt::Debug for ReloadingInitialized<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReloadingInitialized")
.field("source", &self.source)
.finish()
}
}
pub struct ContextAssigned<P: IntoScriptPluginParams> {
pub context: Arc<Mutex<P::C>>,
pub is_new_context: bool,
}
impl<P: IntoScriptPluginParams> Clone for ContextAssigned<P> {
fn clone(&self) -> Self {
Self {
context: self.context.clone(),
is_new_context: self.is_new_context,
}
}
}
pub struct ResidentRemoved<P: IntoScriptPluginParams> {
pub removed_from_context: Arc<Mutex<P::C>>,
}
impl<P: IntoScriptPluginParams> Clone for ResidentRemoved<P> {
fn clone(&self) -> Self {
Self {
removed_from_context: self.removed_from_context.clone(),
}
}
}
pub struct ContextRemoved<P: IntoScriptPluginParams> {
pub removed_context: Arc<Mutex<P::C>>,
}
impl<P: IntoScriptPluginParams> Clone for ContextRemoved<P> {
fn clone(&self) -> Self {
Self {
removed_context: self.removed_context.clone(),
}
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for LoadingInitialized {
fn poll_next(
&mut self,
ctxt: &Context,
world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
let attachment = &ctxt.attachment;
let guard = WorldGuard::new_exclusive(world);
let ctxt = P::load(attachment, &self.content, guard.clone());
Box::new(ready(ctxt.map_err(ScriptError::from).map(|context| {
Box::new(ContextAssigned::<P> {
context: Arc::new(Mutex::new(context)),
is_new_context: true,
}) as Box<dyn MachineState<P>>
})))
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for ReloadingInitialized<P> {
fn poll_next(
&mut self,
ctxt: &Context,
world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
let attachment = &ctxt.attachment;
let guard = WorldGuard::new_exclusive(world);
let mut previous_context_guard = self.existing_context.lock();
let ctxt = P::reload(
attachment,
&self.content,
&mut previous_context_guard,
guard.clone(),
);
Box::new(ready(ctxt.map_err(ScriptError::from).map(|_| {
Box::new(ContextAssigned::<P> {
context: self.existing_context.clone(),
is_new_context: false,
}) as Box<dyn MachineState<P>>
})))
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for UnloadingInitialized<P> {
fn poll_next(
&mut self,
ctxt: &Context,
world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
let attachment = &ctxt.attachment;
let contexts = world.get_resource_or_init::<ScriptContext<P>>();
let mut contexts_guard = contexts.write();
let residents_len = contexts_guard.residents_len(attachment);
if residents_len == 1 {
contexts_guard.remove(attachment);
Box::new(ready(Ok(Box::new(ContextRemoved {
removed_context: self.existing_context.clone(),
}) as Box<dyn MachineState<P>>)))
} else {
contexts_guard.remove_resident(attachment);
Box::new(ready(Ok(Box::new(ResidentRemoved {
removed_from_context: self.existing_context.clone(),
}) as Box<dyn MachineState<P>>)))
}
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for ContextAssigned<P> {
fn poll_next(
&mut self,
ctxt: &Context,
world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
let attachment = &ctxt.attachment;
let contexts = world.get_resource_or_init::<ScriptContext<P>>();
let mut contexts_guard = contexts.write();
match contexts_guard.insert(attachment.clone(), self.context.clone()) {
Ok(_) => {}
Err(_) => {
drop(contexts_guard);
_ = world.write_message(ScriptErrorEvent::new(
ScriptError::from(InteropError::str("no context policy matched"))
.with_language(P::LANGUAGE),
))
}
}
Box::new(ready(Ok(
Box::new(LoadingCompleted) as Box<dyn MachineState<P>>
)))
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for LoadingCompleted {
fn poll_next(
&mut self,
_ctxt: &Context,
_world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
Box::new(ready(Ok(Box::new(Self) as Box<dyn MachineState<P>>)))
}
fn is_final(&self) -> bool {
true
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for ContextRemoved<P> {
fn poll_next(
&mut self,
_ctxt: &Context,
_world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
Box::new(ready(
Ok(Box::new(self.clone()) as Box<dyn MachineState<P>>),
))
}
fn is_final(&self) -> bool {
true
}
}
impl<P: IntoScriptPluginParams> MachineState<P> for ResidentRemoved<P> {
fn poll_next(
&mut self,
_ctxt: &Context,
_world: &mut World,
) -> Box<dyn Future<Output = Result<Box<dyn MachineState<P>>, ScriptError>> + Send + Sync> {
Box::new(ready(
Ok(Box::new(self.clone()) as Box<dyn MachineState<P>>),
))
}
fn is_final(&self) -> bool {
true
}
}