1use std::marker::PhantomData;
27
28use bevy_ecs::component::Component;
29use bevy_ecs::entity::Entity;
30use bevy_ecs::event::EntityEvent;
31use bevy_ecs::system::{Commands, Query};
32use bevy_ecs::world::World;
33use bevy_log::warn;
34use bevy_tasks::{Task, block_on, poll_once};
35use brink_format::Value;
36
37use crate::flow::BrinkFlow;
38
39#[derive(EntityEvent)]
60pub struct BrinkExternalAwaited<M: Send + Sync + 'static = ()> {
61 pub entity: Entity,
63 pub name: String,
65 pub args: Vec<Value>,
67 _marker: PhantomData<fn() -> M>,
68}
69
70impl<M: Send + Sync + 'static> BrinkExternalAwaited<M> {
71 pub(crate) fn new(entity: Entity, name: String, args: Vec<Value>) -> Self {
72 Self {
73 entity,
74 name,
75 args,
76 _marker: PhantomData,
77 }
78 }
79}
80
81#[derive(Component)]
87pub struct BrinkAwaiting<M: Send + Sync + 'static = ()> {
88 pub name: String,
90 _marker: PhantomData<fn() -> M>,
91}
92
93impl<M: Send + Sync + 'static> BrinkAwaiting<M> {
94 pub(crate) fn new(name: String) -> Self {
95 Self {
96 name,
97 _marker: PhantomData,
98 }
99 }
100}
101
102#[derive(Component)]
108pub struct BrinkPendingTask<M: Send + Sync + 'static = ()> {
109 pub(crate) task: Task<Value>,
110 #[cfg(feature = "dev")]
114 name: String,
115 #[cfg(feature = "dev")]
116 args: Vec<Value>,
117 _marker: PhantomData<fn() -> M>,
118}
119
120impl<M: Send + Sync + 'static> BrinkPendingTask<M> {
121 pub(crate) fn new(
122 task: Task<Value>,
123 #[cfg(feature = "dev")] name: String,
124 #[cfg(feature = "dev")] args: Vec<Value>,
125 ) -> Self {
126 Self {
127 task,
128 #[cfg(feature = "dev")]
129 name,
130 #[cfg(feature = "dev")]
131 args,
132 _marker: PhantomData,
133 }
134 }
135}
136
137pub trait BrinkResolveExternalExt {
139 fn resolve_brink_external<M: Send + Sync + 'static>(&mut self, flow: Entity, value: Value);
145}
146
147impl BrinkResolveExternalExt for Commands<'_, '_> {
148 fn resolve_brink_external<M: Send + Sync + 'static>(&mut self, flow: Entity, value: Value) {
149 self.queue(move |world: &mut World| {
150 resolve_external_world::<M>(world, flow, value);
151 });
152 }
153}
154
155pub(crate) fn resolve_external_world<M: Send + Sync + 'static>(
159 world: &mut World,
160 flow: Entity,
161 value: Value,
162) {
163 #[cfg(feature = "dev")]
167 let record_info = {
168 let name = world.get::<BrinkAwaiting<M>>(flow).map(|a| a.name.clone());
169 let args = world
170 .get::<BrinkFlow<M>>(flow)
171 .filter(|f| f.inner.has_pending_external())
172 .map(|f| f.inner.pending_external_args().to_vec());
173 name.zip(args).map(|(n, a)| (n, a, value.clone()))
174 };
175
176 let resolved = {
177 let mut flows = world.query::<&mut BrinkFlow<M>>();
178 match flows.get_mut(world, flow) {
179 Ok(mut f) if f.inner.has_pending_external() => {
180 f.inner.resolve_external(value);
181 true
182 }
183 Ok(_) => {
184 warn!(
185 "resolve_brink_external on {flow:?}: flow has no pending external \
186 (already resolved?); ignoring"
187 );
188 false
189 }
190 Err(_) => {
191 warn!("resolve_brink_external on {flow:?}: not a brink flow; ignoring");
192 false
193 }
194 }
195 };
196 if resolved {
197 world.entity_mut(flow).remove::<BrinkAwaiting<M>>();
198 #[cfg(feature = "dev")]
199 if let Some((name, args, recorded)) = record_info {
200 crate::replay::record_external::<M>(world, flow, &name, &args, &recorded);
201 }
202 }
203}
204
205pub fn poll_brink_tasks<M: Send + Sync + 'static>(
210 mut tasks: Query<(Entity, &mut BrinkPendingTask<M>, &mut BrinkFlow<M>)>,
211 mut commands: Commands,
212) {
213 for (entity, mut pending, mut flow) in &mut tasks {
214 if let Some(value) = block_on(poll_once(&mut pending.task)) {
215 if flow.inner.has_pending_external() {
217 #[cfg(feature = "dev")]
221 {
222 let (name, args, recorded) =
223 (pending.name.clone(), pending.args.clone(), value.clone());
224 commands.queue(move |world: &mut World| {
225 crate::replay::record_external::<M>(world, entity, &name, &args, &recorded);
226 });
227 }
228 flow.inner.resolve_external(value);
229 }
230 commands.entity(entity).remove::<BrinkPendingTask<M>>();
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::asset::{LineTablesAsset, ProgramAsset};
239 use crate::test_support::{add_story_assets, compile_test_story, make_test_app};
240 use crate::{
241 Advance, BrinkBindings, BrinkBindingsAppExt, BrinkContext, BrinkFlowRequest, BrinkLocale,
242 BrinkProgram, advance_flow,
243 };
244 use bevy_app::{App, Update};
245 use bevy_asset::Assets;
246 use bevy_ecs::prelude::*;
247
248 #[derive(Resource, Default)]
249 struct Lines(Vec<String>);
250
251 #[expect(
256 clippy::type_complexity,
257 clippy::needless_pass_by_value,
258 reason = "bevy systems take their params (Query/Res) by value"
259 )]
260 fn step_driver(
261 mut flows: Query<(
262 Entity,
263 &mut BrinkFlow<()>,
264 &mut BrinkContext<()>,
265 &BrinkProgram<()>,
266 &BrinkLocale<()>,
267 )>,
268 globals: Option<ResMut<crate::BrinkGlobals<()>>>,
269 programs: Res<Assets<ProgramAsset>>,
270 tables: Res<Assets<LineTablesAsset>>,
271 bindings: Res<BrinkBindings<()>>,
272 mut commands: Commands,
273 mut out: ResMut<Lines>,
274 ) {
275 let Some(mut globals) = globals else {
276 return;
277 };
278 for (entity, mut flow, mut ctx, prog, loc) in &mut flows {
279 if flow.inner.has_pending_external() {
280 continue;
281 }
282 let (Some(p), Some(t)) = (programs.get(&prog.handle), tables.get(&loc.handle)) else {
283 continue;
284 };
285 let handler = bindings.handler();
286 let mut view = crate::globals::flow_context_view(&mut globals, &mut ctx);
287 if let Ok(Advance::Step(line)) = flow.step_one(
288 &p.program,
289 &t.tables,
290 &mut view,
291 &handler,
292 entity,
293 &mut commands,
294 ) {
295 out.0.push(line.text().to_string());
296 }
297 handler.flush(&mut commands);
298 }
299 }
300
301 fn spawn_flow(app: &mut App, src: &str) -> Entity {
302 let (program, tables, ctx) = compile_test_story(src);
303 let story = add_story_assets(app, program, tables, ctx);
304 let entity = app
305 .world_mut()
306 .spawn(BrinkFlowRequest::<()>::builder().story(story).build())
307 .id();
308 app.update(); entity
310 }
311
312 fn pending(app: &App, flow: Entity) -> bool {
313 app.world()
314 .entity(flow)
315 .get::<BrinkFlow<()>>()
316 .is_some_and(|f| f.inner.has_pending_external())
317 }
318
319 #[test]
323 fn task_binding_resolves_across_frames() {
324 let mut app = make_test_app();
325 app.init_resource::<Lines>();
326 app.add_systems(Update, step_driver);
327 app.bind_brink_task::<(), _, _>("expensive_roll", |args: Vec<Value>| async move {
328 let n = args.first().and_then(Value::as_int).unwrap_or(0);
329 Value::Int(n * 2)
330 });
331
332 let flow = spawn_flow(
333 &mut app,
334 "EXTERNAL expensive_roll(n)\nRolled: {expensive_roll(21)}.\n-> END\n",
335 );
336
337 let mut got = false;
339 for _ in 0..200 {
340 app.update();
341 if app
342 .world()
343 .resource::<Lines>()
344 .0
345 .iter()
346 .any(|l| l.contains("Rolled: 42."))
347 {
348 got = true;
349 break;
350 }
351 }
352 assert!(
353 got,
354 "task should resolve to 42 and resume the flow; got {:?}",
355 app.world().resource::<Lines>().0
356 );
357 assert!(!pending(&app, flow), "flow no longer parked after resolve");
358 }
359
360 #[test]
363 fn async_event_binding_fires_once_and_resolves() {
364 #[derive(Resource, Default)]
365 struct Awaited(Vec<String>);
366
367 let mut app = make_test_app();
368 app.init_resource::<Lines>();
369 app.init_resource::<Awaited>();
370 app.add_systems(Update, step_driver);
371 app.bind_brink_async::<()>("pick_target");
372 app.add_observer(
373 |on: On<BrinkExternalAwaited<()>>, mut commands: Commands, mut log: ResMut<Awaited>| {
374 log.0.push(on.event().name.clone());
375 commands.resolve_brink_external::<()>(on.event().entity, Value::Int(7));
376 },
377 );
378
379 spawn_flow(
380 &mut app,
381 "EXTERNAL pick_target()\nYou aim at {pick_target()}.\n-> END\n",
382 );
383
384 let mut got = false;
385 for _ in 0..50 {
386 app.update();
387 if app
388 .world()
389 .resource::<Lines>()
390 .0
391 .iter()
392 .any(|l| l.contains("You aim at 7."))
393 {
394 got = true;
395 break;
396 }
397 }
398 assert!(
399 got,
400 "observer should resolve pick_target to 7 and resume; got {:?}",
401 app.world().resource::<Lines>().0
402 );
403 assert_eq!(
404 app.world().resource::<Awaited>().0,
405 vec!["pick_target".to_string()],
406 "BrinkExternalAwaited fires exactly once"
407 );
408 }
409
410 #[test]
414 fn async_event_binding_stays_frozen_until_resolved() {
415 #[derive(Resource, Default)]
416 struct FireCount(usize);
417
418 let mut app = make_test_app();
419 app.init_resource::<Lines>();
420 app.init_resource::<FireCount>();
421 app.add_systems(Update, step_driver);
422 app.bind_brink_async::<()>("pick_target");
423 app.add_observer(
425 |_on: On<BrinkExternalAwaited<()>>, mut n: ResMut<FireCount>| {
426 n.0 += 1;
427 },
428 );
429
430 let flow = spawn_flow(
431 &mut app,
432 "EXTERNAL pick_target()\nYou aim at {pick_target()}.\n-> END\n",
433 );
434
435 for _ in 0..20 {
436 app.update();
437 }
438
439 assert!(pending(&app, flow), "flow stays parked without resolution");
440 assert_eq!(
441 app.world().resource::<FireCount>().0,
442 1,
443 "event fires once, not per frame"
444 );
445 assert!(
446 !app.world()
447 .resource::<Lines>()
448 .0
449 .iter()
450 .any(|l| l.contains("aim at")),
451 "no resolved line while frozen"
452 );
453 }
454
455 #[test]
458 fn advance_flow_rejects_async_external() {
459 let mut app = make_test_app();
460 app.bind_brink_async::<()>("pick_target");
461
462 let flow = spawn_flow(
463 &mut app,
464 "EXTERNAL pick_target()\nYou aim at {pick_target()}.\n-> END\n",
465 );
466
467 let err = advance_flow::<()>(app.world_mut(), flow).unwrap_err();
468 assert!(
469 matches!(err, crate::BrinkCallError::AsyncExternalUnsupported(ref n) if n == "pick_target"),
470 "got {err:?}"
471 );
472 }
473}