bones_ecs/
resources.rs

1//! World resource storage.
2
3use std::{fmt::Debug, marker::PhantomData, sync::Arc};
4
5use once_map::OnceMap;
6
7use crate::prelude::*;
8
9/// An untyped, atomic resource cell.
10pub type AtomicUntypedResource = Arc<UntypedResource>;
11
12/// An untyped resource that may be inserted into [`UntypedResources`].
13///
14/// This is fundamentally a [`Arc<AtomicCell<Option<SchemaBox>>>`] and thus represents
15/// a cell that may or may not contain a resource of it's schema.
16pub struct UntypedResource {
17    cell: AtomicCell<Option<SchemaBox>>,
18    schema: &'static Schema,
19}
20
21impl std::fmt::Debug for UntypedResource {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("UntypedResource").finish_non_exhaustive()
24    }
25}
26
27impl UntypedResource {
28    /// Initialize a new, empty [`UntypedResource`].
29    pub fn empty(schema: &'static Schema) -> Self {
30        Self {
31            cell: AtomicCell::new(None),
32            schema,
33        }
34    }
35
36    /// Creates a new [`UntypedResource`] storing the given data.
37    pub fn new(resource: SchemaBox) -> Self {
38        Self {
39            schema: resource.schema(),
40            cell: AtomicCell::new(Some(resource)),
41        }
42    }
43
44    /// Create a new [`UntypedResource`] for the given schema, initially populated with the default
45    /// value for the schema.
46    pub fn from_default(schema: &'static Schema) -> Self {
47        Self {
48            cell: AtomicCell::new(Some(SchemaBox::default(schema))),
49            schema,
50        }
51    }
52
53    /// Clone the inner data, creating a new copy instead of returning another handle the the same
54    /// data, as the normal `clone()` implementation does.
55    pub fn clone_data(&self) -> Option<SchemaBox> {
56        (*self.cell.borrow()).clone()
57    }
58
59    /// Insert resource data into the cell, returning the previous data.
60    /// # Errors
61    /// Errors if the schema of the data does not match that of this cell.
62    pub fn insert(&self, data: SchemaBox) -> Result<Option<SchemaBox>, SchemaMismatchError> {
63        self.schema.ensure_match(data.schema())?;
64        let mut data = Some(data);
65        std::mem::swap(&mut data, &mut *self.cell.borrow_mut());
66        Ok(data)
67    }
68
69    /// Remove the resource data, returning what was stored in it.
70    pub fn remove(&self) -> Option<SchemaBox> {
71        let mut data = None;
72        std::mem::swap(&mut data, &mut *self.cell.borrow_mut());
73        data
74    }
75
76    /// Borrow the resource.
77    #[track_caller]
78    pub fn borrow(&self) -> Ref<Option<SchemaBox>> {
79        self.cell.borrow()
80    }
81
82    /// Mutably borrow the resource.
83    #[track_caller]
84    pub fn borrow_mut(&self) -> RefMut<Option<SchemaBox>> {
85        self.cell.borrow_mut()
86    }
87
88    /// Get the schema of the resource.
89    pub fn schema(&self) -> &'static Schema {
90        self.schema
91    }
92}
93
94/// Storage for un-typed resources.
95///
96/// This is the backing data store used by [`Resources`].
97///
98/// Unless you are intending to do modding or otherwise need raw pointers to your resource data, you
99/// should use [`Resources`] instead.
100#[derive(Default)]
101pub struct UntypedResources {
102    resources: OnceMap<SchemaId, AtomicUntypedResource>,
103    shared_resources: OnceMap<SchemaId, Box<()>>,
104}
105
106impl Clone for UntypedResources {
107    fn clone(&self) -> Self {
108        let binding = self.resources.read_only_view();
109        let resources = binding.iter().map(|(_, v)| (v.schema, v));
110
111        let new_resources = OnceMap::default();
112        let new_shared_resources = OnceMap::default();
113        for (schema, resource_cell) in resources {
114            let is_shared = self.shared_resources.contains_key(&schema.id());
115
116            if !is_shared {
117                let resource = resource_cell.clone_data();
118                new_resources.map_insert(
119                    schema.id(),
120                    |_| Arc::new(UntypedResource::empty(schema)),
121                    |_, cell| {
122                        if let Some(resource) = resource {
123                            cell.insert(resource).unwrap();
124                        }
125                    },
126                );
127            } else {
128                new_shared_resources.insert(schema.id(), |_| Box::new(()));
129                new_resources.insert(schema.id(), |_| resource_cell.clone());
130            }
131        }
132        Self {
133            resources: new_resources,
134            shared_resources: new_shared_resources,
135        }
136    }
137}
138
139/// Error thrown when a resource cell cannot be inserted because it already exists.
140#[derive(Debug, Clone, Copy)]
141pub struct CellAlreadyPresentError;
142impl std::fmt::Display for CellAlreadyPresentError {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.write_str("Resource cell already present")
145    }
146}
147impl std::error::Error for CellAlreadyPresentError {}
148
149impl UntypedResources {
150    /// Create an empty [`UntypedResources`].
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Check whether or not a cell for the given resource has been initialized yet.
156    pub fn contains_cell(&self, id: SchemaId) -> bool {
157        self.resources.contains_key(&id)
158    }
159
160    /// Check whether or not the resource with the given ID is present.
161    pub fn contains(&self, id: SchemaId) -> bool {
162        self.resources
163            .map_get(&id, |_, cell| cell.borrow().is_some())
164            .unwrap_or_default()
165    }
166
167    /// This is an advanced use-case function that allows you to insert a resource cell directly.
168    ///
169    /// Normally this is completely unnecessary, because cells are automatically inserted lazily as
170    /// requested.
171    ///
172    /// Inserting this manually is used internally for shared resources, by inserting the same
173    /// cell into multiple worlds.
174    ///
175    /// # Errors
176    /// This will error if there is already a cell for the resource present. You cannot add a new
177    /// cell once one has already been inserted.
178    pub fn insert_cell(&self, cell: AtomicUntypedResource) -> Result<(), CellAlreadyPresentError> {
179        let schema = cell.schema;
180        if self.resources.contains_key(&schema.id()) {
181            Err(CellAlreadyPresentError)
182        } else {
183            self.resources.insert(schema.id(), |_| cell);
184            self.shared_resources.insert(schema.id(), |_| Box::new(()));
185            Ok(())
186        }
187    }
188
189    /// Borrow the resource for the given schema.
190    pub fn get(&self, schema: &'static Schema) -> &UntypedResource {
191        self.resources
192            .insert(schema.id(), |_| Arc::new(UntypedResource::empty(schema)))
193    }
194
195    /// Get a cell for the resource with the given schema.
196    pub fn get_cell(&self, schema: &'static Schema) -> AtomicUntypedResource {
197        self.resources.map_insert(
198            schema.id(),
199            |_| Arc::new(UntypedResource::empty(schema)),
200            |_, cell| cell.clone(),
201        )
202    }
203}
204
205/// A collection of resources.
206///
207/// [`Resources`] is essentially a type-map
208#[derive(Clone, Default)]
209pub struct Resources {
210    untyped: UntypedResources,
211}
212
213impl Resources {
214    /// Create an empty [`Resources`].
215    pub fn new() -> Self {
216        Self::default()
217    }
218
219    /// Insert a resource.
220    pub fn insert<T: HasSchema>(&self, resource: T) -> Option<T> {
221        self.untyped
222            .get(T::schema())
223            .insert(SchemaBox::new(resource))
224            .unwrap()
225            .map(|x| x.cast_into())
226    }
227
228    /// Check whether or not a resource is in the store.
229    ///
230    /// See [get()][Self::get]
231    pub fn contains<T: HasSchema>(&self) -> bool {
232        self.untyped.resources.contains_key(&T::schema().id())
233    }
234
235    /// Remove a resource from the store, if it is present.
236    pub fn remove<T: HasSchema>(&self) -> Option<T> {
237        self.untyped
238            .get(T::schema())
239            .remove()
240            // SOUND: we know the type matches because we retrieve it by it's schema.
241            .map(|x| unsafe { x.cast_into_unchecked() })
242    }
243
244    /// Borrow a resource.
245    #[track_caller]
246    pub fn get<T: HasSchema>(&self) -> Option<Ref<T>> {
247        let b = self.untyped.get(T::schema()).borrow();
248        if b.is_some() {
249            Some(Ref::map(b, |b| unsafe {
250                b.as_ref().unwrap().as_ref().cast_into_unchecked()
251            }))
252        } else {
253            None
254        }
255    }
256
257    /// Borrow a resource.
258    #[track_caller]
259    pub fn get_mut<T: HasSchema>(&self) -> Option<RefMut<T>> {
260        let b = self.untyped.get(T::schema()).borrow_mut();
261        if b.is_some() {
262            Some(RefMut::map(b, |b| unsafe {
263                b.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
264            }))
265        } else {
266            None
267        }
268    }
269
270    /// Gets a clone of the resource cell for the resource of the given type.
271    pub fn get_cell<T: HasSchema>(&self) -> AtomicResource<T> {
272        let untyped = self.untyped.get_cell(T::schema()).clone();
273        AtomicResource {
274            untyped,
275            _phantom: PhantomData,
276        }
277    }
278
279    /// Borrow the underlying [`UntypedResources`] store.
280    pub fn untyped(&self) -> &UntypedResources {
281        &self.untyped
282    }
283
284    /// Consume [`Resources`] and extract the underlying [`UntypedResources`].
285    pub fn into_untyped(self) -> UntypedResources {
286        self.untyped
287    }
288}
289
290/// A handle to a resource from a [`Resources`] collection.
291///
292/// This is not the resource itself, but a cheaply clonable handle to it.
293///
294/// To access the resource you must borrow it with either [`borrow()`][Self::borrow] or
295/// [`borrow_mut()`][Self::borrow_mut].
296#[derive(Clone)]
297pub struct AtomicResource<T: HasSchema> {
298    untyped: AtomicUntypedResource,
299    _phantom: PhantomData<T>,
300}
301impl<T: HasSchema + Debug> Debug for AtomicResource<T> {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.write_str("AtomicResource(")?;
304        self.untyped
305            .cell
306            .borrow()
307            .as_ref()
308            .map(|x| x.cast_ref::<T>())
309            .fmt(f)?;
310        f.write_str(")")?;
311        Ok(())
312    }
313}
314
315impl<T: HasSchema + Default> Default for AtomicResource<T> {
316    fn default() -> Self {
317        Self {
318            untyped: Arc::new(UntypedResource::new(SchemaBox::new(T::default()))),
319            _phantom: Default::default(),
320        }
321    }
322}
323
324impl<T: HasSchema> AtomicResource<T> {
325    /// Create a new, empty resource cell.
326    pub fn empty() -> Self {
327        Self {
328            untyped: Arc::new(UntypedResource::empty(T::schema())),
329            _phantom: PhantomData,
330        }
331    }
332
333    /// Create a new resource cell with the given data.
334    pub fn new(data: T) -> Self {
335        AtomicResource {
336            untyped: Arc::new(UntypedResource::new(SchemaBox::new(data))),
337            _phantom: PhantomData,
338        }
339    }
340
341    /// Create from an [`UntypedResource`].
342    pub fn from_untyped(untyped: AtomicUntypedResource) -> Result<Self, SchemaMismatchError> {
343        T::schema().ensure_match(untyped.schema)?;
344        Ok(AtomicResource {
345            untyped,
346            _phantom: PhantomData,
347        })
348    }
349
350    /// Remove the resource from the cell, leaving the cell empty.
351    pub fn remove(&self) -> Option<T> {
352        self.untyped
353            .remove()
354            // SOUND: The untyped data of an atomic resource must always be `T`.
355            .map(|x| unsafe { x.cast_into_unchecked() })
356    }
357
358    /// Lock the resource for reading.
359    ///
360    /// This returns a read guard, very similar to an [`RwLock`][std::sync::RwLock].
361    pub fn borrow(&self) -> Option<Ref<T>> {
362        let borrow = self.untyped.borrow();
363        if borrow.is_some() {
364            Some(Ref::map(borrow, |r| unsafe {
365                r.as_ref().unwrap().as_ref().cast_into_unchecked()
366            }))
367        } else {
368            None
369        }
370    }
371
372    /// Lock the resource for read-writing.
373    ///
374    /// This returns a write guard, very similar to an [`RwLock`][std::sync::RwLock].
375    pub fn borrow_mut(&self) -> Option<RefMut<T>> {
376        let borrow = self.untyped.borrow_mut();
377        if borrow.is_some() {
378            Some(RefMut::map(borrow, |r| unsafe {
379                r.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
380            }))
381        } else {
382            None
383        }
384    }
385
386    /// Convert into an untyped resource.
387    pub fn into_untyped(self) -> AtomicUntypedResource {
388        self.untyped
389    }
390}
391
392impl<T: HasSchema + FromWorld> AtomicResource<T> {
393    /// Initialize the resource using it's [`FromWorld`] implementation, if it is not present.
394    pub fn init(&self, world: &World) {
395        let mut borrow = self.untyped.borrow_mut();
396        if unlikely(borrow.is_none()) {
397            *borrow = Some(SchemaBox::new(T::from_world(world)))
398        }
399    }
400
401    /// Borrow the resource, initializing it if it doesn't exist.
402    #[track_caller]
403    pub fn init_borrow(&self, world: &World) -> Ref<T> {
404        let map_borrow = |borrow| {
405            // SOUND: we know the schema matches.
406            Ref::map(borrow, |b: &Option<SchemaBox>| unsafe {
407                b.as_ref().unwrap().as_ref().cast_into_unchecked()
408            })
409        };
410        let borrow = self.untyped.borrow();
411        if unlikely(borrow.is_none()) {
412            drop(borrow);
413            {
414                let mut borrow_mut = self.untyped.borrow_mut();
415                *borrow_mut = Some(SchemaBox::new(T::from_world(world)));
416            }
417
418            map_borrow(self.untyped.borrow())
419        } else {
420            map_borrow(borrow)
421        }
422    }
423
424    /// Borrow the resource, initializing it if it doesn't exist.
425    #[track_caller]
426    pub fn init_borrow_mut(&self, world: &World) -> RefMut<T> {
427        let mut borrow = self.untyped.borrow_mut();
428        if unlikely(borrow.is_none()) {
429            *borrow = Some(SchemaBox::new(T::from_world(world)));
430        }
431        RefMut::map(borrow, |b| unsafe {
432            b.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
433        })
434    }
435}
436
437#[cfg(test)]
438mod test {
439    use crate::prelude::*;
440
441    #[test]
442    fn sanity_check() {
443        #[derive(HasSchema, Clone, Debug, Default)]
444        #[repr(C)]
445        struct A(String);
446
447        #[derive(HasSchema, Clone, Debug, Default)]
448        #[repr(C)]
449        struct B(u32);
450
451        let r1 = Resources::new();
452
453        r1.insert(A(String::from("hi")));
454        let r1a = r1.get_cell::<A>();
455        assert_eq!(r1a.borrow().unwrap().0, "hi");
456
457        let r2 = r1.clone();
458
459        r1.insert(A(String::from("bye")));
460        r1.insert(A(String::from("world")));
461        assert_eq!(r1a.borrow().unwrap().0, "world");
462
463        let r2a = r2.get_cell::<A>();
464        assert_eq!(r2a.borrow().unwrap().0, "hi");
465
466        r1.insert(B(1));
467        let r1b = r1.get_cell::<B>();
468        assert_eq!(r1b.borrow().unwrap().0, 1);
469        r1.insert(B(2));
470        assert_eq!(r1b.borrow().unwrap().0, 2);
471        assert_eq!(r1a.borrow().unwrap().0, "world");
472    }
473}