diff --git a/Cargo.toml b/Cargo.toml index 02db4e0..a09888a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ json = ["dep:serde_json"] msgpack = ["dep:rmp-serde"] xml = ["dep:quick-xml"] csv = ["dep:csv"] +postcard = ["dep:postcard"] [dependencies] bevy = { version = "0.13", default-features = false, features = ["bevy_asset"] } @@ -33,6 +34,7 @@ thiserror = "1.0" quick-xml = { version = "0.31", features = [ "serialize" ], optional = true } serde = { version = "1" } anyhow = { version = "1" } +postcard = {version = "1.0", features = ["use-std"], optional = true} [dev-dependencies] bevy = { version = "0.13" } @@ -47,6 +49,11 @@ name = "msgpack" path = "examples/msgpack.rs" required-features = ["msgpack"] +[[example]] +name = "postcard" +path = "examples/postcard.rs" +required-features = ["postcard"] + [[example]] name = "ron" path = "examples/ron.rs" diff --git a/README.md b/README.md index 1118b87..6d1a509 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Supported formats: |:-----------|:-----------|:---------------------------------------| | `json` | `json` | [`json.rs`](./examples/json.rs) | | `msgpack` | `msgpack` | [`msgpack.rs`](./examples/msgpack.rs) | +| `postcard`| `postcard` | [`postcard.rs`](./examples/postcard.rs)| | `ron` | `ron` | [`ron.rs`](./examples/ron.rs) | | `toml` | `toml` | [`toml.rs`](./examples/toml.rs) | | `xml` | `xml` | [`xml.rs`](./examples/xml.rs) | @@ -37,6 +38,7 @@ as a generic parameter. You also need to configure custom file endings for each use bevy::prelude::*; use bevy_common_assets::json::JsonAssetPlugin; use bevy_common_assets::msgpack::MsgPackAssetPlugin; +use bevy_common_assets::postcard::PostcardAssetPlugin; use bevy_common_assets::ron::RonAssetPlugin; use bevy_common_assets::toml::TomlAssetPlugin; use bevy_common_assets::xml::XmlAssetPlugin; @@ -49,6 +51,7 @@ fn main() { JsonAssetPlugin::::new(&["level.json", "custom.json"]), RonAssetPlugin::::new(&["level.ron"]), MsgPackAssetPlugin::::new(&["level.msgpack"]), + PostcardAssetPlugin::::new(&["level.postcard"]), TomlAssetPlugin::::new(&["level.toml"]), XmlAssetPlugin::::new(&["level.xml"]), YamlAssetPlugin::::new(&["level.yaml"]) diff --git a/assets/trees.level.postcard b/assets/trees.level.postcard new file mode 100644 index 0000000..3700c1d Binary files /dev/null and b/assets/trees.level.postcard differ diff --git a/examples/postcard.rs b/examples/postcard.rs new file mode 100644 index 0000000..1fa1fad --- /dev/null +++ b/examples/postcard.rs @@ -0,0 +1,61 @@ +use bevy::prelude::*; +use bevy::reflect::TypePath; +use bevy_common_assets::postcard::PostcardAssetPlugin; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + PostcardAssetPlugin::::new(&["level.postcard"]), + )) + .insert_resource(Msaa::Off) + .init_state::() + .add_systems(Startup, setup) + .add_systems(Update, spawn_level.run_if(in_state(AppState::Loading))) + .run() +} + +fn setup(mut commands: Commands, asset_server: Res) { + let level = LevelHandle(asset_server.load("trees.level.postcard")); + commands.insert_resource(level); + let tree = ImageHandle(asset_server.load("tree.png")); + commands.insert_resource(tree); + commands.spawn(Camera2dBundle::default()); +} + +fn spawn_level( + mut commands: Commands, + level: Res, + tree: Res, + mut levels: ResMut>, + mut state: ResMut>, +) { + if let Some(level) = levels.remove(level.0.id()) { + for position in level.positions { + commands.spawn(SpriteBundle { + transform: Transform::from_translation(position.into()), + texture: tree.0.clone(), + ..default() + }); + } + state.set(AppState::Level); + } +} + +#[derive(serde::Deserialize, Asset, TypePath)] +struct Level { + positions: Vec<[f32; 3]>, +} + +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] +enum AppState { + #[default] + Loading, + Level, +} + +#[derive(Resource)] +struct ImageHandle(Handle); + +#[derive(Resource)] +struct LevelHandle(Handle); diff --git a/src/lib.rs b/src/lib.rs index a81644a..b8f0bb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,10 @@ pub mod json; #[cfg_attr(docsrs, doc(cfg(feature = "msgpack")))] #[cfg(feature = "msgpack")] pub mod msgpack; +/// Module containing a Bevy plugin to load assets from `postcard` files with custom file extensions. +#[cfg_attr(docsrs, doc(cfg(feature = "postcard")))] +#[cfg(feature = "postcard")] +pub mod postcard; /// Module containing a Bevy plugin to load assets from `ron` files with custom file extensions. #[cfg_attr(docsrs, doc(cfg(feature = "ron")))] #[cfg(feature = "ron")] @@ -84,7 +88,8 @@ pub mod yaml; feature = "toml", feature = "xml", feature = "yaml", - feature = "csv" + feature = "csv", + feature = "postcard", ))] #[doc = include_str!("../README.md")] #[cfg(doctest)] diff --git a/src/postcard.rs b/src/postcard.rs new file mode 100644 index 0000000..3f41562 --- /dev/null +++ b/src/postcard.rs @@ -0,0 +1,113 @@ +use bevy::{ + app::{App, Plugin}, + asset::{ + io::Reader, saver::AssetSaver, Asset, AssetApp, AssetLoader, AsyncReadExt, AsyncWriteExt, + LoadContext, + }, + prelude::*, + utils::{thiserror, BoxedFuture}, +}; +use postcard::{from_bytes, to_stdvec}; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use thiserror::Error; + +/// Plugin to load your asset type `A` from `Postcard` files. +pub struct PostcardAssetPlugin { + extensions: Vec<&'static str>, + _marker: PhantomData, +} + +impl Plugin for PostcardAssetPlugin +where + for<'de> A: Deserialize<'de> + Asset, +{ + fn build(&self, app: &mut App) { + app.init_asset::() + .register_asset_loader(PostcardAssetLoader:: { + extensions: self.extensions.clone(), + _marker: PhantomData, + }); + } +} + +impl PostcardAssetPlugin +where + for<'de> A: Deserialize<'de> + Asset, +{ + /// Create a new plugin that will load assets from files with the given extensions. + pub fn new(extensions: &[&'static str]) -> Self { + Self { + extensions: extensions.to_owned(), + _marker: PhantomData, + } + } +} + +struct PostcardAssetLoader { + extensions: Vec<&'static str>, + _marker: PhantomData, +} + +/// Possible errors that can be produced by [`PostcardAssetLoader`] or [`PostcardAssetSaver`] +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum PostcardAssetError { + /// An [IO Error](std::io::Error) + #[error("Could not read the file: {0}")] + Io(#[from] std::io::Error), + /// A [Postcard Error](postcard::Error) + #[error("Could not parse Postcard: {0}")] + PostcardError(#[from] postcard::Error), +} + +impl AssetLoader for PostcardAssetLoader +where + for<'de> A: Deserialize<'de> + Asset, +{ + type Asset = A; + type Settings = (); + type Error = PostcardAssetError; + + fn load<'a>( + &'a self, + reader: &'a mut Reader, + _settings: &'a (), + _load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let asset = from_bytes::(&bytes)?; + Ok(asset) + }) + } + + fn extensions(&self) -> &[&str] { + &self.extensions + } +} + +struct PostcardAssetSaver { + _marker: PhantomData, +} + +impl AssetSaver for PostcardAssetSaver { + type Asset = A; + type Settings = (); + type OutputLoader = (); + type Error = PostcardAssetError; + + fn save<'a>( + &'a self, + writer: &'a mut bevy::asset::io::Writer, + asset: bevy::asset::saver::SavedAsset<'a, Self::Asset>, + _settings: &'a Self::Settings, + ) -> BoxedFuture<'a, Result<::Settings, Self::Error>> { + Box::pin(async move { + let bytes = to_stdvec(&asset.get())?; + writer.write_all(&bytes).await?; + Ok(()) + }) + } +}