qmi 0.1.5

An ECS with too much macro usage
Documentation
#![allow(clippy::new_without_default)]

use anyhow::Result;
use persy::PersyId;
use serde::{Deserialize, Serialize};

use crate::pool::{ComponentPool, ComponentStorage, PageMap};
use std::{collections::HashMap, fs::remove_file, path::Path};

pub mod persistence;
pub mod pool;
pub mod query;

#[derive(Clone, Serialize, Deserialize, Debug)]
struct Entity;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EntityID(pub u32);

pub struct QMI {
    next_entity_id: EntityID,
    pub component_storage: HashMap<String, Box<dyn ComponentStorage>>,
    db: persy::Persy,
}

impl QMI {
    pub fn open<P: AsRef<Path>>(path: P, new: bool) -> Result<Self> {
        let path_ref = path.as_ref();
        if new {
            let _ = remove_file(path_ref);
        }
        let path_exists = path_ref.exists();
        let db = {
            if !path_exists {
                persy::Persy::create(path_ref)?;
            }
            persy::Persy::open(path_ref, persy::Config::default())?
        };

        let mut db_tx = db.begin()?;
        if !db.exists_index("qmi_index")? {
            db_tx.create_index::<String, PersyId>("qmi_index", persy::ValueMode::Replace)?;
        }
        if !db.exists_segment("qmi_data")? {
            db_tx.create_segment("qmi_data")?;
        }
        let next_entity_id_key = "next_entity_id".to_string();
        let next_entity_pid = db_tx
            .get::<String, persy::PersyId>("qmi_index", &next_entity_id_key)?
            .next();
        let mut next_entity_id = EntityID(0);
        if let Some(pid) = next_entity_pid {
            let bytes = db_tx.read("qmi_data", &pid)?.unwrap();
            next_entity_id = EntityID(u32::from_le_bytes(bytes.as_slice().try_into()?));
        }
        db_tx.commit()?;
        Ok(Self {
            db,
            next_entity_id,
            component_storage: HashMap::new(),
        })
    }

    pub fn spawn(&mut self) -> EntityID {
        let id = self.next_entity_id;
        self.next_entity_id.0 += 1;
        self.insert(id, Entity);
        id
    }

    pub fn insert<T>(&mut self, entity: EntityID, component: T)
    where
        T: 'static + Serialize + for<'de> Deserialize<'de>,
    {
        let type_name = std::any::type_name::<T>().rsplit("::").next().unwrap();
        if !self.component_storage.contains_key(type_name) {
            self.component_storage.insert(
                type_name.to_string(),
                Box::new(ComponentPool::<T> {
                    dense_data: Vec::new(),
                    dense_entities: Vec::new(),
                    sparse_map: PageMap::new(),
                }),
            );
        }
        let pool = self
            .component_storage
            .get_mut(type_name)
            .unwrap()
            .as_any_mut()
            .downcast_mut::<ComponentPool<T>>()
            .unwrap();

        if let Some(id) = pool.sparse_map.get(entity.0 as usize) {
            pool.dense_data[id] = component;
            return;
        }

        let id = pool.dense_data.len();
        pool.sparse_map.insert(entity.0 as usize, id);
        pool.dense_data.push(component);
        pool.dense_entities.push(entity);
    }

    pub fn save(&mut self) -> Result<()> {
        let mut db_tx = self.db.begin()?;
        let next_entity_id_bytes = self.next_entity_id.0.to_le_bytes();
        let next_entity_id_key = "next_entity_id".to_string();
        if let Some(pid) = db_tx
            .get::<String, persy::PersyId>("qmi_index", &next_entity_id_key)?
            .next()
        {
            db_tx.update("qmi_data", &pid, &next_entity_id_bytes)?;
        } else {
            let persy_id = db_tx.insert("qmi_data", &next_entity_id_bytes)?;
            db_tx.put("qmi_index", next_entity_id_key, persy_id)?;
        }

        for (type_name, storage) in &self.component_storage {
            let pool_bytes = storage.serialize()?;
            let index_key = type_name.rsplit("::").next().unwrap().to_string();
            if let Some(persy_id) = db_tx
                .get::<String, persy::PersyId>("qmi_index", &index_key)?
                .next()
            {
                db_tx.update("qmi_data", &persy_id, &pool_bytes)?;
            } else {
                let persy_id = db_tx.insert("qmi_data", &pool_bytes)?;
                db_tx.put("qmi_index", index_key, persy_id)?;
            }
        }
        db_tx.prepare()?.commit()?;
        Ok(())
    }

    pub fn load<T>(&mut self) -> Result<(), Box<dyn std::error::Error>>
    where
        T: 'static + serde::Serialize + for<'de> serde::Deserialize<'de>,
    {
        let mut db_tx = self.db.begin()?;
        let type_name = std::any::type_name::<T>().to_string();
        let index_key = type_name.rsplit("::").next().unwrap().to_string();

        if let Some(persy_id) = db_tx
            .get::<String, persy::PersyId>("qmi_index", &index_key)?
            .next()
        {
            let bytes = db_tx
                .read("qmi_data", &persy_id)?
                .expect("Index pointed to a missing component data record.");

            let reconstructed_pool = ComponentPool::<T>::deserialize(&bytes)
                .unwrap_or_else(|e| panic!("Failed to deserialize pool {}: {}", type_name, e));

            self.component_storage
                .insert(index_key, Box::new(reconstructed_pool));
        } else {
            let fresh_pool = ComponentPool::<T> {
                dense_data: Vec::new(),
                dense_entities: Vec::new(),
                sparse_map: PageMap::new(),
            };
            self.component_storage
                .insert(index_key, Box::new(fresh_pool));
        }

        db_tx.prepare()?.commit()?;
        Ok(())
    }
}