From a7233c7c10b79d70742a35f282aacd1ee1a8b50c Mon Sep 17 00:00:00 2001 From: Niklas Eicker Date: Sun, 7 Jan 2024 18:39:20 +0100 Subject: [PATCH] Add support for CSV files --- Cargo.toml | 7 +++ README.md | 17 ++++---- assets/trees.level.csv | 7 +++ examples/csv.rs | 71 ++++++++++++++++++++++++++++++ src/csv.rs | 99 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 7 ++- 6 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 assets/trees.level.csv create mode 100644 examples/csv.rs create mode 100644 src/csv.rs diff --git a/Cargo.toml b/Cargo.toml index 579b6e8..5509dc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ yaml = ["dep:serde_yaml"] json = ["dep:serde_json"] msgpack = ["dep:rmp-serde"] xml = ["dep:quick-xml"] +csv = ["dep:csv"] [dependencies] bevy = { version = "0.12", default-features = false, features = ["bevy_asset"] } @@ -27,6 +28,7 @@ serde_ron = { version = "0.8", package = "ron", optional = true } serde_yaml = { version = "0.9", optional = true } serde_json = { version = "1", optional = true } rmp-serde = { version = "1", optional = true } +csv = { version = "1", optional = true } thiserror = "1.0" quick-xml = { version = "0.31", features = [ "serialize" ], optional = true } serde = { version = "1" } @@ -70,6 +72,11 @@ name = "xml" path = "examples/xml.rs" required-features = ["xml"] +[[example]] +name = "csv" +path = "examples/csv.rs" +required-features = ["csv"] + [[example]] name = "multiple_formats" path = "examples/multiple_formats.rs" diff --git a/README.md b/README.md index 9c29d91..e657cd5 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,15 @@ Collection of [Bevy][bevy] plugins offering generic asset loaders for common fil Supported formats: -| format | feature | example | -|:----------|:----------|:--------------------------------------| -| `json` | `json` | [`json.rs`](./examples/json.rs) | -| `msgpack` | `msgpack` | [`msgpack.rs`](./examples/msgpack.rs) | -| `ron` | `ron` | [`ron.rs`](./examples/ron.rs) | -| `toml` | `toml` | [`toml.rs`](./examples/toml.rs) | -| `xml` | `xml` | [`xml.rs`](./examples/xml.rs) | -| `yaml` | `yaml` | [`yaml.rs`](./examples/yaml.rs) | +| format | feature | example | +|:-----------|:-----------|:---------------------------------------| +| `json` | `json` | [`json.rs`](./examples/json.rs) | +| `msgpack` | `msgpack` | [`msgpack.rs`](./examples/msgpack.rs) | +| `ron` | `ron` | [`ron.rs`](./examples/ron.rs) | +| `toml` | `toml` | [`toml.rs`](./examples/toml.rs) | +| `xml` | `xml` | [`xml.rs`](./examples/xml.rs) | +| `yaml` | `yaml` | [`yaml.rs`](./examples/yaml.rs) | +| `csv` | `csv` | [`csv.rs`](./examples/csv.rs) | ## Usage diff --git a/assets/trees.level.csv b/assets/trees.level.csv new file mode 100644 index 0000000..4e1c174 --- /dev/null +++ b/assets/trees.level.csv @@ -0,0 +1,7 @@ +x,y,z +42.0,42.0,0.0 +4.0,32.0,0.0 +54.0,7.0,0.0 +-61.0,4.0,0.0 +-6.0,-72.0,0.0 +6.0,-89.0,0.0 diff --git a/examples/csv.rs b/examples/csv.rs new file mode 100644 index 0000000..4294843 --- /dev/null +++ b/examples/csv.rs @@ -0,0 +1,71 @@ +use bevy::asset::RecursiveDependencyLoadState; +use bevy::prelude::*; +use bevy::reflect::TypePath; +use bevy_common_assets::csv::{CsvAssetPlugin, LoadedCsv}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + CsvAssetPlugin::::new(&["level.csv"]), + )) + .insert_resource(Msaa::Off) + .add_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.csv")); + 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, + asset_server: Res, + tree: Res, + mut positios: ResMut>, + mut state: ResMut>, +) { + if asset_server.get_recursive_dependency_load_state(&level.0) + == Some(RecursiveDependencyLoadState::Loaded) + { + for (_, position) in positios.iter() { + commands.spawn(SpriteBundle { + transform: Transform::from_translation(Vec3::new( + position.x, position.y, position.z, + )), + texture: tree.0.clone(), + ..default() + }); + } + + state.set(AppState::Level); + } +} + +#[derive(serde::Deserialize, Asset, TypePath, Debug)] +struct TreePosition { + x: f32, + y: f32, + z: f32, +} + +#[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/csv.rs b/src/csv.rs new file mode 100644 index 0000000..e17c8f3 --- /dev/null +++ b/src/csv.rs @@ -0,0 +1,99 @@ +use bevy::app::{App, Plugin}; +use bevy::asset::io::Reader; +use bevy::asset::{Asset, AssetApp, AssetLoader, AsyncReadExt, BoxedFuture, Handle, LoadContext}; +use bevy::prelude::TypePath; +use std::marker::PhantomData; +use thiserror::Error; + +/// Plugin to load your asset type `A` from csv files. +pub struct CsvAssetPlugin { + extensions: Vec<&'static str>, + _marker: PhantomData, +} + +impl Plugin for CsvAssetPlugin +where + for<'de> A: serde::Deserialize<'de> + Asset, +{ + fn build(&self, app: &mut App) { + app.init_asset::() + .init_asset::>() + .register_asset_loader(CsvAssetLoader:: { + extensions: self.extensions.clone(), + _marker: PhantomData, + }); + } +} + +impl CsvAssetPlugin +where + for<'de> A: serde::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 CsvAssetLoader { + extensions: Vec<&'static str>, + _marker: PhantomData, +} + +/// Possible errors that can be produced by [`CsvAssetLoader`] +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum CsvLoaderError { + /// An [IO Error](std::io::Error) + #[error("Could not read the file: {0}")] + Io(#[from] std::io::Error), + /// A [CSV Error](serde_csv::Error) + #[error("Could not parse CSV: {0}")] + CsvError(#[from] csv::Error), +} + +/// Asset representing a loaded CSV file with rows deserialized to Assets of type `A` +#[derive(TypePath, Asset)] +pub struct LoadedCsv +where + for<'de> A: serde::Deserialize<'de> + Asset, +{ + /// Handles to the Assets the were loaded from the rows of this CSV file + pub rows: Vec>, +} + +impl AssetLoader for CsvAssetLoader +where + for<'de> A: serde::Deserialize<'de> + Asset, +{ + type Asset = LoadedCsv; + type Settings = (); + type Error = CsvLoaderError; + + 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 mut reader = csv::Reader::from_reader(bytes.as_slice()); + let mut handles = vec![]; + for (index, result) in reader.deserialize().enumerate() { + let asset: A = result?; + handles + .push(load_context.add_loaded_labeled_asset(index.to_string(), asset.into())); + } + Ok(LoadedCsv { rows: handles }) + }) + } + + fn extensions(&self) -> &[&str] { + &self.extensions + } +} diff --git a/src/lib.rs b/src/lib.rs index 42ec2a6..e4bdf68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,10 @@ #![warn(unused_imports, missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] +/// Module containing a Bevy plugin to load assets from `csv` files with custom file extensions. +#[cfg_attr(docsrs, doc(cfg(feature = "csv")))] +#[cfg(feature = "csv")] +pub mod csv; /// Module containing a Bevy plugin to load assets from `json` files with custom file extensions. #[cfg_attr(docsrs, doc(cfg(feature = "json")))] #[cfg(feature = "json")] @@ -79,7 +83,8 @@ pub mod yaml; feature = "ron", feature = "toml", feature = "xml", - feature = "yaml" + feature = "yaml", + feature = "csv" ))] #[doc = include_str!("../README.md")] #[cfg(doctest)]