use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use bevy_app::App;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::Event;
#[cfg(feature = "effect-trace")]
use bevy_ecs::query::Access;
use bevy_ecs::resource::Resource;
#[cfg(feature = "effect-trace")]
use bevy_ecs::system::BoxedSystem;
use bevy_ecs::system::{Commands, In, IntoSystem, SystemId};
use bevy_ecs::world::World;
use bevy_log::warn;
use brink_format::Value;
use brink_runtime::{ExternalFnHandler, ExternalResult};
use thiserror::Error;
pub type BrinkQueryInput = (Entity, Vec<Value>);
pub(crate) type QuerySystemId = SystemId<In<BrinkQueryInput>, Value>;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum BrinkArgError {
#[error("expected {expected} argument(s), got {got}")]
Count {
expected: usize,
got: usize,
},
#[error("argument {index}: expected {expected}")]
Type {
index: usize,
expected: &'static str,
},
}
pub trait BrinkCommand: Sized {
fn from_ink_args(args: &[Value]) -> Result<Self, BrinkArgError>;
fn reply(&self) -> Value {
Value::Null
}
}
type PureFn = Box<dyn Fn(&[Value]) -> Value + Send + Sync>;
type CommandFn = Box<dyn Fn(&[Value]) -> Result<QueuedCommand, BrinkArgError> + Send + Sync>;
pub(crate) type TaskFn =
Box<dyn Fn(Vec<Value>) -> Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
pub(crate) enum AsyncKind {
Event,
Task(TaskFn),
}
pub(crate) type TriggerFn = Box<dyn FnOnce(&mut World) + Send>;
struct QueuedCommand {
trigger: TriggerFn,
reply: Value,
}
#[derive(Resource)]
pub struct BrinkBindings<M: Send + Sync + 'static = ()> {
pure: HashMap<String, PureFn>,
commands: HashMap<String, CommandFn>,
queries: HashMap<String, QuerySystemId>,
#[cfg(feature = "effect-trace")]
query_access: HashMap<String, Access>,
pub(crate) async_bindings: HashMap<String, AsyncKind>,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> Default for BrinkBindings<M> {
fn default() -> Self {
Self {
pure: HashMap::new(),
commands: HashMap::new(),
queries: HashMap::new(),
#[cfg(feature = "effect-trace")]
query_access: HashMap::new(),
async_bindings: HashMap::new(),
_marker: PhantomData,
}
}
}
impl<M: Send + Sync + 'static> BrinkBindings<M> {
#[must_use]
pub fn handler(&self) -> BrinkHandler<'_, M> {
BrinkHandler {
bindings: self,
queued: RefCell::new(Vec::new()),
}
}
pub(crate) fn query(&self, name: &str) -> Option<QuerySystemId> {
self.queries.get(name).copied()
}
#[cfg(feature = "effect-trace")]
pub(crate) fn query_access(&self, name: &str) -> Option<&Access> {
self.query_access.get(name)
}
#[must_use]
pub(crate) fn is_command(&self, name: &str) -> bool {
self.commands.contains_key(name)
}
}
pub struct BrinkHandler<'a, M: Send + Sync + 'static = ()> {
bindings: &'a BrinkBindings<M>,
pub(crate) queued: RefCell<Vec<TriggerFn>>,
}
impl<M: Send + Sync + 'static> BrinkHandler<'_, M> {
pub fn flush(self, commands: &mut Commands) {
for trigger in self.queued.into_inner() {
commands.queue(trigger);
}
}
pub(crate) fn take_queued(&self) -> Vec<TriggerFn> {
std::mem::take(&mut self.queued.borrow_mut())
}
#[must_use]
pub fn queued_len(&self) -> usize {
self.queued.borrow().len()
}
}
fn resolve_binding<M: Send + Sync + 'static>(
bindings: &BrinkBindings<M>,
queued: &RefCell<Vec<TriggerFn>>,
name: &str,
args: &[Value],
) -> ExternalResult {
if let Some(f) = bindings.pure.get(name) {
return ExternalResult::Resolved(f(args));
}
if let Some(parse) = bindings.commands.get(name) {
return match parse(args) {
Ok(queued_cmd) => {
queued.borrow_mut().push(queued_cmd.trigger);
ExternalResult::Resolved(queued_cmd.reply)
}
Err(err) => {
warn!("brink command '{name}': {err}; emitting nothing, returning null");
ExternalResult::Resolved(Value::Null)
}
};
}
if bindings.queries.contains_key(name) || bindings.async_bindings.contains_key(name) {
return ExternalResult::Pending;
}
ExternalResult::Fallback
}
impl<M: Send + Sync + 'static> ExternalFnHandler for BrinkHandler<'_, M> {
fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
resolve_binding(self.bindings, &self.queued, name, args)
}
}
impl<M: Send + Sync + 'static> BrinkBindings<M> {
pub(crate) fn eval_handler(&self) -> EvalHandler<'_, M> {
EvalHandler {
bindings: self,
queued: RefCell::new(Vec::new()),
}
}
}
pub(crate) struct EvalHandler<'a, M: Send + Sync + 'static> {
bindings: &'a BrinkBindings<M>,
queued: RefCell<Vec<TriggerFn>>,
}
impl<M: Send + Sync + 'static> EvalHandler<'_, M> {
pub(crate) fn take_queued(&self) -> Vec<TriggerFn> {
std::mem::take(&mut self.queued.borrow_mut())
}
}
impl<M: Send + Sync + 'static> ExternalFnHandler for EvalHandler<'_, M> {
fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
resolve_binding(self.bindings, &self.queued, name, args)
}
}
pub trait BrinkBindingsAppExt {
fn bind_brink_fn<M, F, R>(&mut self, name: impl Into<String>, f: F) -> &mut Self
where
M: Send + Sync + 'static,
F: Fn(&[Value]) -> R + Send + Sync + 'static,
R: Into<Value>;
fn bind_brink_command<M, E>(&mut self, name: impl Into<String>) -> &mut Self
where
M: Send + Sync + 'static,
E: Event + BrinkCommand,
for<'a> <E as Event>::Trigger<'a>: Default;
fn bind_brink_query<M, S, SM>(&mut self, name: impl Into<String>, system: S) -> &mut Self
where
M: Send + Sync + 'static,
S: IntoSystem<In<BrinkQueryInput>, Value, SM> + 'static;
fn bind_brink_async<M>(&mut self, name: impl Into<String>) -> &mut Self
where
M: Send + Sync + 'static;
fn bind_brink_task<M, F, Fut>(&mut self, name: impl Into<String>, f: F) -> &mut Self
where
M: Send + Sync + 'static,
F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Value> + Send + 'static;
}
impl BrinkBindingsAppExt for App {
fn bind_brink_fn<M, F, R>(&mut self, name: impl Into<String>, f: F) -> &mut Self
where
M: Send + Sync + 'static,
F: Fn(&[Value]) -> R + Send + Sync + 'static,
R: Into<Value>,
{
let name = name.into();
{
let mut reg = self
.world_mut()
.get_resource_or_insert_with(BrinkBindings::<M>::default);
reg.pure.insert(name, Box::new(move |args| f(args).into()));
}
self
}
fn bind_brink_command<M, E>(&mut self, name: impl Into<String>) -> &mut Self
where
M: Send + Sync + 'static,
E: Event + BrinkCommand,
for<'a> <E as Event>::Trigger<'a>: Default,
{
let name = name.into();
{
let mut reg = self
.world_mut()
.get_resource_or_insert_with(BrinkBindings::<M>::default);
reg.commands.insert(
name,
Box::new(move |args: &[Value]| {
let event = E::from_ink_args(args)?;
let reply = event.reply();
Ok(QueuedCommand {
trigger: Box::new(move |world: &mut World| {
world.trigger(event);
}),
reply,
})
}),
);
}
self
}
fn bind_brink_query<M, S, SM>(&mut self, name: impl Into<String>, system: S) -> &mut Self
where
M: Send + Sync + 'static,
S: IntoSystem<In<BrinkQueryInput>, Value, SM> + 'static,
{
let name = name.into();
#[cfg(feature = "effect-trace")]
let (id, access) = {
let mut boxed: BoxedSystem<In<BrinkQueryInput>, Value> =
Box::new(IntoSystem::into_system(system));
let access = boxed.initialize(self.world_mut()).combined_access().clone();
let id = self.world_mut().register_boxed_system(boxed);
(id, access)
};
#[cfg(not(feature = "effect-trace"))]
let id = self.world_mut().register_system(system);
{
let mut reg = self
.world_mut()
.get_resource_or_insert_with(BrinkBindings::<M>::default);
#[cfg(feature = "effect-trace")]
reg.query_access.insert(name.clone(), access);
reg.queries.insert(name, id);
}
self
}
fn bind_brink_async<M>(&mut self, name: impl Into<String>) -> &mut Self
where
M: Send + Sync + 'static,
{
let name = name.into();
{
let mut reg = self
.world_mut()
.get_resource_or_insert_with(BrinkBindings::<M>::default);
reg.async_bindings.insert(name, AsyncKind::Event);
}
self
}
fn bind_brink_task<M, F, Fut>(&mut self, name: impl Into<String>, f: F) -> &mut Self
where
M: Send + Sync + 'static,
F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Value> + Send + 'static,
{
let name = name.into();
let factory: TaskFn = Box::new(move |args| Box::pin(f(args)));
{
let mut reg = self
.world_mut()
.get_resource_or_insert_with(BrinkBindings::<M>::default);
reg.async_bindings.insert(name, AsyncKind::Task(factory));
}
self
}
}