use bevy::{
app::{App, Plugin},
asset::{AssetApp, AssetLoader, LoadContext, io::Reader},
};
use serde::Deserialize;
use thiserror::Error;
use crate::model::{EffectGraphAsset, FORMAT_VERSION};
pub const MAGIC_HEADER: &str =
"// Hanabi effect graph - https://github.com/djeedai/hanabi-workshop";
pub struct EffectGraphPlugin;
impl Plugin for EffectGraphPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<EffectGraphAsset>()
.init_asset_loader::<EffectGraphLoader>();
}
}
pub fn from_ron_bytes(bytes: &[u8]) -> Result<EffectGraphAsset, EffectGraphLoaderError> {
let AssetVersion { version } = ron::de::from_bytes(bytes)?;
if version > FORMAT_VERSION {
return Err(EffectGraphLoaderError::UnsupportedVersion {
found: version,
supported: FORMAT_VERSION,
});
}
if version == FORMAT_VERSION {
return Ok(ron::de::from_bytes(bytes)?);
}
migrate_from(bytes, version)
}
pub fn to_ron_string(asset: &EffectGraphAsset) -> Result<String, ron::Error> {
let body = ron::ser::to_string_pretty(asset, ron::ser::PrettyConfig::default())?;
Ok(format!("{MAGIC_HEADER}\n{body}"))
}
#[derive(Deserialize)]
struct AssetVersion {
version: u32,
}
fn migrate_from(bytes: &[u8], version: u32) -> Result<EffectGraphAsset, EffectGraphLoaderError> {
let value: ron::Value = ron::de::from_bytes(bytes)?;
let value = apply_migrations(value, version);
value
.into_rust()
.map_err(|source| EffectGraphLoaderError::Migration {
from: version,
source,
})
}
fn apply_migrations(value: ron::Value, from: u32) -> ron::Value {
let _ = from;
value
}
#[derive(Default, bevy::reflect::TypePath)]
pub struct EffectGraphLoader;
#[derive(Debug, Error)]
pub enum EffectGraphLoaderError {
#[error("failed to read asset bytes: {0}")]
Io(#[from] std::io::Error),
#[error("failed to deserialize EffectGraphAsset RON: {0}")]
Ron(#[from] ron::error::SpannedError),
#[error("unsupported EffectGraphAsset version {found}; this build supports up to {supported}")]
UnsupportedVersion { found: u32, supported: u32 },
#[error("failed to migrate EffectGraphAsset from version {from}: {source}")]
Migration { from: u32, source: ron::Error },
}
impl AssetLoader for EffectGraphLoader {
type Asset = EffectGraphAsset;
type Settings = ();
type Error = EffectGraphLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
from_ron_bytes(&bytes)
}
fn extensions(&self) -> &[&str] {
&["hnb"]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{EffectGraph, EffectGraphAsset, GraphLayout};
fn sample_asset() -> EffectGraphAsset {
EffectGraphAsset {
version: FORMAT_VERSION,
graph: EffectGraph::empty(),
layout: Some(GraphLayout {
pan: (1.0, -2.0),
zoom: 1.5,
node_pos: Vec::new(),
stack_pos: Vec::new(),
}),
}
}
#[test]
fn ron_round_trips_through_helpers() {
let asset = sample_asset();
let text = to_ron_string(&asset).expect("serialize");
let back = from_ron_bytes(text.as_bytes()).expect("deserialize");
assert_eq!(asset, back);
}
#[test]
fn writes_magic_header() {
let text = to_ron_string(&sample_asset()).expect("serialize");
assert!(text.starts_with(MAGIC_HEADER));
}
#[test]
fn loads_legacy_file_without_header() {
let asset = sample_asset();
let body = ron::ser::to_string_pretty(&asset, ron::ser::PrettyConfig::default())
.expect("serialize");
assert!(!body.starts_with(MAGIC_HEADER));
let back = from_ron_bytes(body.as_bytes()).expect("deserialize");
assert_eq!(asset, back);
}
#[test]
fn rejects_future_version() {
let mut asset = sample_asset();
asset.version = FORMAT_VERSION + 1;
let text = to_ron_string(&asset).expect("serialize");
assert!(matches!(
from_ron_bytes(text.as_bytes()),
Err(EffectGraphLoaderError::UnsupportedVersion { .. })
));
}
}