nightshade 0.13.1

A cross-platform data-oriented game engine.
Documentation
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;

use serde::{Deserialize, Serialize};

use super::asset_uuid::AssetUuid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AssetType {
    Mesh,
    Texture,
    Material,
    Prefab,
    Audio,
    Script,
    Animation,
    Shader,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetEntry {
    pub uuid: AssetUuid,
    pub path: String,
    pub asset_type: AssetType,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub metadata: HashMap<String, String>,
}

impl AssetEntry {
    pub fn new(uuid: AssetUuid, path: impl Into<String>, asset_type: AssetType) -> Self {
        Self {
            uuid,
            path: path.into(),
            asset_type,
            name: None,
            metadata: HashMap::new(),
        }
    }

    pub fn with_name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AssetManifest {
    pub version: u32,
    pub assets: Vec<AssetEntry>,
    #[serde(skip)]
    uuid_to_index: HashMap<AssetUuid, usize>,
    #[serde(skip)]
    path_to_uuid: HashMap<String, AssetUuid>,
    #[serde(skip)]
    name_to_uuid: HashMap<String, AssetUuid>,
}

impl AssetManifest {
    pub const CURRENT_VERSION: u32 = 1;

    pub fn new() -> Self {
        Self {
            version: Self::CURRENT_VERSION,
            assets: Vec::new(),
            uuid_to_index: HashMap::new(),
            path_to_uuid: HashMap::new(),
            name_to_uuid: HashMap::new(),
        }
    }

    pub fn add_asset(&mut self, entry: AssetEntry) {
        let index = self.assets.len();
        let uuid = entry.uuid;
        let path = entry.path.clone();
        let name = entry.name.clone();

        self.uuid_to_index.insert(uuid, index);
        self.path_to_uuid.insert(path, uuid);
        if let Some(name) = name {
            self.name_to_uuid.insert(name, uuid);
        }
        self.assets.push(entry);
    }

    pub fn get_by_uuid(&self, uuid: AssetUuid) -> Option<&AssetEntry> {
        self.uuid_to_index
            .get(&uuid)
            .and_then(|index| self.assets.get(*index))
    }

    pub fn get_by_path(&self, path: &str) -> Option<&AssetEntry> {
        self.path_to_uuid
            .get(path)
            .and_then(|uuid| self.get_by_uuid(*uuid))
    }

    pub fn get_by_name(&self, name: &str) -> Option<&AssetEntry> {
        self.name_to_uuid
            .get(name)
            .and_then(|uuid| self.get_by_uuid(*uuid))
    }

    pub fn resolve_uuid(&self, uuid: AssetUuid) -> Option<&str> {
        self.get_by_uuid(uuid).map(|entry| entry.path.as_str())
    }

    pub fn resolve_path(&self, path: &str) -> Option<AssetUuid> {
        self.path_to_uuid.get(path).copied()
    }

    pub fn resolve_name(&self, name: &str) -> Option<AssetUuid> {
        self.name_to_uuid.get(name).copied()
    }

    pub fn assets_of_type(&self, asset_type: AssetType) -> impl Iterator<Item = &AssetEntry> {
        self.assets
            .iter()
            .filter(move |entry| entry.asset_type == asset_type)
    }

    pub fn rebuild_indices(&mut self) {
        self.uuid_to_index.clear();
        self.path_to_uuid.clear();
        self.name_to_uuid.clear();

        for (index, entry) in self.assets.iter().enumerate() {
            self.uuid_to_index.insert(entry.uuid, index);
            self.path_to_uuid.insert(entry.path.clone(), entry.uuid);
            if let Some(name) = &entry.name {
                self.name_to_uuid.insert(name.clone(), entry.uuid);
            }
        }
    }

    pub fn is_empty(&self) -> bool {
        self.assets.is_empty()
    }

    pub fn len(&self) -> usize {
        self.assets.len()
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ManifestError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Binary error: {0}")]
    Binary(#[from] bincode::Error),
}

pub fn save_manifest_json(manifest: &AssetManifest, path: &Path) -> Result<(), ManifestError> {
    let file = File::create(path)?;
    let writer = BufWriter::new(file);
    serde_json::to_writer_pretty(writer, manifest)?;
    Ok(())
}

pub fn load_manifest_json(path: &Path) -> Result<AssetManifest, ManifestError> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut manifest: AssetManifest = serde_json::from_reader(reader)?;
    manifest.rebuild_indices();
    Ok(manifest)
}

pub fn save_manifest_binary(manifest: &AssetManifest, path: &Path) -> Result<(), ManifestError> {
    let file = File::create(path)?;
    let writer = BufWriter::new(file);
    bincode::serialize_into(writer, manifest)?;
    Ok(())
}

pub fn load_manifest_binary(path: &Path) -> Result<AssetManifest, ManifestError> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut manifest: AssetManifest = bincode::deserialize_from(reader)?;
    manifest.rebuild_indices();
    Ok(manifest)
}

pub fn save_manifest(manifest: &AssetManifest, path: &Path) -> Result<(), ManifestError> {
    match path.extension().and_then(|e| e.to_str()) {
        Some("bin") => save_manifest_binary(manifest, path),
        _ => save_manifest_json(manifest, path),
    }
}

pub fn load_manifest(path: &Path) -> Result<AssetManifest, ManifestError> {
    match path.extension().and_then(|e| e.to_str()) {
        Some("bin") => load_manifest_binary(path),
        _ => load_manifest_json(path),
    }
}