Skip to content

Commit

Permalink
Merge pull request #22 from Leafwing-Studios/raw-manifest-example
Browse files Browse the repository at this point in the history
Add raw manifest example
  • Loading branch information
sixfold-origami authored Apr 15, 2024
2 parents 446fa75 + 3371e3d commit 51929ed
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 35 deletions.
4 changes: 4 additions & 0 deletions assets/credits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Credits

- `sprites/sword.png`: https://opengameart.org/content/sword-normal
- `sprites/shield.png`: https://opengameart.org/content/round-shield-64x64
20 changes: 20 additions & 0 deletions assets/raw_items.ron
Original file line number Diff line number Diff line change
@@ -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",
),
],
)
Binary file added assets/sprites/shield.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/sprites/sword.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
191 changes: 190 additions & 1 deletion examples/raw_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Image>,
}

/// 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<Image>`] 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<Id<Item>, 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<RawItem>,
}

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<Item>) -> 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<Self, Self::ConversionError> {
// Asset server to load our sprite assets
let asset_server = world.resource::<AssetServer>();

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::<SimpleAssetState>()
// Coordinates asset loading and state transitions.
.add_plugins(ManifestPlugin::<SimpleAssetState>::default())
// Registers our item manifest, triggering it to be loaded.
.register_manifest::<ItemManifest>("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<ItemManifest>,
mut app_exit_events: EventWriter<AppExit>,
) {
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);
}
}
8 changes: 5 additions & 3 deletions examples/simple.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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::<SimpleAssetState>()
// Coordinates asset loading and state transitions.
Expand All @@ -89,7 +91,7 @@ fn list_available_items(
mut app_exit_events: EventWriter<AppExit>,
) {
for (id, item) in item_manifest.items.iter() {
println!("{:?}: {:?}", id, item);
info!("{:?}: {:?}", id, item);
}

// We are out of here
Expand Down
32 changes: 1 addition & 31 deletions src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self::RawItem, Error = Self::ConversionError>;
type Item;

/// The error type that can occur when converting raw manifests into a manifest.
///
Expand Down Expand Up @@ -163,20 +163,6 @@ pub trait MutableManifest: Manifest {
item: Self::Item,
) -> Result<Id<Self::Item>, ManifestModificationError<Self>>;

/// 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<Id<Self::Item>, ManifestModificationError<Self>> {
let conversion_result = TryFrom::<Self::RawItem>::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.
Expand All @@ -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<Id<Self::Item>, ManifestModificationError<Self>> {
let conversion_result = TryFrom::<Self::RawItem>::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.
Expand Down

0 comments on commit 51929ed

Please sign in to comment.