Skip to main content

loam_runtime/
command.rs

1use std::fmt;
2
3use crate::bridge::Bridge;
4use crate::domain::{
5    ChartCommand, ChartPose, DomainError, DomainHandle, DomainId, DomainSpace, Domains, Field,
6    Instance, Pose,
7};
8use crate::entity::{Entities, EntitiesSnapshot, Entity, SceneId};
9use crate::relation::Relation;
10use crate::session::{
11    Assets, Material, MaterialId, PaletteId, PreparedGeometry, PreparedId, RestoreError,
12};
13use crate::store::{Owner, StoreError};
14use crate::stores::{HasRelation, HasStore, Stores};
15use crate::view::Views;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct RequestId(u64);
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Outcome {
22    Done,
23    Spawned(Entity),
24}
25
26impl fmt::Display for Outcome {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Done => f.write_str("done"),
30            Self::Spawned(entity) => write!(f, "spawned {entity}"),
31        }
32    }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum Rejection {
37    Reserved(Entity),
38    Capacity,
39    Domain(DomainError),
40    Store(StoreError),
41    Restore(RestoreError),
42    Cancelled,
43    Unsupported(&'static str),
44    /// A world-owned edit refused the command before any change.
45    #[cfg(feature = "physics")]
46    Edit(loam_physics::EditError),
47}
48
49impl fmt::Display for Rejection {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Self::Reserved(entity) => write!(f, "{entity} is still reserved"),
53            Self::Capacity => f.write_str("the queue is full"),
54            Self::Domain(error) => fmt::Display::fmt(error, f),
55            Self::Store(error) => fmt::Display::fmt(error, f),
56            Self::Restore(error) => fmt::Display::fmt(error, f),
57            Self::Cancelled => f.write_str("cancelled by a reset"),
58            Self::Unsupported(what) => write!(f, "unsupported: {what}"),
59            #[cfg(feature = "physics")]
60            Self::Edit(error) => fmt::Display::fmt(error, f),
61        }
62    }
63}
64
65impl From<DomainError> for Rejection {
66    fn from(error: DomainError) -> Self {
67        Self::Domain(error)
68    }
69}
70
71impl From<StoreError> for Rejection {
72    fn from(error: StoreError) -> Self {
73        Self::Store(error)
74    }
75}
76
77#[cfg(feature = "physics")]
78impl From<loam_physics::EditError> for Rejection {
79    fn from(error: loam_physics::EditError) -> Self {
80        Self::Edit(error)
81    }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct CommandResult {
86    pub request: RequestId,
87    pub name: &'static str,
88    pub outcome: Result<Outcome, Rejection>,
89}
90
91/// A simulation-time spawn; the entity resolves only after the boundary commits it.
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct Reservation {
94    pub request: RequestId,
95    pub entity: Entity,
96}
97
98pub trait AppCommand<A>: Send + 'static {
99    fn name(&self) -> &'static str;
100
101    fn apply(&mut self, dispatch: &mut Dispatch<'_, A>) -> Result<Outcome, Rejection>;
102}
103
104pub enum Command<A> {
105    Spawn(SpawnBundle<A>),
106    Despawn(Entity),
107    Chart(DomainId, ChartCommand),
108    App(Box<dyn AppCommand<A>>),
109    Reset,
110}
111
112impl<A: 'static> Command<A> {
113    pub fn name(&self) -> &'static str {
114        match self {
115            Self::Spawn(_) => "spawn",
116            Self::Despawn(_) => "despawn",
117            Self::Chart(_, _) => "chart",
118            Self::App(command) => command.name(),
119            Self::Reset => "reset",
120        }
121    }
122}
123
124impl<A> Command<A> {
125    pub fn try_app_fn(
126        name: &'static str,
127        apply: impl FnMut(&mut Dispatch<'_, A>) -> Result<Outcome, Rejection> + Send + 'static,
128    ) -> Self {
129        Self::App(Box::new(FnCommand { name, apply }))
130    }
131}
132
133pub struct Request<A> {
134    pub id: RequestId,
135    pub command: Command<A>,
136}
137
138/// Deferred to the next boundary; a result arrives before the next entry that can observe it.
139pub struct Commands<A> {
140    queue: Vec<Request<A>>,
141    entities: Entities,
142    next: u64,
143}
144
145impl<A: Stores> Commands<A> {
146    pub(crate) fn new(scene: SceneId) -> Self {
147        Self {
148            queue: Vec::new(),
149            entities: Entities::new(scene),
150            next: 0,
151        }
152    }
153
154    pub(crate) fn entities(&self) -> &Entities {
155        &self.entities
156    }
157
158    pub(crate) fn entities_mut(&mut self) -> &mut Entities {
159        &mut self.entities
160    }
161
162    pub(crate) fn drain_into(&mut self, into: &mut Vec<Request<A>>) {
163        into.append(&mut self.queue);
164    }
165
166    pub(crate) fn cancel_into(&mut self, results: &mut Vec<CommandResult>) {
167        for request in self.queue.drain(..) {
168            if let Command::Spawn(SpawnBundle {
169                reserved: Some(entity),
170                ..
171            }) = &request.command
172            {
173                self.entities.release(*entity);
174            }
175            results.push(CommandResult {
176                request: request.id,
177                name: request.command.name(),
178                outcome: Err(Rejection::Cancelled),
179            });
180        }
181    }
182
183    pub(crate) fn next_request(&self) -> RequestId {
184        RequestId(self.next)
185    }
186
187    pub(crate) fn restore(&mut self, entities: &EntitiesSnapshot, next: RequestId) {
188        self.entities.restore(entities);
189        self.next = self.next.max(next.0);
190    }
191
192    pub fn submit(&mut self, command: Command<A>) -> RequestId {
193        let id = RequestId(self.next);
194        self.next += 1;
195        self.queue.push(Request { id, command });
196        id
197    }
198
199    pub fn app(&mut self, command: impl AppCommand<A>) -> RequestId {
200        self.submit(Command::App(Box::new(command)))
201    }
202
203    pub fn try_app_fn(
204        &mut self,
205        name: &'static str,
206        apply: impl FnMut(&mut Dispatch<'_, A>) -> Result<Outcome, Rejection> + Send + 'static,
207    ) -> RequestId {
208        self.submit(Command::try_app_fn(name, apply))
209    }
210
211    pub fn spawn(&mut self, mut bundle: SpawnBundle<A>) -> Result<Reservation, Rejection> {
212        let entity = self.entities.reserve();
213        bundle.reserved = Some(entity);
214        let request = self.submit(Command::Spawn(bundle));
215        Ok(Reservation { request, entity })
216    }
217
218    pub fn pending(&self) -> &[Request<A>] {
219        &self.queue
220    }
221
222    pub fn len(&self) -> usize {
223        self.queue.len()
224    }
225
226    pub fn is_empty(&self) -> bool {
227        self.queue.is_empty()
228    }
229}
230
231struct FnCommand<F> {
232    name: &'static str,
233    apply: F,
234}
235
236impl<A, F> AppCommand<A> for FnCommand<F>
237where
238    F: FnMut(&mut Dispatch<'_, A>) -> Result<Outcome, Rejection> + Send + 'static,
239{
240    fn name(&self) -> &'static str {
241        self.name
242    }
243
244    fn apply(&mut self, dispatch: &mut Dispatch<'_, A>) -> Result<Outcome, Rejection> {
245        (self.apply)(dispatch)
246    }
247}
248
249trait Place: Send {
250    fn place(
251        &self,
252        domains: &mut Domains,
253        entity: Entity,
254        instance: Option<Instance>,
255    ) -> Result<DomainId, Rejection>;
256}
257
258struct TypedPlace<S: DomainSpace> {
259    domain: DomainHandle<S>,
260    pose: Pose<S>,
261}
262
263impl<S: DomainSpace> Place for TypedPlace<S> {
264    fn place(
265        &self,
266        domains: &mut Domains,
267        entity: Entity,
268        instance: Option<Instance>,
269    ) -> Result<DomainId, Rejection> {
270        let domain = domains.typed(self.domain)?;
271        domain.attach_pose(entity, self.pose)?;
272        if let Some(instance) = instance {
273            domain.attach_instance(entity, instance)?;
274        }
275        Ok(self.domain.id())
276    }
277}
278
279struct ChartPlace {
280    domain: DomainId,
281    pose: ChartPose,
282}
283
284impl Place for ChartPlace {
285    fn place(
286        &self,
287        domains: &mut Domains,
288        entity: Entity,
289        instance: Option<Instance>,
290    ) -> Result<DomainId, Rejection> {
291        let domain = domains
292            .facade(self.domain)
293            .ok_or(DomainError::UnknownDomain(self.domain))?;
294        domain.apply(&ChartCommand::Place {
295            entity,
296            pose: self.pose,
297        })?;
298        if let Some(instance) = instance {
299            domain.apply(&ChartCommand::Attach { entity, instance })?;
300        }
301        Ok(self.domain)
302    }
303}
304
305trait Attach<A>: Send {
306    fn attach(self: Box<Self>, app: &mut A, entity: Entity) -> Result<(), StoreError>;
307}
308
309struct Row<T>(T);
310
311impl<A: HasStore<T>, T: Send + 'static> Attach<A> for Row<T> {
312    fn attach(self: Box<Self>, app: &mut A, entity: Entity) -> Result<(), StoreError> {
313        app.store_mut().insert_raw(entity, self.0)
314    }
315}
316
317/// One entity and its initial attachments, accepted or rejected together.
318pub struct SpawnBundle<A> {
319    placement: Option<Box<dyn Place>>,
320    instance: Option<Instance>,
321    rows: Vec<Box<dyn Attach<A>>>,
322    reserved: Option<Entity>,
323}
324
325impl<A: Stores> Default for SpawnBundle<A> {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331impl<A: Stores> SpawnBundle<A> {
332    pub fn new() -> Self {
333        Self {
334            placement: None,
335            instance: None,
336            rows: Vec::new(),
337            reserved: None,
338        }
339    }
340
341    pub fn at<S: DomainSpace>(mut self, domain: DomainHandle<S>, pose: Pose<S>) -> Self {
342        self.placement = Some(Box::new(TypedPlace { domain, pose }));
343        self
344    }
345
346    pub fn at_chart(mut self, domain: DomainId, pose: ChartPose) -> Self {
347        self.placement = Some(Box::new(ChartPlace { domain, pose }));
348        self
349    }
350
351    pub fn instance(mut self, instance: Instance) -> Self {
352        self.instance = Some(instance);
353        self
354    }
355
356    pub fn row<T: Send + 'static>(mut self, row: T) -> Self
357    where
358        A: HasStore<T>,
359    {
360        self.rows.push(Box::new(Row(row)));
361        self
362    }
363}
364
365/// Immediate application at a boundary; every handle it returns is live.
366pub struct Dispatch<'a, A> {
367    pub app: &'a mut A,
368    pub domains: &'a mut Domains,
369    pub views: &'a mut Views,
370    entities: &'a mut Entities,
371    bridges: &'a mut Relation<Bridge>,
372    assets: &'a mut Assets,
373}
374
375impl<'a, A: Stores> Dispatch<'a, A> {
376    pub(crate) fn new(
377        app: &'a mut A,
378        domains: &'a mut Domains,
379        views: &'a mut Views,
380        entities: &'a mut Entities,
381        bridges: &'a mut Relation<Bridge>,
382        assets: &'a mut Assets,
383    ) -> Self {
384        Self {
385            app,
386            domains,
387            views,
388            entities,
389            bridges,
390            assets,
391        }
392    }
393
394    pub fn prepare(&mut self, geometry: PreparedGeometry) -> PreparedId {
395        self.assets.prepare(geometry)
396    }
397
398    pub fn prepared(&self, id: PreparedId) -> Option<&PreparedGeometry> {
399        self.assets.prepared(id)
400    }
401
402    pub fn add_palette(&mut self, colors: Vec<[f32; 4]>) -> PaletteId {
403        self.assets.add_palette(colors)
404    }
405
406    pub fn palette(&self, id: PaletteId) -> Option<&[[f32; 4]]> {
407        self.assets.palette(id)
408    }
409
410    pub fn add_material(&mut self, material: Material) -> MaterialId {
411        self.assets.add_material(material)
412    }
413
414    pub fn material(&self, id: MaterialId) -> Option<&Material> {
415        self.assets.material(id)
416    }
417
418    pub fn spawn(&mut self, bundle: SpawnBundle<A>) -> Result<Entity, Rejection> {
419        let entity = match bundle.reserved {
420            Some(entity) if self.entities.is_reserved(entity) => entity,
421            Some(entity) => return Err(Rejection::Domain(DomainError::Stale(entity))),
422            None => self.entities.reserve(),
423        };
424        match self.attach_bundle(entity, bundle) {
425            Ok(()) => {
426                self.entities.commit(entity);
427                Ok(entity)
428            }
429            Err(rejection) => {
430                self.detach(entity);
431                self.entities.release(entity);
432                Err(rejection)
433            }
434        }
435    }
436
437    pub fn attach<T: Send + 'static>(&mut self, entity: Entity, row: T) -> Result<(), Rejection>
438    where
439        A: HasStore<T>,
440    {
441        self.require_live(entity)?;
442        self.app.store_mut().insert_raw(entity, row)?;
443        Ok(())
444    }
445
446    pub fn link<T: Send + 'static>(
447        &mut self,
448        from: Entity,
449        to: Entity,
450        data: T,
451    ) -> Result<crate::relation::LinkId, Rejection>
452    where
453        A: HasRelation<T>,
454    {
455        self.require_live(from)?;
456        self.require_live(to)?;
457        Ok(self.app.relation_mut().link_raw(from, to, data)?)
458    }
459
460    pub fn attach_instance<S: DomainSpace>(
461        &mut self,
462        domain: DomainHandle<S>,
463        entity: Entity,
464        instance: Instance,
465    ) -> Result<(), Rejection> {
466        self.require_live(entity)?;
467        self.domains
468            .typed(domain)?
469            .attach_instance(entity, instance)?;
470        Ok(())
471    }
472
473    pub fn attach_field<S: DomainSpace>(
474        &mut self,
475        domain: DomainHandle<S>,
476        entity: Entity,
477        field: Field,
478    ) -> Result<(), Rejection> {
479        self.require_live(entity)?;
480        self.domains.typed(domain)?.attach_field(entity, field)?;
481        Ok(())
482    }
483
484    fn attach_bundle(&mut self, entity: Entity, bundle: SpawnBundle<A>) -> Result<(), Rejection> {
485        if let Some(placement) = &bundle.placement {
486            placement.place(self.domains, entity, bundle.instance)?;
487        }
488        for row in bundle.rows {
489            row.attach(self.app, entity)?;
490        }
491        Ok(())
492    }
493
494    fn detach(&mut self, entity: Entity) {
495        self.app.release(entity, Owner::new());
496        for domain in self.domains.iter_mut() {
497            domain.release(entity);
498        }
499        while let Some(id) = self
500            .bridges
501            .outgoing(entity)
502            .next()
503            .or_else(|| self.bridges.incoming(entity).next())
504        {
505            let Ok(link) = self.bridges.unlink(id) else {
506                break;
507            };
508            self.views.unplace(link.data.image);
509        }
510    }
511
512    pub fn despawn(&mut self, entity: Entity) -> Result<(), Rejection> {
513        if self.entities.is_reserved(entity) {
514            return Err(Rejection::Reserved(entity));
515        }
516        if self.entities.resolve(entity).is_none() {
517            return Err(Rejection::Domain(DomainError::Stale(entity)));
518        }
519        self.detach(entity);
520        self.entities.despawn(entity)?;
521        Ok(())
522    }
523
524    fn require_live(&self, entity: Entity) -> Result<(), Rejection> {
525        self.entities
526            .resolve(entity)
527            .map(|_| ())
528            .ok_or(Rejection::Domain(DomainError::Stale(entity)))
529    }
530
531    pub fn apply(&mut self, command: Command<A>) -> Result<Outcome, Rejection> {
532        match command {
533            Command::Spawn(bundle) => self.spawn(bundle).map(Outcome::Spawned),
534            Command::Despawn(entity) => self.despawn(entity).map(|()| Outcome::Done),
535            Command::Chart(domain, command) => {
536                let entity = command.entity();
537                if self.entities.resolve(entity).is_none() {
538                    return Err(Rejection::Domain(DomainError::Stale(entity)));
539                }
540                self.domains
541                    .facade(domain)
542                    .ok_or(DomainError::UnknownDomain(domain))?
543                    .apply(&command)
544            }
545            Command::App(mut command) => command.apply(self),
546            Command::Reset => Err(Rejection::Unsupported(
547                "reset applies at a session boundary",
548            )),
549        }
550    }
551
552    pub fn entities(&self) -> &Entities {
553        self.entities
554    }
555}