use std::marker::PhantomData;
use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EntityEvent;
use bevy_ecs::system::{Commands, EntityCommands};
use bevy_ecs::world::World;
use brink_format::Value;
use crate::bindings::{call_ink_function, call_ink_functions};
pub trait IntoBrinkArgs {
fn into_brink_args(self) -> Vec<Value>;
}
impl IntoBrinkArgs for () {
fn into_brink_args(self) -> Vec<Value> {
Vec::new()
}
}
impl IntoBrinkArgs for Vec<Value> {
fn into_brink_args(self) -> Vec<Value> {
self
}
}
impl IntoBrinkArgs for &[Value] {
fn into_brink_args(self) -> Vec<Value> {
self.to_vec()
}
}
macro_rules! impl_into_brink_args_tuple {
($($T:ident $idx:tt),+) => {
impl<$($T: Into<Value>),+> IntoBrinkArgs for ($($T,)+) {
fn into_brink_args(self) -> Vec<Value> {
vec![$(self.$idx.into()),+]
}
}
};
}
impl_into_brink_args_tuple!(A 0);
impl_into_brink_args_tuple!(A 0, B 1);
impl_into_brink_args_tuple!(A 0, B 1, C 2);
impl_into_brink_args_tuple!(A 0, B 1, C 2, D 3);
#[derive(Component)]
pub struct BrinkCallRequest<M: Send + Sync + 'static = ()> {
pub target: Entity,
pub name: String,
pub args: Vec<Value>,
_marker: PhantomData<fn() -> M>,
}
#[derive(EntityEvent)]
pub struct BrinkCallResolved<M: Send + Sync + 'static = ()> {
pub entity: Entity,
pub value: Value,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> BrinkCallResolved<M> {
pub(crate) fn new(entity: Entity, value: Value) -> Self {
Self {
entity,
value,
_marker: PhantomData,
}
}
}
#[derive(EntityEvent)]
pub struct BrinkCallFailed<M: Send + Sync + 'static = ()> {
pub entity: Entity,
pub error: String,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> BrinkCallFailed<M> {
pub(crate) fn new(entity: Entity, error: String) -> Self {
Self {
entity,
error,
_marker: PhantomData,
}
}
}
#[derive(Component)]
pub struct BrinkCallBatchRequest<M: Send + Sync + 'static = ()> {
pub target: Entity,
pub calls: Vec<(String, Vec<Value>)>,
_marker: PhantomData<fn() -> M>,
}
#[derive(EntityEvent)]
pub struct BrinkCallBatchResolved<M: Send + Sync + 'static = ()> {
pub entity: Entity,
pub results: Vec<Result<Value, String>>,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> BrinkCallBatchResolved<M> {
pub(crate) fn new(entity: Entity, results: Vec<Result<Value, String>>) -> Self {
Self {
entity,
results,
_marker: PhantomData,
}
}
}
pub trait BrinkCallCommandsExt {
fn brink_call<M: Send + Sync + 'static>(
&mut self,
flow: Entity,
name: impl Into<String>,
args: impl IntoBrinkArgs,
) -> EntityCommands<'_>;
fn brink_call_batch<M: Send + Sync + 'static>(
&mut self,
flow: Entity,
calls: impl IntoIterator<Item = (impl Into<String>, impl IntoBrinkArgs)>,
) -> EntityCommands<'_>;
}
impl BrinkCallCommandsExt for Commands<'_, '_> {
fn brink_call<M: Send + Sync + 'static>(
&mut self,
flow: Entity,
name: impl Into<String>,
args: impl IntoBrinkArgs,
) -> EntityCommands<'_> {
self.spawn(BrinkCallRequest::<M> {
target: flow,
name: name.into(),
args: args.into_brink_args(),
_marker: PhantomData,
})
}
fn brink_call_batch<M: Send + Sync + 'static>(
&mut self,
flow: Entity,
calls: impl IntoIterator<Item = (impl Into<String>, impl IntoBrinkArgs)>,
) -> EntityCommands<'_> {
let calls = calls
.into_iter()
.map(|(name, args)| (name.into(), args.into_brink_args()))
.collect();
self.spawn(BrinkCallBatchRequest::<M> {
target: flow,
calls,
_marker: PhantomData,
})
}
}
pub fn resolve_brink_calls<M: Send + Sync + 'static>(world: &mut World) {
let mut query = world.query::<(Entity, &BrinkCallRequest<M>)>();
let pending: Vec<(Entity, Entity, String, Vec<Value>)> = query
.iter(world)
.map(|(call_entity, req)| (call_entity, req.target, req.name.clone(), req.args.clone()))
.collect();
for (call_entity, target, name, args) in pending {
match call_ink_function::<M>(world, target, &name, &args) {
Ok(value) => {
world
.entity_mut(call_entity)
.trigger(|e| BrinkCallResolved::<M>::new(e, value));
}
Err(err) => {
let message = err.to_string();
world
.entity_mut(call_entity)
.trigger(|e| BrinkCallFailed::<M>::new(e, message));
}
}
world.despawn(call_entity);
}
}
type PendingBatch = (Entity, Entity, Vec<(String, Vec<Value>)>);
pub fn resolve_brink_call_batches<M: Send + Sync + 'static>(world: &mut World) {
let mut query = world.query::<(Entity, &BrinkCallBatchRequest<M>)>();
let pending: Vec<PendingBatch> = query
.iter(world)
.map(|(call_entity, req)| (call_entity, req.target, req.calls.clone()))
.collect();
for (call_entity, target, calls) in pending {
let results: Vec<Result<Value, String>> =
call_ink_functions::<M, _, _>(world, target, calls)
.into_iter()
.map(|result| result.map_err(|err| err.to_string()))
.collect();
world
.entity_mut(call_entity)
.trigger(|e| BrinkCallBatchResolved::<M>::new(e, results));
world.despawn(call_entity);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{add_story_assets, compile_test_story, make_test_app};
use crate::{BrinkBindingsAppExt, BrinkFlow, BrinkFlowRequest};
use bevy_app::Update;
use bevy_ecs::prelude::*;
#[derive(Component)]
struct Enemy;
fn enemy_count(In((_e, _args)): In<crate::BrinkQueryInput>, q: Query<&Enemy>) -> Value {
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
Value::Int(q.iter().count() as i32)
}
#[test]
fn brink_call_resolves_to_observer() {
#[derive(Resource, Default)]
struct Result(Vec<bool>);
let mut app = make_test_app();
app.init_resource::<Result>();
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
let (program, tables, ctx) = compile_test_story(
"EXTERNAL enemy_count()\n-> END\n=== function can_spawn() ===\n~ return enemy_count() < 3\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn(Enemy);
let flow = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let mut once = true;
app.add_systems(
Update,
move |mut commands: Commands, flows: Query<Entity, With<BrinkFlow<()>>>| {
if !once {
return;
}
once = false;
if let Ok(f) = flows.single() {
commands.brink_call::<()>(f, "can_spawn", ()).observe(
|on: On<BrinkCallResolved<()>>, mut out: ResMut<Result>| {
out.0.push(on.event().value.as_bool().unwrap_or(false));
},
);
}
},
);
app.update();
app.update();
let _ = flow;
let out = &app.world().resource::<Result>().0;
assert_eq!(
out.as_slice(),
[true],
"1 enemy < 3 → can_spawn true, delivered once"
);
}
#[test]
fn brink_call_batch_resolves_ordered_results_to_observer() {
#[derive(Resource, Default)]
struct Result(Vec<Vec<std::result::Result<Value, String>>>);
let mut app = make_test_app();
app.init_resource::<Result>();
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
let (program, tables, ctx) = compile_test_story(
"EXTERNAL enemy_count()\nVAR total = 0\n-> END\n\
=== function add(n) ===\n~ total = total + n\n~ return total\n\
=== function get() ===\n~ return total\n\
=== function seen() ===\n~ return enemy_count()\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build());
app.update();
let mut once = true;
app.add_systems(
Update,
move |mut commands: Commands, flows: Query<Entity, With<BrinkFlow<()>>>| {
if !once {
return;
}
once = false;
if let Ok(f) = flows.single() {
commands
.brink_call_batch::<()>(
f,
[
("add", vec![Value::Int(1)]),
("nope", vec![]), ("seen", vec![]),
("add", vec![Value::Int(10)]),
("get", vec![]),
],
)
.observe(
|on: On<BrinkCallBatchResolved<()>>, mut out: ResMut<Result>| {
out.0.push(on.event().results.clone());
},
);
}
},
);
app.update();
app.update();
let out = &app.world().resource::<Result>().0;
assert_eq!(out.len(), 1, "delivered exactly once");
let results = &out[0];
assert_eq!(results.len(), 5, "one slot per call, no drops");
assert_eq!(results[0].as_ref().unwrap(), &Value::Int(1));
assert!(
results[1].is_err(),
"the bad call fails in its own slot; got {:?}",
results[1]
);
assert_eq!(
results[2].as_ref().unwrap(),
&Value::Int(2),
"a query-backed call still runs post-error"
);
assert_eq!(results[3].as_ref().unwrap(), &Value::Int(11));
assert_eq!(results[4].as_ref().unwrap(), &Value::Int(11));
}
}