Skip to content

Commit

Permalink
Add support for CSV files
Browse files Browse the repository at this point in the history
  • Loading branch information
NiklasEi committed Jan 7, 2024
1 parent ed5f463 commit a7233c7
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 9 deletions.
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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" }
Expand Down Expand Up @@ -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"
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions assets/trees.level.csv
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions examples/csv.rs
Original file line number Diff line number Diff line change
@@ -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::<TreePosition>::new(&["level.csv"]),
))
.insert_resource(Msaa::Off)
.add_state::<AppState>()
.add_systems(Startup, setup)
.add_systems(Update, spawn_level.run_if(in_state(AppState::Loading)))
.run()
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
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<LevelHandle>,
asset_server: Res<AssetServer>,
tree: Res<ImageHandle>,
mut positios: ResMut<Assets<TreePosition>>,
mut state: ResMut<NextState<AppState>>,
) {
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<Image>);

#[derive(Resource)]
struct LevelHandle(Handle<LoadedCsv<TreePosition>>);
99 changes: 99 additions & 0 deletions src/csv.rs
Original file line number Diff line number Diff line change
@@ -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<A> {
extensions: Vec<&'static str>,
_marker: PhantomData<A>,
}

impl<A> Plugin for CsvAssetPlugin<A>
where
for<'de> A: serde::Deserialize<'de> + Asset,
{
fn build(&self, app: &mut App) {
app.init_asset::<A>()
.init_asset::<LoadedCsv<A>>()
.register_asset_loader(CsvAssetLoader::<A> {
extensions: self.extensions.clone(),
_marker: PhantomData,
});
}
}

impl<A> CsvAssetPlugin<A>
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<A> {
extensions: Vec<&'static str>,
_marker: PhantomData<A>,
}

/// 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<A>
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<Handle<A>>,
}

impl<A> AssetLoader for CsvAssetLoader<A>
where
for<'de> A: serde::Deserialize<'de> + Asset,
{
type Asset = LoadedCsv<A>;
type Settings = ();
type Error = CsvLoaderError;

fn load<'a>(
&'a self,
reader: &'a mut Reader,
_settings: &'a (),
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
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
}
}
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit a7233c7

Please sign in to comment.