bevy_brink/bindings/registration.rs
1//! Registration API: how engine code declares callable ink→engine bindings.
2//!
3//! [`BrinkBindings`] is the registry `Resource`; [`BrinkBindingsAppExt`] is
4//! the `App`-level surface authors call at app-build time
5//! (`bind_brink_fn`/`bind_brink_command`/`bind_brink_query`/`bind_brink_async`/
6//! `bind_brink_task`); [`BrinkHandler`] is the [`ExternalFnHandler`] built
7//! from the registry and handed to a flow's step methods during normal
8//! playback. See the parent module's docs (`crate::bindings`) for the
9//! conceptual overview of the three synchronous binding kinds.
10
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::future::Future;
14use std::marker::PhantomData;
15use std::pin::Pin;
16
17use bevy_app::App;
18use bevy_ecs::entity::Entity;
19use bevy_ecs::event::Event;
20#[cfg(feature = "effect-trace")]
21use bevy_ecs::query::Access;
22use bevy_ecs::resource::Resource;
23#[cfg(feature = "effect-trace")]
24use bevy_ecs::system::BoxedSystem;
25use bevy_ecs::system::{Commands, In, IntoSystem, SystemId};
26use bevy_ecs::world::World;
27use bevy_log::warn;
28use brink_format::Value;
29use brink_runtime::{ExternalFnHandler, ExternalResult};
30use thiserror::Error;
31
32/// Input type for a world-access (`bind_brink_query`) binding system: the
33/// flow entity that triggered the call, plus the ink arguments.
34pub type BrinkQueryInput = (Entity, Vec<Value>);
35
36/// The [`SystemId`] of a registered query binding — a Bevy system taking
37/// [`BrinkQueryInput`] and returning a [`Value`].
38///
39/// `pub(crate)` so the drive/eval API (`super::drive`) can resolve a pending
40/// query external's [`SystemId`] and run it via `run_system_with`.
41pub(crate) type QuerySystemId = SystemId<In<BrinkQueryInput>, Value>;
42
43/// Error produced when ink arguments can't be parsed into a binding's
44/// expected shape. Returned by [`BrinkCommand::from_ink_args`].
45#[derive(Debug, Error, Clone, PartialEq, Eq)]
46pub enum BrinkArgError {
47 /// Wrong number of arguments.
48 #[error("expected {expected} argument(s), got {got}")]
49 Count {
50 /// How many arguments the binding declared.
51 expected: usize,
52 /// How many ink actually passed.
53 got: usize,
54 },
55 /// An argument had the wrong runtime type.
56 #[error("argument {index}: expected {expected}")]
57 Type {
58 /// Zero-based argument position.
59 index: usize,
60 /// The type the binding expected (e.g. `"int"`, `"string"`).
61 expected: &'static str,
62 },
63}
64
65/// A Bevy [`Event`] that can be built from an ink external call's
66/// arguments, for use with [`bind_brink_command`](BrinkBindingsAppExt::bind_brink_command).
67///
68/// Implement (or `#[derive(BrinkCommand)]`) this for the event your
69/// binding fires. The derive generates [`from_ink_args`](Self::from_ink_args)
70/// for structs whose fields are `i32`, `f32`, `bool`, or `String`. To
71/// return a value to ink, hand-implement the trait and override
72/// [`reply`](Self::reply).
73pub trait BrinkCommand: Sized {
74 /// Parse the ink call's arguments (in declaration order) into `Self`.
75 fn from_ink_args(args: &[Value]) -> Result<Self, BrinkArgError>;
76
77 /// The value handed back to ink as this external's return value.
78 ///
79 /// Defaults to [`Value::Null`] — the natural "fire-and-forget, no
80 /// return" behavior. Override to feed a computed value back into the
81 /// story (e.g. a dice roll).
82 fn reply(&self) -> Value {
83 Value::Null
84 }
85}
86
87// Type aliases for the boxed registry entries.
88type PureFn = Box<dyn Fn(&[Value]) -> Value + Send + Sync>;
89type CommandFn = Box<dyn Fn(&[Value]) -> Result<QueuedCommand, BrinkArgError> + Send + Sync>;
90/// Factory for a [`bind_brink_task`](BrinkBindingsAppExt::bind_brink_task)
91/// future: given the ink args, produce a boxed `Send + 'static` future that
92/// computes the external's return value off the main thread.
93///
94/// `pub(crate)` so the drive/eval API (`super::drive`) can spawn the factory's
95/// future when a parked flow's pending external is a `bind_brink_task`.
96pub(crate) type TaskFn =
97 Box<dyn Fn(Vec<Value>) -> Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
98
99/// How an async (defer-across-frames) external resolves once a flow parks on
100/// it. Stored in [`BrinkBindings::async_bindings`].
101///
102/// `pub(crate)` so `super::drive`'s dispatcher can match on the binding kind
103/// (fire an event vs. spawn a task) for a parked flow's pending external.
104pub(crate) enum AsyncKind {
105 /// `bind_brink_async`: fire `BrinkExternalAwaited` and wait for the
106 /// engine to call `resolve_brink_external`.
107 Event,
108 /// `bind_brink_task`: spawn the future on the async task pool and resolve
109 /// with its output when it completes.
110 Task(TaskFn),
111}
112/// A deferred World mutation that triggers a parsed command event. Boxed
113/// so heterogeneous command types share one buffer; run during flush.
114///
115/// `pub(crate)` so the batch driver ([`crate::batch`]) can hold a flow's
116/// buffered command triggers across the batch's Step phase and replay them
117/// in deterministic flow-id order at Apply (`docs/effects-spec.md` §12.4),
118/// rather than flushing each flow's commands immediately as the serial API
119/// does.
120pub(crate) type TriggerFn = Box<dyn FnOnce(&mut World) + Send>;
121
122/// A parsed command ready to be triggered against the World, plus the
123/// value to return to ink.
124struct QueuedCommand {
125 /// Triggers the parsed event when run against the World.
126 trigger: TriggerFn,
127 /// Value returned to ink (usually [`Value::Null`]).
128 reply: Value,
129}
130
131/// Registry of synchronous ink→engine bindings for story marker `M`.
132///
133/// A `Resource`. Populate it at app-build time with
134/// [`bind_brink_fn`](BrinkBindingsAppExt::bind_brink_fn) and
135/// [`bind_brink_command`](BrinkBindingsAppExt::bind_brink_command), then,
136/// in the flow-driving system, call [`handler`](Self::handler) to get a
137/// [`BrinkHandler`] to pass to the flow's step methods.
138#[derive(Resource)]
139pub struct BrinkBindings<M: Send + Sync + 'static = ()> {
140 pure: HashMap<String, PureFn>,
141 commands: HashMap<String, CommandFn>,
142 queries: HashMap<String, QuerySystemId>,
143 /// Each query binding's real, bevy-declared [`Access`], captured once at
144 /// `bind_brink_query` registration time (issue #938's host-side
145 /// ground-truth check — see `crate::ground_truth`'s module docs for why
146 /// registration time is the right moment: bevy's own component access
147 /// is static, so it never varies dispatch to dispatch).
148 #[cfg(feature = "effect-trace")]
149 query_access: HashMap<String, Access>,
150 /// Async (defer-across-frames) bindings: `bind_brink_async` (event) and
151 /// `bind_brink_task` (detached task). A flow pauses on these and resolves
152 /// out-of-band.
153 ///
154 /// `pub(crate)` so `super::drive`'s dispatcher can read a pending
155 /// external's binding kind directly.
156 pub(crate) async_bindings: HashMap<String, AsyncKind>,
157 _marker: PhantomData<fn() -> M>,
158}
159
160impl<M: Send + Sync + 'static> Default for BrinkBindings<M> {
161 fn default() -> Self {
162 Self {
163 pure: HashMap::new(),
164 commands: HashMap::new(),
165 queries: HashMap::new(),
166 #[cfg(feature = "effect-trace")]
167 query_access: HashMap::new(),
168 async_bindings: HashMap::new(),
169 _marker: PhantomData,
170 }
171 }
172}
173
174impl<M: Send + Sync + 'static> BrinkBindings<M> {
175 /// Build a [`BrinkHandler`] borrowing this registry. Pass `&handler`
176 /// to a flow's step method, then call [`BrinkHandler::flush`] to emit
177 /// any buffered command events.
178 ///
179 /// Query bindings (which need World access) yield
180 /// [`ExternalResult::Pending`] so a flow pauses on them; the plugin's
181 /// resolver (or [`advance_flow`](crate::advance_flow)) runs the query
182 /// against the World and resumes.
183 #[must_use]
184 pub fn handler(&self) -> BrinkHandler<'_, M> {
185 BrinkHandler {
186 bindings: self,
187 queued: RefCell::new(Vec::new()),
188 }
189 }
190
191 /// The [`SystemId`] of the query binding registered under `name`, if any.
192 ///
193 /// `pub(crate)` so `super::drive` can resolve a pending query external's
194 /// system to run via `run_system_with`.
195 pub(crate) fn query(&self, name: &str) -> Option<QuerySystemId> {
196 self.queries.get(name).copied()
197 }
198
199 /// The real bevy [`Access`] a `bind_brink_query` binding's system
200 /// carries, if `name` was registered while the `effect-trace` feature
201 /// was enabled (see [`crate::ground_truth`]'s `check` function).
202 #[cfg(feature = "effect-trace")]
203 pub(crate) fn query_access(&self, name: &str) -> Option<&Access> {
204 self.query_access.get(name)
205 }
206
207 /// `true` iff `name` is registered as a
208 /// [`bind_brink_command`](BrinkBindingsAppExt::bind_brink_command)
209 /// binding — consulted by `crate::sleep`'s wake-condition purity gate
210 /// (issue #1609): a command binding mutates the World when its parsed
211 /// event is triggered, so a wake condition reaching one is impure
212 /// regardless of whether a [`CapabilityManifest`](crate::capability::CapabilityManifest)
213 /// entry exists for it. `bevy-brink` knows the binding kind locally —
214 /// this needs no manifest entry to answer.
215 #[must_use]
216 pub(crate) fn is_command(&self, name: &str) -> bool {
217 self.commands.contains_key(name)
218 }
219}
220
221/// An [`ExternalFnHandler`] backed by a [`BrinkBindings`] registry.
222///
223/// Resolves pure-function bindings inline and buffers command-event
224/// triggers (it has no World access mid-step). After stepping, call
225/// [`flush`](Self::flush) to drain the buffered triggers into a
226/// [`Commands`] queue. Unknown names fall through to
227/// [`ExternalResult::Fallback`] so the in-story fallback body (if any)
228/// runs.
229pub struct BrinkHandler<'a, M: Send + Sync + 'static = ()> {
230 bindings: &'a BrinkBindings<M>,
231 /// `pub(crate)` so the `bindings::tests` module can assert on the raw
232 /// buffer in cases where `queued_len`/`take_queued` don't fit (draining
233 /// it directly to apply the triggers).
234 pub(crate) queued: RefCell<Vec<TriggerFn>>,
235}
236
237impl<M: Send + Sync + 'static> BrinkHandler<'_, M> {
238 /// Drain buffered command-event triggers into `commands`. Call once
239 /// after the flow's step method returns (the borrow of `self` taken
240 /// by stepping has ended by then). Consumes the handler.
241 pub fn flush(self, commands: &mut Commands) {
242 for trigger in self.queued.into_inner() {
243 commands.queue(trigger);
244 }
245 }
246
247 /// Take the buffered command-event triggers, leaving the handler empty.
248 /// Used by [`advance_flow`](crate::advance_flow) to accumulate triggers
249 /// across the suspensions of a single line, and by the batch driver
250 /// ([`crate::batch`]) to move a flow's buffered command triggers into
251 /// its per-flow batch outcome for deterministic flow-id-ordered replay
252 /// at Apply.
253 pub(crate) fn take_queued(&self) -> Vec<TriggerFn> {
254 std::mem::take(&mut self.queued.borrow_mut())
255 }
256
257 /// Number of command triggers buffered so far (for tests/diagnostics).
258 #[must_use]
259 pub fn queued_len(&self) -> usize {
260 self.queued.borrow().len()
261 }
262}
263
264/// Resolve `name` against `bindings`, the shared body of
265/// [`BrinkHandler::call`] and [`EvalHandler::call`].
266///
267/// Factored out so the two handlers can never drift the way they did before
268/// issue #1096: `EvalHandler::call` originally only resolved pure and
269/// query/async bindings, missing the `commands` bucket `BrinkHandler::call`
270/// already handled, so a `bind_brink_command`-bound external reached through
271/// an engine→ink call silently fell through to [`ExternalResult::Fallback`].
272/// A binding kind added to one handler and missed in the other is exactly
273/// that drift; with one shared body, it can't happen again.
274///
275/// A pure binding resolves inline. A command binding parses its args and, on
276/// success, buffers the parsed trigger into `queued` (both handlers hold no
277/// World access mid-step, so the trigger fires later — via
278/// [`BrinkHandler::flush`] for normal playback, or `super::drive`'s
279/// `flush_eval_triggers` for an engine→ink call) and resolves with the
280/// binding's reply; on a parse failure it warns and resolves `Null` without
281/// buffering. A world-access query or async binding yields
282/// [`ExternalResult::Pending`] so the caller's driver can pause and resolve
283/// it out-of-band (a query via `run_system_with` between suspensions; an
284/// async binding via the plugin's resolver on the `step_one` path, or an
285/// error from the one-pass exclusive drivers, which can't await it). An
286/// unregistered name falls back to the in-story body, if any.
287fn resolve_binding<M: Send + Sync + 'static>(
288 bindings: &BrinkBindings<M>,
289 queued: &RefCell<Vec<TriggerFn>>,
290 name: &str,
291 args: &[Value],
292) -> ExternalResult {
293 if let Some(f) = bindings.pure.get(name) {
294 return ExternalResult::Resolved(f(args));
295 }
296 if let Some(parse) = bindings.commands.get(name) {
297 return match parse(args) {
298 Ok(queued_cmd) => {
299 queued.borrow_mut().push(queued_cmd.trigger);
300 ExternalResult::Resolved(queued_cmd.reply)
301 }
302 Err(err) => {
303 warn!("brink command '{name}': {err}; emitting nothing, returning null");
304 ExternalResult::Resolved(Value::Null)
305 }
306 };
307 }
308 if bindings.queries.contains_key(name) || bindings.async_bindings.contains_key(name) {
309 return ExternalResult::Pending;
310 }
311 ExternalResult::Fallback
312}
313
314impl<M: Send + Sync + 'static> ExternalFnHandler for BrinkHandler<'_, M> {
315 fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
316 resolve_binding(self.bindings, &self.queued, name, args)
317 }
318}
319
320impl<M: Send + Sync + 'static> BrinkBindings<M> {
321 /// Build an [`EvalHandler`] for an engine→ink call. Pure bindings
322 /// resolve inline; command bindings buffer a trigger (mirroring
323 /// [`BrinkHandler`]) that `super::drive`'s exclusive driver fires against
324 /// the World once the call completes; query bindings yield
325 /// [`ExternalResult::Pending`] so the driver can run them against the
326 /// World between suspensions; everything else falls back to the
327 /// in-story body.
328 ///
329 /// `pub(crate)` so `super::drive`'s exclusive eval driver can build one.
330 pub(crate) fn eval_handler(&self) -> EvalHandler<'_, M> {
331 EvalHandler {
332 bindings: self,
333 queued: RefCell::new(Vec::new()),
334 }
335 }
336}
337
338/// Handler used while evaluating an ink function from engine code
339/// (`super::drive::call_ink_function`). Unlike [`BrinkHandler`], it cannot
340/// touch the World mid-step — a `bind_brink_command`-bound external's trigger
341/// is buffered here (see [`take_queued`](Self::take_queued)) exactly as
342/// [`BrinkHandler`] buffers it, and the exclusive driver fires the buffered
343/// triggers against the World once the call completes (issue #1096: a
344/// command binding reached through this path used to fall through to
345/// [`ExternalResult::Fallback`] — the same silent no-op an *unbound* external
346/// takes — instead of firing its event). World-access (query) bindings are
347/// still deferred to the driver via [`ExternalResult::Pending`].
348///
349/// `pub(crate)` — this is `super::drive::call_ink_function`'s handler type,
350/// returned from [`BrinkBindings::eval_handler`].
351pub(crate) struct EvalHandler<'a, M: Send + Sync + 'static> {
352 bindings: &'a BrinkBindings<M>,
353 /// Command-event triggers queued during this evaluation pass. Can't be
354 /// fired here (no World access mid-step) — the driver drains this via
355 /// [`take_queued`](Self::take_queued) after each begin/resume step and
356 /// fires the triggers directly against the World once the call as a
357 /// whole completes, the same buffer-then-flush shape [`BrinkHandler`]
358 /// uses for normal playback.
359 queued: RefCell<Vec<TriggerFn>>,
360}
361
362impl<M: Send + Sync + 'static> EvalHandler<'_, M> {
363 /// Take the buffered command-event triggers, leaving the handler empty.
364 /// Called by `super::drive` after each `begin_function_eval`/
365 /// `resume_function_eval` step, since a fresh [`EvalHandler`] is built
366 /// per re-borrow of the World.
367 pub(crate) fn take_queued(&self) -> Vec<TriggerFn> {
368 std::mem::take(&mut self.queued.borrow_mut())
369 }
370}
371
372impl<M: Send + Sync + 'static> ExternalFnHandler for EvalHandler<'_, M> {
373 fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
374 resolve_binding(self.bindings, &self.queued, name, args)
375 }
376}
377
378/// App-extension verbs for registering synchronous ink→engine bindings.
379///
380/// Both verbs take the story marker `M` as the first explicit type
381/// parameter (use `()` for the default single-story case). They insert
382/// into the [`BrinkBindings<M>`] resource, creating it on first use.
383pub trait BrinkBindingsAppExt {
384 /// Register a **pure** binding: a side-effect-free function of the ink
385 /// arguments that returns a value to the story. Resolved inline while
386 /// the VM steps — no World access, no latency.
387 ///
388 /// The return type is anything `Into<Value>`, so primitives work
389 /// directly: `|args| 1.5_f32`, `|args| count as i32`, etc.
390 fn bind_brink_fn<M, F, R>(&mut self, name: impl Into<String>, f: F) -> &mut Self
391 where
392 M: Send + Sync + 'static,
393 F: Fn(&[Value]) -> R + Send + Sync + 'static,
394 R: Into<Value>;
395
396 /// Register a **command** binding: parse the ink arguments into a Bevy
397 /// [`Event`] and trigger it (fire-and-forget). The event is buffered
398 /// during stepping and emitted when the handler is flushed. The story
399 /// receives [`BrinkCommand::reply`] as the call's return value
400 /// (`Value::Null` by default).
401 ///
402 /// `E` should be a plain `#[derive(Event)]` (a global observer event):
403 /// react to it with `app.add_observer(|on: On<E>| { … })`.
404 fn bind_brink_command<M, E>(&mut self, name: impl Into<String>) -> &mut Self
405 where
406 M: Send + Sync + 'static,
407 E: Event + BrinkCommand,
408 for<'a> <E as Event>::Trigger<'a>: Default;
409
410 /// Register a **query** binding: a Bevy system with arbitrary
411 /// `SystemParam`s that reads the World and returns a [`Value`] to the
412 /// story. The system takes [`BrinkQueryInput`] — the flow [`Entity`]
413 /// that triggered the call plus the ink arguments.
414 ///
415 /// Resolving a query needs World access, so it can't run inline while
416 /// the VM steps. Engine→ink calls (`super::drive::call_ink_function`)
417 /// drive it via `run_system_with` between suspensions; the binding can
418 /// therefore query anything in the World, with no upfront declaration.
419 ///
420 /// ```no_run
421 /// # use bevy_app::App;
422 /// # use bevy_ecs::component::Component;
423 /// # use bevy_ecs::system::{In, Query};
424 /// # use bevy_brink::{BrinkBindingsAppExt, BrinkQueryInput, Value};
425 /// # #[derive(Component)]
426 /// # struct Enemy;
427 /// # let mut app = App::new();
428 /// fn enemy_count(In((_e, _args)): In<BrinkQueryInput>, q: Query<&Enemy>) -> Value {
429 /// Value::Int(q.iter().count() as i32)
430 /// }
431 /// app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
432 /// ```
433 fn bind_brink_query<M, S, SM>(&mut self, name: impl Into<String>, system: S) -> &mut Self
434 where
435 M: Send + Sync + 'static,
436 S: IntoSystem<In<BrinkQueryInput>, Value, SM> + 'static;
437
438 /// Register an **async (event) primitive** binding: when ink calls the
439 /// external, the flow *parks* and
440 /// [`BrinkExternalAwaited`](crate::BrinkExternalAwaited) fires (once) at
441 /// the flow entity. The engine does whatever multi-frame work the external
442 /// represents (UI, input, world state) and resolves with
443 /// [`resolve_brink_external`](crate::BrinkResolveExternalExt::resolve_brink_external).
444 /// Use this when the value can't be produced in one pass and needs World
445 /// access over several frames.
446 ///
447 /// Only usable on the `step_one` playback path — the one-pass exclusive
448 /// drivers (`super::drive::advance_flow`/`super::drive::call_ink_function`)
449 /// return [`BrinkCallError::AsyncExternalUnsupported`](crate::BrinkCallError::AsyncExternalUnsupported)
450 /// on an async external.
451 fn bind_brink_async<M>(&mut self, name: impl Into<String>) -> &mut Self
452 where
453 M: Send + Sync + 'static;
454
455 /// Register an **async task** binding: when ink calls the external,
456 /// bevy-brink spawns `f(args)` on [`bevy_tasks::AsyncComputeTaskPool`] and
457 /// resolves the flow's external with the future's output once it completes
458 /// (polled each frame by [`poll_brink_tasks`](crate::poll_brink_tasks)).
459 ///
460 /// The future is `Send + 'static` and runs off the main thread, so it
461 /// **cannot access the World** — it computes from the ink arguments only
462 /// (heavy compute, IO, network). For World-dependent async, use
463 /// [`bind_brink_async`](Self::bind_brink_async).
464 ///
465 /// ```no_run
466 /// # use bevy_app::App;
467 /// # use bevy_brink::{BrinkBindingsAppExt, Value};
468 /// # async fn compute_roll(n: i32) -> i32 { n }
469 /// # let mut app = App::new();
470 /// app.bind_brink_task::<(), _, _>("expensive_roll", |args| async move {
471 /// let n = args.first().and_then(Value::as_int).unwrap_or(1);
472 /// Value::Int(compute_roll(n).await)
473 /// });
474 /// ```
475 fn bind_brink_task<M, F, Fut>(&mut self, name: impl Into<String>, f: F) -> &mut Self
476 where
477 M: Send + Sync + 'static,
478 F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
479 Fut: Future<Output = Value> + Send + 'static;
480}
481
482impl BrinkBindingsAppExt for App {
483 fn bind_brink_fn<M, F, R>(&mut self, name: impl Into<String>, f: F) -> &mut Self
484 where
485 M: Send + Sync + 'static,
486 F: Fn(&[Value]) -> R + Send + Sync + 'static,
487 R: Into<Value>,
488 {
489 let name = name.into();
490 {
491 let mut reg = self
492 .world_mut()
493 .get_resource_or_insert_with(BrinkBindings::<M>::default);
494 reg.pure.insert(name, Box::new(move |args| f(args).into()));
495 }
496 self
497 }
498
499 fn bind_brink_command<M, E>(&mut self, name: impl Into<String>) -> &mut Self
500 where
501 M: Send + Sync + 'static,
502 E: Event + BrinkCommand,
503 for<'a> <E as Event>::Trigger<'a>: Default,
504 {
505 let name = name.into();
506 {
507 let mut reg = self
508 .world_mut()
509 .get_resource_or_insert_with(BrinkBindings::<M>::default);
510 reg.commands.insert(
511 name,
512 Box::new(move |args: &[Value]| {
513 let event = E::from_ink_args(args)?;
514 let reply = event.reply();
515 Ok(QueuedCommand {
516 trigger: Box::new(move |world: &mut World| {
517 world.trigger(event);
518 }),
519 reply,
520 })
521 }),
522 );
523 }
524 self
525 }
526
527 fn bind_brink_query<M, S, SM>(&mut self, name: impl Into<String>, system: S) -> &mut Self
528 where
529 M: Send + Sync + 'static,
530 S: IntoSystem<In<BrinkQueryInput>, Value, SM> + 'static,
531 {
532 let name = name.into();
533 // With `effect-trace`, capture the system's real bevy-declared
534 // `Access` once, at registration — bevy's own component access is
535 // static (a `Query<&Foo>` declares the same access whether or not it
536 // ever matches an entity), so this is the exact ground truth
537 // `crate::ground_truth::check` later compares real dispatches
538 // against. `register_boxed_system` (rather than `register_system`)
539 // lets us call `System::initialize` ourselves first without needing
540 // `S: Clone`; the already-initialized boxed system is then handed to
541 // bevy exactly as `register_system` would have built it, so the
542 // registered binding behaves identically either way.
543 #[cfg(feature = "effect-trace")]
544 let (id, access) = {
545 let mut boxed: BoxedSystem<In<BrinkQueryInput>, Value> =
546 Box::new(IntoSystem::into_system(system));
547 let access = boxed.initialize(self.world_mut()).combined_access().clone();
548 let id = self.world_mut().register_boxed_system(boxed);
549 (id, access)
550 };
551 #[cfg(not(feature = "effect-trace"))]
552 let id = self.world_mut().register_system(system);
553 {
554 let mut reg = self
555 .world_mut()
556 .get_resource_or_insert_with(BrinkBindings::<M>::default);
557 #[cfg(feature = "effect-trace")]
558 reg.query_access.insert(name.clone(), access);
559 reg.queries.insert(name, id);
560 }
561 self
562 }
563
564 fn bind_brink_async<M>(&mut self, name: impl Into<String>) -> &mut Self
565 where
566 M: Send + Sync + 'static,
567 {
568 let name = name.into();
569 {
570 let mut reg = self
571 .world_mut()
572 .get_resource_or_insert_with(BrinkBindings::<M>::default);
573 reg.async_bindings.insert(name, AsyncKind::Event);
574 }
575 self
576 }
577
578 fn bind_brink_task<M, F, Fut>(&mut self, name: impl Into<String>, f: F) -> &mut Self
579 where
580 M: Send + Sync + 'static,
581 F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
582 Fut: Future<Output = Value> + Send + 'static,
583 {
584 let name = name.into();
585 let factory: TaskFn = Box::new(move |args| Box::pin(f(args)));
586 {
587 let mut reg = self
588 .world_mut()
589 .get_resource_or_insert_with(BrinkBindings::<M>::default);
590 reg.async_bindings.insert(name, AsyncKind::Task(factory));
591 }
592 self
593 }
594}