#![allow(clippy::new_without_default)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::storage::{ComponentPool, ComponentStorage, PageMap};
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>>,
default_capacities: HashMap<String, usize>,
db: persy::Persy,
}
impl QMI {
pub fn with_capacity<P: AsRef<Path>>(path: P, capacity: usize) -> Self {
let mut qmi = Self::create_or_open(path);
qmi.default_capacities
.insert("default".to_string(), capacity);
qmi
}
pub fn reserve_component<T: 'static>(&mut self, capacity: usize) {
let type_name = std::any::type_name::<T>();
self.default_capacities
.insert(type_name.to_string(), capacity);
let storage_res = self.component_storage.get_mut(type_name);
if let Some(storage) = storage_res
&& let Some(pool) = storage.as_any_mut().downcast_mut::<ComponentPool<T>>()
{
pool.dense_data.reserve(capacity);
pool.dense_entities.reserve(capacity);
}
}
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(),
default_capacities: 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: 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 = 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(())
}
}