Skip to content

Commit

Permalink
feat: add types and traits around LevelMetadata (#229)
Browse files Browse the repository at this point in the history
Works towards #205.

The asset types are being redesigned a bit to clone less and provide
better APIs. One design goal is to correctly handle both external level
projects and internal level projects. Since we want to avoid loading
level data into a separate asset in the internal level case, these two
scenarios will produce different level metadata on loading. Both need to
map level iids to their indices and background image handles. External
level projects additionally need to produce a handle to the external
level asset. This PR provides `LevelMetadata` and
`ExternalLevelMetadata` types to represent the metadata produced during
project loading.

Mapping iids to level indices with the help of the level metadata types
allows constant-time lookup of levels by iid. This is handy since users
can have access to the level iid thanks to the new `LevelIid` component,
and can use it for finding level data. This also speeds up accessing
levels by `LevelSelection` - two of the 4 level selection variants can
now be constant-time-lookup. To aid in these two use cases, a
`LevelMetadataAccessor` trait is also added in this PR. It can be
implemented for types that store raw level data and a mapping of level
iids to `LevelMetadata`. It provides methods for accessing raw level
data by iid and by `LevelSelection`.

These types/traits have not actually been integrated into `LdtkProject`
in this PR. Doing so is a pretty major change and will be a large PR.
These types/traits have been given their own PR in an attempt to keep
PRs small.
  • Loading branch information
Trouv authored Sep 14, 2023
1 parent d3de2d9 commit 382dea2
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 1 deletion.
89 changes: 89 additions & 0 deletions src/assets/level_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use crate::assets::LevelIndices;
use bevy::{
prelude::*,
reflect::{TypePath, TypeUuid},
};
use derive_getters::Getters;

use crate::assets::LdtkLevel;

/// Metadata produced for every level during [`LdtkProject`] loading.
///
/// [`LdtkProject`]: crate::assets::LdtkProject
#[derive(Clone, Debug, Default, Eq, PartialEq, TypeUuid, TypePath, Getters)]
#[uuid = "bba47e30-5036-4994-acde-d62a440b16b8"]
pub struct LevelMetadata {
/// Image handle for the background image of this level, if it has one.
bg_image: Option<Handle<Image>>,
/// Indices of this level in the project.
indices: LevelIndices,
}

impl LevelMetadata {
/// Construct a new [`LevelMetadata`].
pub fn new(bg_image: Option<Handle<Image>>, indices: LevelIndices) -> Self {
LevelMetadata { bg_image, indices }
}
}

/// Metadata produced for every level during [`LdtkProject`] loading for external-levels projects.
///
/// [`LdtkProject`]: crate::assets::LdtkProject
#[derive(Clone, Debug, Default, Eq, PartialEq, TypeUuid, TypePath, Getters)]
#[uuid = "d3190ad4-6fa4-4f47-b15b-87f92f191738"]
pub struct ExternalLevelMetadata {
/// Common metadata for this level.
metadata: LevelMetadata,
/// Handle to this external level's asset data.
external_handle: Handle<LdtkLevel>,
}

impl ExternalLevelMetadata {
/// Construct a new [`ExternalLevelMetadata`].
pub fn new(metadata: LevelMetadata, external_handle: Handle<LdtkLevel>) -> Self {
ExternalLevelMetadata {
metadata,
external_handle,
}
}
}

#[cfg(test)]
mod tests {
use bevy::render::texture::DEFAULT_IMAGE_HANDLE;

use super::*;

#[test]
fn level_metadata_construction() {
let level_metadata = LevelMetadata::new(None, LevelIndices::in_root(1));

assert_eq!(*level_metadata.bg_image(), None);
assert_eq!(*level_metadata.indices(), LevelIndices::in_root(1));

let level_metadata = LevelMetadata::new(
Some(DEFAULT_IMAGE_HANDLE.typed()),
LevelIndices::in_world(2, 3),
);

assert_eq!(
*level_metadata.bg_image(),
Some(DEFAULT_IMAGE_HANDLE.typed())
);
assert_eq!(*level_metadata.indices(), LevelIndices::in_world(2, 3));
}

#[test]
fn external_level_metadata_construction() {
let level_metadata = LevelMetadata::new(None, LevelIndices::in_root(1));

let external_level_metadata =
ExternalLevelMetadata::new(level_metadata.clone(), Handle::default());

assert_eq!(*external_level_metadata.metadata(), level_metadata);
assert_eq!(
*external_level_metadata.external_handle(),
Handle::default()
);
}
}
274 changes: 274 additions & 0 deletions src/assets/level_metadata_accessor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
use crate::{
assets::LevelMetadata,
ldtk::{raw_level_accessor::RawLevelAccessor, Level},
LevelSelection,
};

/// Convenience methods for types that store levels and level metadata.
pub trait LevelMetadataAccessor: RawLevelAccessor {
/// Returns a reference to the level metadata corresponding to the given level iid.
// We accept an `&String` here to avoid creating a new `String`.
// Implementations will use this to index a `HashMap<String, _>`, which requires `&String`.
// So, accepting `&str` or `AsRef<str>` or `Into<String>` would all require either taking
// ownership or creating a new string.
#[allow(clippy::ptr_arg)]
fn get_level_metadata_by_iid(&self, iid: &String) -> Option<&LevelMetadata>;

/// Immutable access to a level at the given level iid.
///
/// Note: all levels are considered [raw](RawLevelAccessor#raw-levels).
// We accept an `&String` here to avoid creating a new `String`.
// Implementations will use this to index a `HashMap<String, _>`, which requires `&String`.
// So, accepting `&str` or `AsRef<str>` or `Into<String>` would all require either taking
// ownership or creating a new string.
#[allow(clippy::ptr_arg)]
fn get_raw_level_by_iid(&self, iid: &String) -> Option<&Level> {
self.get_level_metadata_by_iid(iid)
.and_then(|metadata| self.get_raw_level_at_indices(metadata.indices()))
}

/// Find the level matching the given the given [`LevelSelection`].
///
/// This lookup is constant for [`LevelSelection::Iid`] and [`LevelSelection::Indices`] variants.
/// The other variants require iterating through the levels to find the match.
///
/// Note: all levels are considered [raw](RawLevelAccessor#raw-levels).
fn find_raw_level_by_level_selection(
&self,
level_selection: &LevelSelection,
) -> Option<&Level> {
match level_selection {
LevelSelection::Iid(iid) => self.get_raw_level_by_iid(iid.get()),
LevelSelection::Indices(indices) => self.get_raw_level_at_indices(indices),
LevelSelection::Identifier(selected_identifier) => self
.iter_raw_levels()
.find(|Level { identifier, .. }| identifier == selected_identifier),
LevelSelection::Uid(selected_uid) => self
.iter_raw_levels()
.find(|Level { uid, .. }| uid == selected_uid),
}
}
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use crate::{
ldtk::{raw_level_accessor::tests::sample_levels, LdtkJson, World},
LevelIid,
};

use super::*;

struct BasicLevelMetadataAccessor {
data: LdtkJson,
level_metadata: HashMap<String, LevelMetadata>,
}

impl RawLevelAccessor for BasicLevelMetadataAccessor {
fn worlds(&self) -> &[crate::ldtk::World] {
self.data.worlds()
}

fn root_levels(&self) -> &[Level] {
self.data.root_levels()
}
}

impl LevelMetadataAccessor for BasicLevelMetadataAccessor {
fn get_level_metadata_by_iid(&self, iid: &String) -> Option<&LevelMetadata> {
self.level_metadata.get(iid)
}
}

impl BasicLevelMetadataAccessor {
fn sample_with_root_levels() -> BasicLevelMetadataAccessor {
let [level_a, level_b, level_c, level_d] = sample_levels();

let data = LdtkJson {
levels: vec![level_a, level_b, level_c, level_d],
..Default::default()
};

let level_metadata = data
.iter_raw_levels_with_indices()
.map(|(indices, level)| (level.iid.clone(), LevelMetadata::new(None, indices)))
.collect();

BasicLevelMetadataAccessor {
data,
level_metadata,
}
}

fn sample_with_world_levels() -> BasicLevelMetadataAccessor {
let [level_a, level_b, level_c, level_d] = sample_levels();

let world_a = World {
levels: vec![level_a.clone(), level_b.clone()],
..Default::default()
};

let world_b = World {
levels: vec![level_c.clone(), level_d.clone()],
..Default::default()
};

let data = LdtkJson {
worlds: vec![world_a, world_b],
..Default::default()
};

let level_metadata = data
.iter_raw_levels_with_indices()
.map(|(indices, level)| (level.iid.clone(), LevelMetadata::new(None, indices)))
.collect();

BasicLevelMetadataAccessor {
data,
level_metadata,
}
}
}

#[test]
fn iid_lookup_returns_expected_root_levels() {
let accessor = BasicLevelMetadataAccessor::sample_with_root_levels();

let expected_levels = sample_levels();

for expected_level in expected_levels {
assert_eq!(
accessor.get_raw_level_by_iid(&expected_level.iid),
Some(&expected_level)
);
}
assert_eq!(
accessor.get_raw_level_by_iid(&"cd51071d-5224-4628-ae0d-abbe28090521".to_string()),
None,
);
}

#[test]
fn iid_lookup_returns_expected_world_levels() {
let accessor = BasicLevelMetadataAccessor::sample_with_world_levels();

let expected_levels = sample_levels();

for expected_level in expected_levels {
assert_eq!(
accessor.get_raw_level_by_iid(&expected_level.iid),
Some(&expected_level)
);
}
assert_eq!(
accessor.get_raw_level_by_iid(&"cd51071d-5224-4628-ae0d-abbe28090521".to_string()),
None,
);
}

#[test]
fn find_by_level_selection_returns_expected_root_levels() {
let accessor = BasicLevelMetadataAccessor::sample_with_root_levels();

let expected_levels = sample_levels();

for (i, expected_level) in expected_levels.iter().enumerate() {
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::index(i)),
Some(expected_level)
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Identifier(
expected_level.identifier.clone()
)),
Some(expected_level)
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Iid(LevelIid::new(
expected_level.iid.clone()
))),
Some(expected_level)
);
assert_eq!(
accessor
.find_raw_level_by_level_selection(&LevelSelection::Uid(expected_level.uid)),
Some(expected_level)
);
}

assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::index(4)),
None
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Identifier(
"Back_Rooms".to_string()
)),
None
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Iid(LevelIid::new(
"cd51071d-5224-4628-ae0d-abbe28090521".to_string()
))),
None
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Uid(2023)),
None,
);
}

#[test]
fn find_by_level_selection_returns_expected_world_levels() {
let accessor = BasicLevelMetadataAccessor::sample_with_world_levels();

let expected_levels = sample_levels();

for (i, expected_level) in expected_levels.iter().enumerate() {
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::indices(i / 2, i % 2)),
Some(expected_level)
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Identifier(
expected_level.identifier.clone()
)),
Some(expected_level)
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Iid(LevelIid::new(
expected_level.iid.clone()
))),
Some(expected_level)
);
assert_eq!(
accessor
.find_raw_level_by_level_selection(&LevelSelection::Uid(expected_level.uid)),
Some(expected_level)
);
}

assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::index(4)),
None
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Identifier(
"Back_Rooms".to_string()
)),
None
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Iid(LevelIid::new(
"cd51071d-5224-4628-ae0d-abbe28090521".to_string()
))),
None
);
assert_eq!(
accessor.find_raw_level_by_level_selection(&LevelSelection::Uid(2023)),
None,
);
}
}
6 changes: 6 additions & 0 deletions src/assets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use std::path::Path;
mod ldtk_asset_plugin;
pub use ldtk_asset_plugin::LdtkAssetPlugin;

mod level_metadata;
pub use level_metadata::{ExternalLevelMetadata, LevelMetadata};

mod level_metadata_accessor;
pub use level_metadata_accessor::LevelMetadataAccessor;

mod ldtk_level;
pub use ldtk_level::LdtkLevel;

Expand Down
Loading

0 comments on commit 382dea2

Please sign in to comment.