#![allow(clippy::new_without_default)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::storage::{ComponentPool, ComponentStorage};
use std::{collections::HashMap, path::Path};
mod query;
mod storage;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EntityID(pub u32);
pub struct QMI {
next_entity_id: EntityID,
component_storage: HashMap<String, Box<dyn ComponentStorage>>,
db: persy::Persy,
}
impl QMI {
pub fn create_or_open<P: AsRef<Path>>(path: P) -> Self {
let path_ref = path.as_ref();
let db_result = {
if !path_ref.exists() {
persy::Persy::create(path_ref).expect("Could not create QMI db file at path.");
}
persy::Persy::open(path_ref, persy::Config::default())
};
let db = match db_result {
Ok(opened_db) => opened_db,
Err(_) => {
eprintln!("Database file structure is unreadable. Recreating...");
let _ = std::fs::remove_file(path_ref);
persy::Persy::create(path_ref).expect("Could not recreate QMI db file at path.");
persy::Persy::open(path_ref, persy::Config::default())
.expect("Could not open QMI db file at path.")
}
};
let qmi = Self {
component_storage: HashMap::new(),
next_entity_id: EntityID(0),
db,
};
if let Err(e) = qmi.ensure_topology() {
panic!("Critical database topology initialization failure: {:?}", e);
}
qmi
}
fn ensure_topology(&self) -> anyhow::Result<()> {
let mut tx = self.db.begin()?;
let result = (|| -> anyhow::Result<bool> {
let mut changes = false;
if !tx.exists_segment("qmi_data")? {
tx.create_segment("qmi_data")?;
changes = true;
}
if !tx.exists_index("qmi_index")? {
tx.create_index::<String, persy::PersyId>("qmi_index", persy::ValueMode::Replace)?;
changes = true;
}
Ok(changes)
})();
match result {
Ok(changes) => {
if changes {
tx.prepare()?.commit()?;
}
}
Err(e) => {
tx.rollback()?;
return Err(e);
}
}
Ok(())
}
pub fn spawn(&mut self) -> EntityID {
let id = self.next_entity_id;
self.next_entity_id.0 += 1;
id
}
pub fn insert<T>(&mut self, entity: EntityID, component: T)
where
T: 'static + Serialize,
{
let type_name = std::any::type_name::<T>();
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: HashMap::new(),
}),
);
}
let pool = self
.component_storage
.get_mut(type_name)
.unwrap()
.as_any_mut()
.downcast_mut::<ComponentPool<T>>()
.unwrap();
if let Some(&idx) = pool.sparse_map.get(&entity) {
pool.dense_data[idx] = component;
return;
}
let idx = pool.dense_data.len();
pool.sparse_map.insert(entity, idx);
pool.dense_data.push(component);
pool.dense_entities.push(entity);
}
fn query_single<T: 'static>(&self) -> impl Iterator<Item = (EntityID, &T)> {
self.component_storage
.get(std::any::type_name::<T>())
.expect("Query failed. ComponentPool does not exist")
.as_any()
.downcast_ref::<ComponentPool<T>>()
.unwrap()
.iter_entities()
}
fn query_mut_single<T: 'static>(&mut self) -> impl Iterator<Item = (EntityID, &mut T)> {
self.component_storage
.get_mut(std::any::type_name::<T>())
.expect("Mutable query failed. ComponentPool does not exist")
.as_any_mut()
.downcast_mut::<ComponentPool<T>>()
.unwrap()
.iter_entities_mut()
}
pub fn save(&mut self) -> Result<()> {
let mut db_tx = self.db.begin()?;
let next_entity_id_bytes = postcard::to_allocvec(&self.next_entity_id)?;
let next_entity_id_key = "next_entity_id".to_string();
if let Some(persy_id) = db_tx
.get::<String, persy::PersyId>("qmi_index", &next_entity_id_key)?
.next()
{
db_tx.update("qmi_data", &persy_id, &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 storage_pool in self.component_storage.values() {
let pool_bytes = storage_pool.serialize()?;
let type_key = storage_pool.type_name().to_string();
if let Some(persy_id) = db_tx.get("qmi_index", &type_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", type_key, persy_id)?;
}
}
db_tx.prepare()?.commit()?;
Ok(())
}
}