nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use crate::prelude::egui_tiles;
use serde::{Deserialize, Serialize, de::DeserializeOwned};

use super::error::MosaicError;
use super::widget::Pane;

pub const CURRENT_VERSION: &str = "1.0";

fn default_none<T>() -> Option<T> {
    None
}

fn validate_version(version: &str) {
    if version.is_empty() {
        tracing::warn!("Project file has empty version field, assuming current version");
    } else if version != CURRENT_VERSION {
        tracing::warn!(
            "Project file version '{}' differs from current version '{}', loading anyway",
            version,
            CURRENT_VERSION
        );
    }
}

#[derive(Serialize, Deserialize)]
pub struct ProjectSaveFile<W, D = ()> {
    pub version: String,
    pub name: String,
    pub windows: Vec<WindowLayout<W>>,
    #[serde(default = "default_none", skip_serializing_if = "Option::is_none")]
    pub data: Option<D>,
}

impl<W, D> ProjectSaveFile<W, D> {
    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
        Self {
            version: version.into(),
            name: name.into(),
            windows: Vec::new(),
            data: None,
        }
    }

    pub fn with_data(mut self, data: D) -> Self {
        self.data = Some(data);
        self
    }
}

impl<W, D> Default for ProjectSaveFile<W, D> {
    fn default() -> Self {
        Self {
            version: CURRENT_VERSION.to_string(),
            name: String::new(),
            windows: Vec::new(),
            data: None,
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl<W, D> ProjectSaveFile<W, D>
where
    W: Serialize + DeserializeOwned,
    D: Serialize + DeserializeOwned,
{
    pub fn save_to_path(&self, path: &std::path::Path) -> Result<(), MosaicError> {
        let path_str = path.to_string_lossy().to_lowercase();
        if path_str.ends_with(".bin") {
            let bytes = self.to_binary_bytes()?;
            std::fs::write(path, bytes)?;
        } else {
            let bytes = self.to_json_bytes()?;
            std::fs::write(path, bytes)?;
        }
        Ok(())
    }

    pub fn load_from_path(path: &std::path::Path) -> Result<Self, MosaicError> {
        let bytes = std::fs::read(path)?;
        let path_str = path.to_string_lossy().to_lowercase();
        let result: Self = if path_str.ends_with(".bin") {
            bincode::deserialize(&bytes)
                .map_err(|error| MosaicError::Deserialize(error.to_string()))?
        } else {
            serde_json::from_slice(&bytes)
                .map_err(|error| MosaicError::Deserialize(error.to_string()))?
        };
        validate_version(&result.version);
        Ok(result)
    }
}

impl<W, D> ProjectSaveFile<W, D>
where
    W: Serialize + DeserializeOwned,
    D: Serialize + DeserializeOwned,
{
    pub fn to_json_bytes(&self) -> Result<Vec<u8>, MosaicError> {
        serde_json::to_vec_pretty(self).map_err(|error| MosaicError::Serialize(error.to_string()))
    }

    pub fn to_binary_bytes(&self) -> Result<Vec<u8>, MosaicError> {
        bincode::serialize(self).map_err(|error| MosaicError::Serialize(error.to_string()))
    }

    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MosaicError> {
        let result: Self = serde_json::from_slice(bytes).or_else(|_| {
            bincode::deserialize(bytes).map_err(|error| MosaicError::Deserialize(error.to_string()))
        })?;
        validate_version(&result.version);
        Ok(result)
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct WindowLayout<W> {
    pub tree: egui_tiles::Tree<Pane<W>>,
    pub layout_name: String,
}

impl<W> WindowLayout<W> {
    pub fn new(tree: egui_tiles::Tree<Pane<W>>, layout_name: impl Into<String>) -> Self {
        Self {
            tree,
            layout_name: layout_name.into(),
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct LayoutSaveFile<W> {
    pub version: String,
    pub tree: egui_tiles::Tree<Pane<W>>,
    pub layout_name: String,
}

impl<W> LayoutSaveFile<W> {
    pub fn new(
        tree: egui_tiles::Tree<Pane<W>>,
        layout_name: impl Into<String>,
        version: impl Into<String>,
    ) -> Self {
        Self {
            version: version.into(),
            tree,
            layout_name: layout_name.into(),
        }
    }
}