-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add types and traits around LevelMetadata (#229)
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
Showing
5 changed files
with
378 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.