diff --git a/assets/credits.md b/assets/credits.md new file mode 100644 index 0000000..a52ded7 --- /dev/null +++ b/assets/credits.md @@ -0,0 +1,4 @@ +# Credits + +- `sprites/sword.png`: https://opengameart.org/content/sword-normal +- `sprites/shield.png`: https://opengameart.org/content/round-shield-64x64 \ No newline at end of file diff --git a/assets/raw_items.ron b/assets/raw_items.ron new file mode 100644 index 0000000..b01efe9 --- /dev/null +++ b/assets/raw_items.ron @@ -0,0 +1,20 @@ +( + items: [ + ( + name: "sword", + description: "A sharp sword", + value: 10, + weight: 2.0, + max_stack: 1, + sprite: "sprites/sword.png", + ), + ( + name: "shield", + description: "A sturdy shield", + value: 5, + weight: 5.0, + max_stack: 1, + sprite: "sprites/shield.png", + ), + ], +) \ No newline at end of file diff --git a/assets/sprites/shield.png b/assets/sprites/shield.png new file mode 100644 index 0000000..2bade6e Binary files /dev/null and b/assets/sprites/shield.png differ diff --git a/assets/sprites/sword.png b/assets/sprites/sword.png new file mode 100644 index 0000000..5105aec Binary files /dev/null and b/assets/sprites/sword.png differ diff --git a/examples/raw_manifest.rs b/examples/raw_manifest.rs index 3916fb6..e4e7232 100644 --- a/examples/raw_manifest.rs +++ b/examples/raw_manifest.rs @@ -9,4 +9,193 @@ //! //! This example showcases a relatively simple raw manifest pattern, where the raw manifest is used to initialize handles to sprites; //! see the other examples for more complex use cases! -fn main() {} +//! This example builds on the `simple.rs` example, using much of the same code and patterns. + +use std::path::PathBuf; + +use bevy::{app::AppExit, prelude::*, utils::HashMap}; +use leafwing_manifest::{ + asset_state::SimpleAssetState, + identifier::Id, + manifest::{Manifest, ManifestFormat}, + plugin::{AppExt, ManifestPlugin}, +}; +use serde::{Deserialize, Serialize}; + +/// The data for as single item that might be held in the player's inventory. +/// +/// This is the format that our item data is stored in after it's been loaded into a Bevy [`Resource`]. +#[derive(Debug, PartialEq)] +#[allow(dead_code)] // Properties are for demonstration purposes only. +struct Item { + name: String, + description: String, + value: i32, + weight: f32, + max_stack: u8, + sprite: Handle, +} + +/// The raw format for [`Item`] data. +/// +/// This is used inside of [`RawItemManifest`] to be saved/loaded to disk as an [`Asset`]. +/// The only difference in this case is that the `sprite` field has been changed from a loaded [`Handle`] to a [`PathBuf`]. +/// This [`PathBuf`] references the actual sprite path in our assets folder, +/// but other identifiers could be used for more complex asset loading strategies. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct RawItem { + name: String, + description: String, + value: i32, + weight: f32, + max_stack: u8, + sprite: PathBuf, +} + +/// A data-driven manifest, which contains the canonical data for all the items in the game. +/// +/// This is the bevy [`Resource`] that our [`Item`]s will be stored in after they are loaded +#[derive(Debug, Resource, PartialEq)] +struct ItemManifest { + items: HashMap, Item>, +} + +/// The raw format for [`ItemManifest`] +/// +/// This is what actually gets serialized to disk when saving/loading our manifest asset. +/// Since we generate our [`Id`]s from item names, the raw storage is just a plain [`Vec`], +/// And the [`Id`]s can be generated when processing the raw manifest into the standard manifest. +#[derive(Debug, Asset, TypePath, Serialize, Deserialize, PartialEq)] +struct RawItemManifest { + items: Vec, +} + +impl Manifest for ItemManifest { + // Because we're using a different format for raw/final data, + // we need to specify both types here + type Item = Item; + type RawItem = RawItem; + // Similarly, the manifest types also need to be converted + type RawManifest = RawItemManifest; + // Asset loading always returns a Handle, so our conversion is technically infallible. + // Asset loading can still fail further down the pipeline, which would have to be handled separately. + type ConversionError = std::convert::Infallible; + + const FORMAT: ManifestFormat = ManifestFormat::Ron; + + fn get(&self, id: Id) -> Option<&Self::Item> { + self.items.get(&id) + } + + // After the raw manifest is deserialied from the disk, we need to process the data slightly. + // In this case, we need to look up and load our sprite assets, and store the handles. + fn from_raw_manifest( + raw_manifest: Self::RawManifest, + world: &mut World, + ) -> Result { + // Asset server to load our sprite assets + let asset_server = world.resource::(); + + let items: HashMap<_, _> = raw_manifest + .items + .into_iter() + .map(|raw_item| { + // Load the sprite from the path provided in the raw data + let sprite_handle = asset_server.load(raw_item.sprite); + + // Construct actual item data + // Most of this is identical, except for the newly generated asset handle + let item = Item { + name: raw_item.name, + description: raw_item.description, + value: raw_item.value, + weight: raw_item.weight, + max_stack: raw_item.max_stack, + sprite: sprite_handle, + }; + + // Build an Id for our item, so it can be looked up later + let id = Id::from_name(&item.name); + + (id, item) + }) + .collect(); + + Ok(ItemManifest { items }) + } +} + +fn main() { + App::new() + // This example is TUI only, but the default plugins are used because they contain a bunch of asset loading stuff we need. + .add_plugins(DefaultPlugins) + // This is our simple state, used to navigate the asset loading process. + .init_state::() + // Coordinates asset loading and state transitions. + .add_plugins(ManifestPlugin::::default()) + // Registers our item manifest, triggering it to be loaded. + .register_manifest::("raw_items.ron") + .add_systems( + Update, + list_available_items.run_if(in_state(SimpleAssetState::Ready)), + ) + .run(); +} + +/// This system reads the generated item manifest resource and prints out all the items. +fn list_available_items( + item_manifest: Res, + mut app_exit_events: EventWriter, +) { + for (id, item) in item_manifest.items.iter() { + info!("{:?}: {:?}", id, item); + } + + // We are out of here + app_exit_events.send_default(); +} + +/// This module is used to generate the item manifest. +/// +/// While manifests *can* be hand-authored, it's often more convenient to generate them using tooling of some kind. +/// Serde's [`Serialize`] and [`Deserialize`] traits are a good fit for this purpose. +/// `ron` is a straightforward human-readable format that plays well with Rust's type system, and is a good point to start. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_raw_item_manifest() { + let mut items = Vec::default(); + + items.push(RawItem { + name: "sword".into(), + description: "A sharp sword".into(), + value: 10, + weight: 2.0, + max_stack: 1, + sprite: PathBuf::from("sprites/sword.png"), + }); + + items.push(RawItem { + name: "shield".into(), + description: "A sturdy shield".into(), + value: 5, + weight: 5.0, + max_stack: 1, + sprite: PathBuf::from("sprites/shield.png"), + }); + + let item_manifest = RawItemManifest { items }; + + let serialized = ron::ser::to_string_pretty(&item_manifest, Default::default()).unwrap(); + println!("{}", serialized); + + // Save the results, to ensure that our example has a valid manifest to read. + std::fs::write("assets/raw_items.ron", &serialized).unwrap(); + + let deserialized: RawItemManifest = ron::de::from_str(&serialized).unwrap(); + + assert_eq!(item_manifest, deserialized); + } +} diff --git a/examples/simple.rs b/examples/simple.rs index a7b56b7..d1037d3 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,10 +1,12 @@ //! This example demonstrates the simplest use of the `leafwing_manifest` crate. //! //! In this example, the manifest and raw manifest are the same type, and the data is read directly from the serialized format on disk into the [`ItemManifest`] resource. +//! //! This pattern is great for simple prototyping and small projects, but can be quickly outgrown as the project's needs scale. //! See the other examples for more advanced use cases! +//! The `raw_manifest.rs` example is a good next step that builds upon this example. -use bevy::{app::AppExit, prelude::*, utils::HashMap}; +use bevy::{app::AppExit, log::LogPlugin, prelude::*, utils::HashMap}; use leafwing_manifest::{ asset_state::SimpleAssetState, identifier::Id, @@ -69,7 +71,7 @@ fn main() { App::new() // leafwing_manifest requires `AssetPlugin` to function // This is included in `DefaultPlugins`, but this example is very small, so it only uses the `MinimalPlugins` - .add_plugins((MinimalPlugins, AssetPlugin::default())) + .add_plugins((MinimalPlugins, AssetPlugin::default(), LogPlugin::default())) // This is our simple state, used to navigate the asset loading process. .init_state::() // Coordinates asset loading and state transitions. @@ -89,7 +91,7 @@ fn list_available_items( mut app_exit_events: EventWriter, ) { for (id, item) in item_manifest.items.iter() { - println!("{:?}: {:?}", id, item); + info!("{:?}: {:?}", id, item); } // We are out of here diff --git a/src/manifest.rs b/src/manifest.rs index e0a6787..cb1cdc7 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -48,7 +48,7 @@ pub trait Manifest: Sized + Resource { /// These are commonly [`Bundle`](bevy::ecs::bundle::Bundle) types, allowing you to directly spawn them into the [`World`]. /// If you wish to store [`Handles`](bevy::asset::Handle) to other assets (such as textures, sprites or sounds), /// starting the asset loading process for those assets in [`from_raw_manifest`](Manifest::from_raw_manifest) works very well! - type Item: TryFrom; + type Item; /// The error type that can occur when converting raw manifests into a manifest. /// @@ -163,20 +163,6 @@ pub trait MutableManifest: Manifest { item: Self::Item, ) -> Result, ManifestModificationError>; - /// Converts and then inserts a raw item into the manifest. - /// - /// This is a convenience method that combines the conversion and insertion steps. - fn insert_raw_item( - &mut self, - raw_item: Self::RawItem, - ) -> Result, ManifestModificationError> { - let conversion_result = TryFrom::::try_from(raw_item); - - match conversion_result { - Ok(item) => self.insert(item), - Err(e) => Err(ManifestModificationError::ConversionFailed(e)), - } - } /// Inserts a new item into the manifest by name. /// /// The item is given a unique identifier, which is returned. @@ -196,22 +182,6 @@ pub trait MutableManifest: Manifest { } } - /// Converts and then inserts a raw item into the manifest by name. - /// - /// This is a convenience method that combines the conversion and insertion steps. - fn insert_raw_item_by_name( - &mut self, - name: &str, - raw_item: Self::RawItem, - ) -> Result, ManifestModificationError> { - let conversion_result = TryFrom::::try_from(raw_item); - - match conversion_result { - Ok(item) => self.insert_by_name(name, item), - Err(e) => Err(ManifestModificationError::ConversionFailed(e)), - } - } - /// Removes an item from the manifest. /// /// The item removed is returned, if it was found.