xvc_ecs/ecs/
mod.rs

1//! Xvc Entity Component System allows arbitrary [serializable][Storable] components associated
2//! with [entities][XvcEntities] of integers.
3//! It's used instead of _object-oriented_ architecture for flexible and maintainable features.
4//!
5//! In Xvc-ECS, each entity is a plain integer.
6//! Components are arbitrary structs implementing [Storable], and [stores][XvcStore] are systems
7//! that we use to represent associations between entity and components, and between entities.
8#![warn(missing_docs)]
9#![forbid(unsafe_code)]
10pub mod event;
11pub mod hstore;
12pub mod r11store;
13pub mod r1nstore;
14pub mod rmnstore;
15pub mod storable;
16pub mod vstore;
17pub mod xvcstore;
18
19use rand::{rngs, RngCore, SeedableRng};
20use std::fmt;
21use std::fs;
22use std::path::Path;
23use std::path::PathBuf;
24use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
25use std::sync::Once;
26use std::time::SystemTime;
27use std::time::UNIX_EPOCH;
28
29use serde::{Deserialize, Serialize};
30
31use crate::error::{Error as XvcError, Result};
32
33/// Describes an entity in Entity Component System-sense.
34///
35/// It doesn't have any semantics except being unique for a given entity.
36/// Various types of information (components) can be attached to this entity.
37/// XvcStore uses the entity as a key for the components.
38///
39/// It's possible to convert to `(u64, u64)` or `u128` back and forth.
40/// Normally, you should use [XvcEntityGenerator] to create entities.
41/// It randomizes the first value to be unique and saves the last number across sessions.
42/// This changed in 0.5. See https://github.com/iesahin/xvc/issues/198
43#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
44pub struct XvcEntity(u64, u64);
45
46impl fmt::Display for XvcEntity {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "({}, {})", self.0, self.1)
49    }
50}
51
52impl From<(u64, u64)> for XvcEntity {
53    fn from(e: (u64, u64)) -> Self {
54        Self(e.0, e.1)
55    }
56}
57
58impl From<u128> for XvcEntity {
59    fn from(e: u128) -> Self {
60        Self((e >> 64) as u64, e as u64)
61    }
62}
63
64impl From<XvcEntity> for u128 {
65    fn from(e: XvcEntity) -> u128 {
66        ((e.0 as u128) << 64) | (e.1 as u128)
67    }
68}
69
70impl From<XvcEntity> for (u64, u64) {
71    fn from(e: XvcEntity) -> (u64, u64) {
72        (e.0, e.1)
73    }
74}
75
76/// Keeps track of the latest [XvcEntity] values, and creates new entities.
77///
78/// It's thread safe as it uses [AtomicUsize] for the highest entity value.
79/// It can save itself to a file, loads from file.
80/// Only one instance of this created for each app.
81/// It runs load or init functions via [Once] and doesn't allow to load a second instance.
82
83#[derive(Debug)]
84pub struct XvcEntityGenerator {
85    /// The counter is used to generate the first portion of new entities. It's incremented at every [`next_element`] call.
86    counter: AtomicU64,
87    /// The random value is used to generate the second portion of new entities. It's generated once at the
88    /// initialization of this struct and never changed.
89    random: u64,
90    /// The counter is saved only if its value is changed.
91    dirty: AtomicBool,
92}
93
94static INIT: Once = Once::new();
95
96/// Loads the generator from a directory.
97///
98/// It loads all files from that directory, and selects the highest one.
99/// This function can only be used once in a process.
100/// You cannot load a second instance of the entity generator, as it will defeat its thread-safe
101/// uniqueness purpose.
102pub fn load_generator(dir: &Path) -> Result<XvcEntityGenerator> {
103    let mut gen: Result<XvcEntityGenerator> = Err(XvcError::CanInitializeOnlyOnce {
104        object: "XvcEntityGenerator".to_string(),
105    });
106    INIT.call_once(|| gen = XvcEntityGenerator::load(dir));
107    gen
108}
109
110/// Inits a generator for the first time.
111///
112/// Normally this only be used once an Xvc repository initializes.
113/// The starting value for entities is 1.
114pub fn init_generator() -> Result<XvcEntityGenerator> {
115    let mut gen: Result<XvcEntityGenerator> = Err(XvcError::CanInitializeOnlyOnce {
116        object: "XvcEntityGenerator".to_string(),
117    });
118
119    INIT.call_once(|| gen = Ok(XvcEntityGenerator::new(1)));
120    gen
121}
122
123impl Iterator for XvcEntityGenerator {
124    type Item = XvcEntity;
125
126    fn next(&mut self) -> Option<Self::Item> {
127        Some(self.next_element())
128    }
129}
130
131impl XvcEntityGenerator {
132    fn new(start: u64) -> XvcEntityGenerator {
133        let counter = AtomicU64::new(start);
134        let mut rng = rngs::StdRng::from_entropy();
135        let init_random = rng.next_u64();
136        // When we create a new generator from scratch, we need to save it.
137        // In the load function, we set this to false, as we don't want to save duplicate values.
138        let dirty = AtomicBool::new(true);
139        Self {
140            dirty,
141            counter,
142            random: init_random,
143        }
144    }
145
146    /// Returns the next element by atomically incresing the current value.
147    pub fn next_element(&self) -> XvcEntity {
148        self.dirty.store(true, Ordering::SeqCst);
149        XvcEntity(self.counter.fetch_add(1, Ordering::SeqCst), self.random)
150    }
151
152    fn load(dir: &Path) -> Result<XvcEntityGenerator> {
153        let path = most_recent_file(dir)?;
154        match path {
155            Some(path) => {
156                let current_val = fs::read_to_string(path)?.parse::<u64>()?;
157                // We don't use new here to set the dirty flag to false.
158                let counter = AtomicU64::new(current_val);
159                let mut rng = rngs::StdRng::from_entropy();
160                let init_random = rng.next_u64();
161                // When we load a new generator from file, we don't need to save it.
162                // In the new function, we set this to true, as we need to save the first value.
163                let dirty = AtomicBool::new(false);
164                Ok(Self {
165                    dirty,
166                    counter,
167                    random: init_random,
168                })
169            }
170            None => Err(XvcError::CannotRestoreEntityCounter {
171                path: dir.as_os_str().to_owned(),
172            }),
173        }
174    }
175
176    /// Saves the current XvcEntity counter to path.
177    /// It saves only the first (e.0) part of the entity. The second part is
178    /// generated randomly to randomize entities in different invocations of the app.
179    pub fn save(&self, dir: &Path) -> Result<()> {
180        if self.dirty.load(Ordering::SeqCst) {
181            if !dir.exists() {
182                fs::create_dir_all(dir)?;
183            }
184            let path = dir.join(timestamp());
185            fs::write(path, format!("{}", self.counter.load(Ordering::SeqCst)))?;
186            // We don't need to save again until changed.
187            self.dirty.store(false, Ordering::SeqCst);
188        }
189        Ok(())
190    }
191}
192
193/// Returns a timestamp string to be used in file names.
194/// This is used to generate sortable unique file names in event logs.
195pub fn timestamp() -> String {
196    let now = SystemTime::now();
197    let since = now
198        .duration_since(UNIX_EPOCH)
199        .expect("Time went backwards!");
200    format!("{}", since.as_micros())
201}
202
203/// Returns all files in a directory sorted by name.
204/// This is used to sort timestamp named files. (See [timestamp]).
205/// Store files are loaded in this order to replay the changes across branches.
206/// TODO: Add link to book chapter.
207pub fn sorted_files(dir: &Path) -> Result<Vec<PathBuf>> {
208    if dir.exists() {
209        let mut files: Vec<PathBuf> = fs::read_dir(dir)?
210            .filter_map(|e| match e {
211                Ok(e) => Some(e.path()),
212                Err(_) => None,
213            })
214            .collect();
215
216        files.sort_unstable();
217        Ok(files)
218    } else {
219        fs::create_dir_all(dir)?;
220        Ok(vec![])
221    }
222}
223
224/// This one returns the most recent timestamp named file.
225/// It gets files with [sorted_files] and returns the last one if there is one.
226/// If there are no files in a directory, this returns `Ok(None)`.
227pub fn most_recent_file(dir: &Path) -> Result<Option<PathBuf>> {
228    if !dir.exists() {
229        return Ok(None);
230    }
231
232    let files = sorted_files(dir)?;
233
234    if files.is_empty() {
235        Ok(None)
236    } else {
237        Ok(files.last().cloned())
238    }
239}
240
241#[macro_export]
242/// Specifies the store name a type is stored.
243/// Usually it's the string representation of a type.
244/// These strings are used to generate store locations for types.
245/// They must be unique across the project.
246///
247/// ## Example
248/// ```rust,ignore
249/// persist!(MyType, "my-type");
250/// ```
251///
252/// makes `MyType` to implement [xvc_ecs::PersistentComponent].
253/// This trait has a `type_description` function that returns the specified string.
254/// The `type_description` string is then used to generate store identifiers like
255/// `my-type-bstore.json` or `my-type-another-type-r11store.json`
256macro_rules! persist {
257    ( $t:ty, $desc:literal ) => {
258        impl $crate::Storable for $t {
259            fn type_description() -> String {
260                $desc.to_string()
261            }
262        }
263    };
264}
265
266#[cfg(test)]
267mod tests {
268
269    use std::{thread::sleep, time::Duration};
270
271    use super::*;
272    use log::LevelFilter;
273    use rand;
274    use tempdir::TempDir;
275    use xvc_logging::setup_logging;
276
277    #[test]
278    fn test_init() -> Result<()> {
279        let gen = init_generator()?;
280        assert_eq!(gen.counter.load(Ordering::SeqCst), 1);
281        assert_eq!(gen.next_element().0, 1);
282        assert_eq!(gen.next_element().0, 2);
283        let gen2 = init_generator();
284        assert!(matches!(gen2, Err(XvcError::CanInitializeOnlyOnce { .. })));
285        Ok(())
286    }
287
288    #[test]
289    fn test_load() -> Result<()> {
290        setup_logging(Some(LevelFilter::Trace), None);
291        let tempdir = TempDir::new("test-xvc-ecs")?;
292        let gen_dir = tempdir.path().join("entity-gen");
293        fs::create_dir_all(&gen_dir)?;
294        let r: u64 = rand::random();
295        let gen_file_1 = gen_dir.join(timestamp());
296        fs::write(gen_file_1, format!("{}", r))?;
297        sleep(Duration::from_millis(1));
298        let gen_file_2 = gen_dir.join(timestamp());
299        fs::write(gen_file_2, format!("{}", r + 1000))?;
300        sleep(Duration::from_millis(1));
301        let gen_file_3 = gen_dir.join(timestamp());
302        fs::write(gen_file_3, format!("{}", r + 2000))?;
303        let gen = XvcEntityGenerator::load(&gen_dir)?;
304        assert_eq!(gen.counter.load(Ordering::SeqCst), r + 2000);
305        assert_eq!(gen.next_element().0, (r + 2000));
306        assert_eq!(gen.next_element().0, (r + 2001));
307        assert_eq!(gen.next_element().0, (r + 2002));
308        gen.save(&gen_dir)?;
309        let new_val = fs::read_to_string(most_recent_file(&gen_dir)?.unwrap())?.parse::<u64>()?;
310        assert_eq!(new_val, r + 2003);
311        Ok(())
312    }
313
314    /// Multiple saves without changed counter should not create new files.
315    /// See: https://github.com/iesahin/xvc/issues/185
316    #[test]
317    fn test_multi_save() -> Result<()> {
318        setup_logging(Some(LevelFilter::Trace), None);
319        let tempdir = TempDir::new("test-xvc-ecs")?;
320        let gen_dir = tempdir.path().join("entity-gen");
321        fs::create_dir_all(&gen_dir)?;
322        // We use new here to circumvent the singleton check.
323        let gen = XvcEntityGenerator::new(10);
324        gen.save(&gen_dir)?;
325        // It must save the counter at first
326        assert!(sorted_files(&gen_dir)?.len() == 1);
327        // It must not save the counter if it's not changed
328        gen.save(&gen_dir)?;
329        assert!(sorted_files(&gen_dir)?.len() == 1);
330        // It must save the counter if it's changed
331        let _e = gen.next_element();
332        gen.save(&gen_dir)?;
333        assert!(sorted_files(&gen_dir)?.len() == 2);
334
335        let gen2 = XvcEntityGenerator::load(&gen_dir)?;
336        // Don't save if it's not changed after load
337        gen.save(&gen_dir)?;
338        assert!(sorted_files(&gen_dir)?.len() == 2);
339        // Save if it's changed after load
340        let _e = gen2.next_element();
341        gen2.save(&gen_dir)?;
342        assert!(sorted_files(&gen_dir)?.len() == 3);
343        // Don't save if it's not changed after save
344        gen2.save(&gen_dir)?;
345        gen2.save(&gen_dir)?;
346        gen2.save(&gen_dir)?;
347        gen2.save(&gen_dir)?;
348        assert!(sorted_files(&gen_dir)?.len() == 3);
349
350        Ok(())
351    }
352
353    #[test]
354    fn test_from_to() -> Result<()> {
355        let e1 = XvcEntity(1, 2);
356        let u1: u128 = e1.into();
357        let e2 = XvcEntity::from(u1);
358        assert_eq!(e1, e2);
359        Ok(())
360    }
361}