From 670cd4e6b704a4748ab41070742733004f1686f9 Mon Sep 17 00:00:00 2001 From: Trevor Lovell Date: Tue, 17 Oct 2023 19:02:19 -0600 Subject: [PATCH] feat!: redesign LdtkProject with better level data accessors and correct modeling of internal/external levels (#244) Closes #205 This is the final PR for redesigning the asset types. The main improvements of this redesign are that the type provides better APIs for accessing raw or loaded level data, and internal/external levels are modeled more correctly. The `LdtkProject` type is still the asset type and stores most metadata applicable to either level locale, along with `LdtkProjectData`. `LdtkProjectData` is a enum whose variants store the actual ldtk json data, with a variant for each level locality. The internal types of the variants are `LdtkJsonWithMetadata`, with either `InternalLevels` or `ExternalLevels` as `L`. `LdtkJsonWithMetadata` is a generic type storing the actual ldtk json data + level metadata, with either `InternalLevels` or `ExternalLevels` as `L`. Splitting these up like this allows us to define some methods that are exclusive to each level locality. This is important because accessing loaded level data is a very different operation memory-wise for each case (indexing the `LdtkJson` for internal-levels, or accessing the `Assets` asset store for external levels). But other methods can be provided for either case, either with a generic implementation at the lowest level, or exposing transparent implementations in the higher-level types. An important point about this new design is that `LdtkLevel` assets are no longer used in the internal-levels case. Level entities will only ever have a `LevelIid` component, no longer a `Handle`. The handle for the asset in the external-levels case is only stored inside the `LdtkProject`. To help make this change clear, `LdtkLevel` has been renamed `LdtkExternalLevel`. Doing things this way actually fixes an egregious clone of all level data that is in all previous versions of this plugin. See the documentation of `LdtkProject` to learn about the best ways to access loaded level data now. feat!: LdtkLevel renamed to LdtkExternalLevel and is no longer used as a component (#244) --- examples/field_instances/level_title.rs | 18 +- examples/platformer/systems.rs | 148 +++-- src/assets/ldtk_asset_plugin.rs | 18 +- src/assets/ldtk_external_level.rs | 107 ++++ src/assets/ldtk_json_with_metadata.rs | 74 ++- src/assets/ldtk_level.rs | 77 --- src/assets/ldtk_project.rs | 484 ++++++++++++--- src/assets/level_metadata.rs | 6 +- src/assets/mod.rs | 17 +- src/components/mod.rs | 22 +- src/ldtk/loaded_level.rs | 6 +- src/level.rs | 793 ++++++++++++------------ src/lib.rs | 5 +- src/systems.rs | 176 +++--- 14 files changed, 1178 insertions(+), 773 deletions(-) create mode 100644 src/assets/ldtk_external_level.rs delete mode 100644 src/assets/ldtk_level.rs diff --git a/examples/field_instances/level_title.rs b/examples/field_instances/level_title.rs index 766fae7c..69bdfe48 100644 --- a/examples/field_instances/level_title.rs +++ b/examples/field_instances/level_title.rs @@ -11,22 +11,24 @@ pub struct LevelTitle(String); pub fn set_level_title_to_current_level( mut level_events: EventReader, - level_handles: Query<&Handle>, - level_assets: Res>, + levels: Query<&LevelIid>, + projects: Query<&Handle>, + project_assets: Res>, mut current_level_title: ResMut, ) { for level_event in level_events.iter() { if matches!(level_event, LevelEvent::Transformed(_)) { - let level_handle = level_handles + let level_iid = levels .get_single() .expect("only one level should be spawned at a time in this example"); - let level_asset = level_assets - .get(level_handle) - .expect("level asset should be loaded before LevelEvent::Transformed"); + let level_data = project_assets + .get(projects.single()) + .expect("project asset should be loaded if levels are spawned") + .get_raw_level_by_iid(&level_iid.to_string()) + .expect("spawned level should exist in the loaded project"); - let title = level_asset - .data() + let title = level_data .get_string_field("title") .expect("level should have non-nullable title string field"); diff --git a/examples/platformer/systems.rs b/examples/platformer/systems.rs index af58f443..78f0b4a8 100644 --- a/examples/platformer/systems.rs +++ b/examples/platformer/systems.rs @@ -79,8 +79,9 @@ pub fn spawn_wall_collision( mut commands: Commands, wall_query: Query<(&GridCoords, &Parent), Added>, parent_query: Query<&Parent, Without>, - level_query: Query<(Entity, &Handle)>, - levels: Res>, + level_query: Query<(Entity, &LevelIid)>, + ldtk_projects: Query<&Handle>, + ldtk_project_assets: Res>, ) { /// Represents a wide wall that is 1 tile tall /// Used to spawn wall collisions @@ -120,22 +121,23 @@ pub fn spawn_wall_collision( }); if !wall_query.is_empty() { - level_query.for_each(|(level_entity, level_handle)| { + level_query.for_each(|(level_entity, level_iid)| { if let Some(level_walls) = level_to_wall_locations.get(&level_entity) { - let level = levels - .get(level_handle) - .expect("Level should be loaded by this point"); + let ldtk_project = ldtk_project_assets + .get(ldtk_projects.single()) + .expect("Project should be loaded if level has spawned"); + + let level = ldtk_project + .as_standalone() + .get_loaded_level_by_iid(&level_iid.to_string()) + .expect("Spawned level should exist in LDtk project"); let LayerInstance { c_wid: width, c_hei: height, grid_size, .. - } = level - .data() - .layer_instances - .clone() - .expect("Level asset should have layers")[0]; + } = level.layer_instances()[0]; // combine wall tiles into flat "plates" in each individual row let mut plate_stack: Vec> = Vec::new(); @@ -319,12 +321,10 @@ pub fn camera_fit_inside_current_level( Without, >, player_query: Query<&Transform, With>, - level_query: Query< - (&Transform, &Handle), - (Without, Without), - >, + level_query: Query<(&Transform, &LevelIid), (Without, Without)>, + ldtk_projects: Query<&Handle>, level_selection: Res, - ldtk_levels: Res>, + ldtk_project_assets: Res>, ) { if let Ok(Transform { translation: player_translation, @@ -335,67 +335,79 @@ pub fn camera_fit_inside_current_level( let (mut orthographic_projection, mut camera_transform) = camera_query.single_mut(); - for (level_transform, level_handle) in &level_query { - if let Some(ldtk_level) = ldtk_levels.get(level_handle) { - let level = &ldtk_level.data(); - if level_selection.is_match(&LevelIndices::default(), level) { - let level_ratio = level.px_wid as f32 / ldtk_level.data().px_hei as f32; - orthographic_projection.viewport_origin = Vec2::ZERO; - if level_ratio > ASPECT_RATIO { - // level is wider than the screen - let height = (level.px_hei as f32 / 9.).round() * 9.; - let width = height * ASPECT_RATIO; - orthographic_projection.scaling_mode = - bevy::render::camera::ScalingMode::Fixed { width, height }; - camera_transform.translation.x = - (player_translation.x - level_transform.translation.x - width / 2.) - .clamp(0., level.px_wid as f32 - width); - camera_transform.translation.y = 0.; - } else { - // level is taller than the screen - let width = (level.px_wid as f32 / 16.).round() * 16.; - let height = width / ASPECT_RATIO; - orthographic_projection.scaling_mode = - bevy::render::camera::ScalingMode::Fixed { width, height }; - camera_transform.translation.y = - (player_translation.y - level_transform.translation.y - height / 2.) - .clamp(0., level.px_hei as f32 - height); - camera_transform.translation.x = 0.; - } - - camera_transform.translation.x += level_transform.translation.x; - camera_transform.translation.y += level_transform.translation.y; + for (level_transform, level_iid) in &level_query { + let ldtk_project = ldtk_project_assets + .get(ldtk_projects.single()) + .expect("Project should be loaded if level has spawned"); + + let level = ldtk_project + .get_raw_level_by_iid(&level_iid.to_string()) + .expect("Spawned level should exist in LDtk project"); + + if level_selection.is_match(&LevelIndices::default(), level) { + let level_ratio = level.px_wid as f32 / level.px_hei as f32; + orthographic_projection.viewport_origin = Vec2::ZERO; + if level_ratio > ASPECT_RATIO { + // level is wider than the screen + let height = (level.px_hei as f32 / 9.).round() * 9.; + let width = height * ASPECT_RATIO; + orthographic_projection.scaling_mode = + bevy::render::camera::ScalingMode::Fixed { width, height }; + camera_transform.translation.x = + (player_translation.x - level_transform.translation.x - width / 2.) + .clamp(0., level.px_wid as f32 - width); + camera_transform.translation.y = 0.; + } else { + // level is taller than the screen + let width = (level.px_wid as f32 / 16.).round() * 16.; + let height = width / ASPECT_RATIO; + orthographic_projection.scaling_mode = + bevy::render::camera::ScalingMode::Fixed { width, height }; + camera_transform.translation.y = + (player_translation.y - level_transform.translation.y - height / 2.) + .clamp(0., level.px_hei as f32 - height); + camera_transform.translation.x = 0.; } + + camera_transform.translation.x += level_transform.translation.x; + camera_transform.translation.y += level_transform.translation.y; } } } } pub fn update_level_selection( - level_query: Query<(&Handle, &Transform), Without>, + level_query: Query<(&LevelIid, &Transform), Without>, player_query: Query<&Transform, With>, mut level_selection: ResMut, - ldtk_levels: Res>, + ldtk_projects: Query<&Handle>, + ldtk_project_assets: Res>, ) { - for (level_handle, level_transform) in &level_query { - if let Some(ldtk_level) = ldtk_levels.get(level_handle) { - let level_bounds = Rect { - min: Vec2::new(level_transform.translation.x, level_transform.translation.y), - max: Vec2::new( - level_transform.translation.x + ldtk_level.data().px_wid as f32, - level_transform.translation.y + ldtk_level.data().px_hei as f32, - ), - }; - - for player_transform in &player_query { - if player_transform.translation.x < level_bounds.max.x - && player_transform.translation.x > level_bounds.min.x - && player_transform.translation.y < level_bounds.max.y - && player_transform.translation.y > level_bounds.min.y - && !level_selection.is_match(&LevelIndices::default(), ldtk_level.data()) - { - *level_selection = LevelSelection::iid(ldtk_level.data().iid.clone()); - } + for (level_iid, level_transform) in &level_query { + let ldtk_project = ldtk_project_assets + .get(ldtk_projects.single()) + .expect("Project should be loaded if level has spawned"); + + let level = ldtk_project + .get_raw_level_by_iid(&level_iid.to_string()) + .expect("Spawned level should exist in LDtk project"); + + let level_bounds = Rect { + min: Vec2::new(level_transform.translation.x, level_transform.translation.y), + max: Vec2::new( + level_transform.translation.x + level.px_wid as f32, + level_transform.translation.y + level.px_hei as f32, + ), + }; + + for player_transform in &player_query { + if player_transform.translation.x < level_bounds.max.x + && player_transform.translation.x > level_bounds.min.x + && player_transform.translation.y < level_bounds.max.y + && player_transform.translation.y > level_bounds.min.y + && !level_selection.is_match(&LevelIndices::default(), level) + { + *level_selection = LevelSelection::iid(level.iid.clone()); } } } @@ -479,7 +491,7 @@ pub fn update_on_ground( pub fn restart_level( mut commands: Commands, - level_query: Query>>, + level_query: Query>, input: Res>, ) { if input.just_pressed(KeyCode::R) { diff --git a/src/assets/ldtk_asset_plugin.rs b/src/assets/ldtk_asset_plugin.rs index a74e8f4c..0978b214 100644 --- a/src/assets/ldtk_asset_plugin.rs +++ b/src/assets/ldtk_asset_plugin.rs @@ -1,6 +1,6 @@ -use crate::assets::{ - ldtk_level::LdtkLevelLoader, ldtk_project::LdtkProjectLoader, LdtkLevel, LdtkProject, -}; +#[cfg(feature = "external_levels")] +use crate::assets::{ldtk_external_level::LdtkExternalLevelLoader, LdtkExternalLevel}; +use crate::assets::{ldtk_project::LdtkProjectLoader, LdtkProject}; use bevy::prelude::*; /// Plugin that registers LDtk-related assets. @@ -10,9 +10,13 @@ pub struct LdtkAssetPlugin; impl Plugin for LdtkAssetPlugin { fn build(&self, app: &mut App) { app.add_asset::() - .init_asset_loader::() - .add_asset::() - .init_asset_loader::() - .register_asset_reflect::(); + .init_asset_loader::(); + + #[cfg(feature = "external_levels")] + { + app.add_asset::() + .init_asset_loader::() + .register_asset_reflect::(); + } } } diff --git a/src/assets/ldtk_external_level.rs b/src/assets/ldtk_external_level.rs new file mode 100644 index 00000000..496c6650 --- /dev/null +++ b/src/assets/ldtk_external_level.rs @@ -0,0 +1,107 @@ +use crate::ldtk::{loaded_level::LoadedLevel, Level}; +use bevy::{ + asset::{AssetLoader, LoadContext, LoadedAsset}, + prelude::*, + reflect::TypeUuid, + utils::BoxedFuture, +}; +use thiserror::Error; + +/// Secondary asset for loading external-levels ldtk files, specific to level data. +/// +/// Loaded as a dependency of the [`LdtkProject`] asset. +/// +/// Requires the `external_levels` feature to be enabled. +/// +/// [`LdtkProject`]: crate::assets::LdtkProject +#[derive(Clone, Debug, PartialEq, TypeUuid, Reflect)] +#[uuid = "5448469b-2134-44f5-a86c-a7b829f70a0c"] +pub struct LdtkExternalLevel { + /// Raw LDtk level data. + data: Level, +} + +impl LdtkExternalLevel { + /// Construct a new [`LdtkExternalLevel`]. + /// + /// Only available for testing. + /// This type should only be constructed via the bevy asset system under normal use. + #[cfg(test)] + pub fn new(data: Level) -> LdtkExternalLevel { + LdtkExternalLevel { data } + } + + /// Internal LDtk level data as a [`LoadedLevel`]. + pub fn data(&self) -> LoadedLevel { + LoadedLevel::try_from(&self.data) + .expect("construction of LdtkExternalLevel should guarantee that the level is loaded.") + } +} + +/// Errors that can occur when loading an [`LdtkExternalLevel`] asset. +#[derive(Debug, Error)] +pub enum LdtkExternalLevelLoaderError { + /// External LDtk level should contain all level data, but some level has null layers. + #[error("external LDtk level should contain all level data, but some level has null layers")] + NullLayers, +} + +/// AssetLoader for [`LdtkExternalLevel`] +#[derive(Default)] +pub struct LdtkExternalLevelLoader; + +impl AssetLoader for LdtkExternalLevelLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<()>> { + Box::pin(async move { + let data: Level = serde_json::from_slice(bytes)?; + + if data.layer_instances.is_none() { + Err(LdtkExternalLevelLoaderError::NullLayers)?; + } + + let ldtk_level = LdtkExternalLevel { data }; + + let loaded_asset = LoadedAsset::new(ldtk_level); + + load_context.set_default_asset(loaded_asset); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["ldtkl"] + } +} + +#[cfg(test)] +mod tests { + use fake::{Fake, Faker}; + + use crate::ldtk::fake::UnloadedLevelFaker; + + use super::*; + + #[test] + fn data_accessor_for_loaded_level_succeeds() { + // default level faker creates a loaded level + let level: Level = Faker.fake(); + + let ldtk_external_level = LdtkExternalLevel::new(level.clone()); + + assert_eq!(ldtk_external_level.data().raw(), &level); + } + + #[test] + #[should_panic] + fn data_accessor_for_unloaded_level_panics() { + let level: Level = UnloadedLevelFaker.fake(); + + let ldtk_external_level = LdtkExternalLevel::new(level.clone()); + + let _should_panic = ldtk_external_level.data(); + } +} diff --git a/src/assets/ldtk_json_with_metadata.rs b/src/assets/ldtk_json_with_metadata.rs index a4bd06b2..0522ba34 100644 --- a/src/assets/ldtk_json_with_metadata.rs +++ b/src/assets/ldtk_json_with_metadata.rs @@ -7,14 +7,13 @@ use crate::{ }; use bevy::reflect::Reflect; use derive_getters::Getters; -use derive_more::Constructor; use std::collections::HashMap; #[cfg(feature = "internal_levels")] use crate::assets::InternalLevels; #[cfg(feature = "external_levels")] -use crate::assets::{ExternalLevels, LdtkLevel}; +use crate::assets::{ExternalLevels, LdtkExternalLevel}; #[cfg(feature = "external_levels")] use bevy::prelude::*; @@ -36,7 +35,7 @@ fn expect_level_loaded(level: &Level) -> LoadedLevel { /// - [external-levels](LdtkJsonWithMetadata#impl-LdtkJsonWithMetadata) /// /// [`LdtkProject`]: crate::assets::LdtkProject -#[derive(Clone, Debug, PartialEq, Constructor, Getters, Reflect)] +#[derive(Clone, Debug, PartialEq, Getters, Reflect)] pub struct LdtkJsonWithMetadata where L: LevelLocale, @@ -47,6 +46,24 @@ where level_map: HashMap, } +impl LdtkJsonWithMetadata +where + L: LevelLocale, +{ + /// Construct a new [`LdtkJsonWithMetadata`]. + /// + /// Only public to the crate to preserve type guarantees about loaded levels. + pub(crate) fn new( + json_data: LdtkJson, + level_map: HashMap, + ) -> LdtkJsonWithMetadata { + LdtkJsonWithMetadata { + json_data, + level_map, + } + } +} + impl RawLevelAccessor for LdtkJsonWithMetadata where L: LevelLocale, @@ -134,12 +151,12 @@ impl LdtkJsonWithMetadata { /// [loaded]: crate::assets::LdtkProject#raw-vs-loaded-levels pub fn iter_external_levels<'a>( &'a self, - external_level_assets: &'a Assets, + external_level_assets: &'a Assets, ) -> impl Iterator> { self.iter_raw_levels() .filter_map(|level| self.level_map.get(&level.iid)) .filter_map(|metadata| external_level_assets.get(metadata.external_handle())) - .map(|level_asset| LoadedLevel::try_from(level_asset.data()).expect("TODO: soon, the loaded-ness of this data will be type guaranteed, but this is not currently the case")) + .map(LdtkExternalLevel::data) } /// Immutable access to an external level at the given [`LevelIndices`]. @@ -149,7 +166,7 @@ impl LdtkJsonWithMetadata { /// [loaded]: crate::assets::LdtkProject#raw-vs-loaded-levels pub fn get_external_level_at_indices<'a>( &'a self, - external_level_assets: &'a Assets, + external_level_assets: &'a Assets, indices: &LevelIndices, ) -> Option> { self.get_external_level_by_iid( @@ -165,13 +182,13 @@ impl LdtkJsonWithMetadata { /// [loaded]: crate::assets::LdtkProject#raw-vs-loaded-levels pub fn get_external_level_by_iid<'a>( &'a self, - external_level_assets: &'a Assets, + external_level_assets: &'a Assets, iid: &String, ) -> Option> { self.level_map() .get(iid) .and_then(|metadata| external_level_assets.get(metadata.external_handle())) - .map(|level_asset| LoadedLevel::try_from(level_asset.data()).expect("TODO: soon, the loaded-ness of this data will be type guaranteed, but this is not currently the case")) + .map(LdtkExternalLevel::data) } /// Find the external level matching the given [`LevelSelection`]. @@ -184,7 +201,7 @@ impl LdtkJsonWithMetadata { /// [loaded]: crate::assets::LdtkProject#raw-vs-loaded-levels pub fn find_external_level_by_level_selection<'a>( &'a self, - external_level_assets: &'a Assets, + external_level_assets: &'a Assets, level_selection: &LevelSelection, ) -> Option> { match level_selection { @@ -205,6 +222,7 @@ impl LdtkJsonWithMetadata { #[cfg(test)] pub mod tests { use super::*; + use derive_more::Constructor; use fake::Dummy; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Constructor)] @@ -455,7 +473,7 @@ pub mod tests { level.iid.clone(), ExternalLevelMetadata::new( LevelMetadata::new(None, LevelIndices::in_root(i)), - Handle::weak(HandleId::random::()), + Handle::weak(HandleId::random::()), ), ) }) @@ -480,7 +498,7 @@ pub mod tests { fn app_setup() -> App { let mut app = App::new(); app.add_plugins(AssetPlugin::default()) - .add_asset::(); + .add_asset::(); app } @@ -494,7 +512,10 @@ pub mod tests { )) .fake(); - let mut assets = app.world.get_resource_mut::>().unwrap(); + let mut assets = app + .world + .get_resource_mut::>() + .unwrap(); let level_map = levels .iter() @@ -504,7 +525,7 @@ pub mod tests { level.iid.clone(), ExternalLevelMetadata::new( LevelMetadata::new(None, LevelIndices::in_root(i)), - assets.add(LdtkLevel::new(level.clone(), None)), + assets.add(LdtkExternalLevel::new(level.clone())), ), ) }) @@ -567,13 +588,21 @@ pub mod tests { assert_eq!( project - .iter_external_levels(app.world.get_resource::>().unwrap()) + .iter_external_levels( + app.world + .get_resource::>() + .unwrap() + ) .count(), project.json_data.levels.len() ); for (external_level, expected_level) in project - .iter_external_levels(app.world.get_resource::>().unwrap()) + .iter_external_levels( + app.world + .get_resource::>() + .unwrap(), + ) .zip(project.json_data.levels.iter()) { assert_eq!(external_level.iid(), &expected_level.iid) @@ -585,7 +614,10 @@ pub mod tests { let mut app = app_setup(); let project = fake_and_load_ldtk_json_with_metadata(&mut app); - let assets = app.world.get_resource::>().unwrap(); + let assets = app + .world + .get_resource::>() + .unwrap(); for (i, expected_level) in project.json_data.levels.iter().enumerate() { assert_eq!( @@ -612,7 +644,10 @@ pub mod tests { let mut app = app_setup(); let project = fake_and_load_ldtk_json_with_metadata(&mut app); - let assets = app.world.get_resource::>().unwrap(); + let assets = app + .world + .get_resource::>() + .unwrap(); for expected_level in &project.json_data.levels { assert_eq!( @@ -638,7 +673,10 @@ pub mod tests { let mut app = app_setup(); let project = fake_and_load_ldtk_json_with_metadata(&mut app); - let assets = app.world.get_resource::>().unwrap(); + let assets = app + .world + .get_resource::>() + .unwrap(); for (i, expected_level) in project.json_data.levels.iter().enumerate() { assert_eq!( diff --git a/src/assets/ldtk_level.rs b/src/assets/ldtk_level.rs deleted file mode 100644 index ce327849..00000000 --- a/src/assets/ldtk_level.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::{assets::ldtk_path_to_asset_path, ldtk::Level}; -use bevy::{ - asset::{AssetLoader, LoadContext, LoadedAsset}, - prelude::*, - reflect::TypeUuid, - utils::BoxedFuture, -}; -use derive_getters::Getters; - -/// Secondary asset for loading ldtk files, specific to level data. -/// -/// Loaded as a labeled asset when loading a standalone ldtk file with [`LdtkProject`]. -/// The label is just the level's identifier. -/// -/// Loaded as a dependency to the [`LdtkProject`] when loading an ldtk file with external levels. -/// -/// [`LdtkProject`]: crate::assets::LdtkProject -#[derive(Clone, Debug, PartialEq, TypeUuid, Getters, Reflect)] -#[uuid = "5448469b-2134-44f5-a86c-a7b829f70a0c"] -pub struct LdtkLevel { - /// Raw ldtk level data. - data: Level, - /// Handle for the background image of this level. - background_image: Option>, -} - -impl LdtkLevel { - /// Construct a new [`LdtkLevel`]. - pub fn new(data: Level, background_image: Option>) -> LdtkLevel { - LdtkLevel { - data, - background_image, - } - } -} - -#[derive(Default)] -pub struct LdtkLevelLoader; - -impl AssetLoader for LdtkLevelLoader { - fn load<'a>( - &'a self, - bytes: &'a [u8], - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, anyhow::Result<()>> { - Box::pin(async move { - let data: Level = serde_json::from_slice(bytes)?; - - let mut background_asset_path = None; - let mut background_image = None; - if let Some(rel_path) = &data.bg_rel_path { - let asset_path = - ldtk_path_to_asset_path(load_context.path().parent().unwrap(), rel_path); - background_asset_path = Some(asset_path.clone()); - background_image = Some(load_context.get_handle(asset_path)); - } - - let ldtk_level = LdtkLevel { - data, - background_image, - }; - - let mut loaded_asset = LoadedAsset::new(ldtk_level); - - if let Some(asset_path) = background_asset_path { - loaded_asset = loaded_asset.with_dependency(asset_path); - } - - load_context.set_default_asset(loaded_asset); - Ok(()) - }) - } - - fn extensions(&self) -> &[&str] { - &["ldtkl"] - } -} diff --git a/src/assets/ldtk_project.rs b/src/assets/ldtk_project.rs index 4c86b443..dd842d0e 100644 --- a/src/assets/ldtk_project.rs +++ b/src/assets/ldtk_project.rs @@ -1,36 +1,134 @@ +use std::path::Path; + use crate::{ - assets::{ldtk_path_to_asset_path, LdtkLevel}, + assets::{ + LdtkJsonWithMetadata, LdtkProjectData, LevelIndices, LevelMetadata, LevelMetadataAccessor, + }, ldtk::{raw_level_accessor::RawLevelAccessor, LdtkJson, Level}, - resources::LevelSelection, }; use bevy::{ - asset::{AssetLoader, LoadContext, LoadedAsset}, + asset::{AssetLoader, AssetPath, LoadContext, LoadedAsset}, prelude::*, - reflect::{TypePath, TypeUuid}, + reflect::{Reflect, TypeUuid}, utils::BoxedFuture, }; use derive_getters::Getters; +use derive_more::From; use std::collections::HashMap; +use thiserror::Error; + +#[cfg(feature = "internal_levels")] +use crate::assets::InternalLevels; -/// Main asset for loading ldtk files. +#[cfg(feature = "external_levels")] +use crate::assets::{ExternalLevelMetadata, ExternalLevels}; + +fn ldtk_path_to_asset_path<'b>(ldtk_path: &Path, rel_path: &str) -> AssetPath<'b> { + ldtk_path.parent().unwrap().join(Path::new(rel_path)).into() +} + +/// Main asset for loading LDtk project data. +/// +/// # Accessing level data +/// This type provides many methods for accessing level data. +/// The correct method for you will vary depending on whether or not you need "complete" level +/// data, and if so, whether or not your project uses internal levels or external levels. +/// +/// ## Raw vs loaded levels +/// There are a couple main flavors that level data can have - raw and loaded. /// -/// Load your ldtk project with the asset server, then insert the handle into the -/// [`LdtkWorldBundle`]. +/// Raw levels don't have any type guarantee that the level data is complete or incomplete. +/// Level data may be incomplete and contain no layer instances if external levels are enabled. +/// However, even in this case, a raw level is sufficient if you don't need any layer data. +/// Raw levels are represented by the [`Level`] type from LDtk. +/// See [`RawLevelAccessor`] and [`LevelMetadataAccessor`] for some methods that access raw levels. /// -/// [`LdtkWorldBundle`]: crate::components::LdtkWorldBundle -#[derive(Clone, Debug, PartialEq, TypeUuid, TypePath, Getters)] -#[uuid = "ecfb87b7-9cd9-4970-8482-f2f68b770d31"] +/// On the other hand, loaded levels are type-guaranteed to have complete level data. +/// Loaded levels are represented by the [`LoadedLevel`] type. +/// Methods for accessing loaded levels vary depending on if the levels are internal or external. +/// +/// ## Accessing internal and external loaded levels +/// By default, LDtk stores level data inside the main project file. +/// You have the option to store level data externally, where each level gets its own file. +/// In this case, some of the level data remains available in the project file, but not layer data. +/// See the [previous section](LdtkProject#raw-vs-loaded-levels) for more details. +/// +/// Level data stored so differently on disk results in a similar difference when loaded in memory. +/// In the external case, an entirely different asset type [`LdtkExternalLevel`] comes into play. +/// So, methods for accessing loaded levels vary between the two cases. +/// +/// If you know that your project uses internal levels, you can coerce it as a "standalone project". +/// To do this, use [`LdtkProject::as_standalone`]. +/// With that, you can use these [`loaded_level` accessors]. +/// +/// If you know that your project uses external levels, you can coerce it as a "parent project". +/// To do this, use [`LdtkProject::as_parent`]. +/// You will also need the [`LdtkExternalLevel`] asset collection. +/// With these, you can use these [`external_level` accessors]. +/// +/// [`LoadedLevel`]: crate::ldtk::loaded_level::LoadedLevel +/// [`LdtkExternalLevel`]: crate::assets::LdtkExternalLevel +/// [`loaded_level` accessors]: LdtkJsonWithMetadata#impl-LdtkJsonWithMetadata +/// [`external_level` accessors]: LdtkJsonWithMetadata#impl-LdtkJsonWithMetadata +#[derive(Clone, Debug, PartialEq, From, TypeUuid, Getters, Reflect)] +#[uuid = "43571891-8570-4416-903f-582efe3426ac"] pub struct LdtkProject { - /// Raw ldtk project data. - data: LdtkJson, + /// LDtk json data and level metadata. + data: LdtkProjectData, /// Map from tileset uids to image handles for the loaded tileset. tileset_map: HashMap>, - /// Map from level iids to level handles. - level_map: HashMap>, /// Image used for rendering int grid colors. int_grid_image_handle: Option>, } +impl LdtkProject { + /// Construct a new [`LdtkProject`]. + /// + /// Private to preserve type guarantees about loaded levels. + fn new( + data: LdtkProjectData, + tileset_map: HashMap>, + int_grid_image_handle: Option>, + ) -> LdtkProject { + LdtkProject { + data, + tileset_map, + int_grid_image_handle, + } + } + + /// Raw ldtk json data. + pub fn json_data(&self) -> &LdtkJson { + self.data.json_data() + } + + /// Unwrap as a [`LdtkJsonWithMetadata`]. + /// For use on internal-levels ldtk projects only. + /// + /// # Panics + /// Panics if `self.data()` is not [`LdtkProjectData::Standalone`]. + /// This shouldn't occur if the project uses internal levels. + /// + /// [`LdtkJsonWithMetadata`]: LdtkJsonWithMetadata + #[cfg(feature = "internal_levels")] + pub fn as_standalone(&self) -> &LdtkJsonWithMetadata { + self.data.as_standalone() + } + + /// Unwrap as a [`LdtkJsonWithMetadata`]. + /// For use on external-levels ldtk projects only. + /// + /// # Panics + /// Panics if `self.data()` is not [`LdtkProjectData::Parent`]. + /// This shouldn't occur if the project uses external levels. + /// + /// [`LdtkJsonWithMetadata`]: LdtkJsonWithMetadata + #[cfg(feature = "external_levels")] + pub fn as_parent(&self) -> &LdtkJsonWithMetadata { + self.data.as_parent() + } +} + impl RawLevelAccessor for LdtkProject { fn worlds(&self) -> &[crate::ldtk::World] { self.data.worlds() @@ -41,22 +139,98 @@ impl RawLevelAccessor for LdtkProject { } } -impl LdtkProject { - /// Find a particular level using a [`LevelSelection`]. - /// - /// Note: the returned level is the one existent in the [`LdtkProject`]. - /// This level will have "incomplete" data if you use LDtk's external levels feature. - /// To always get full level data, you'll need to access `Assets`. - pub fn get_level(&self, level_selection: &LevelSelection) -> Option<&Level> { - self.iter_raw_levels_with_indices() - .find(|(i, l)| level_selection.is_match(i, l)) - .map(|(_, l)| l) +impl LevelMetadataAccessor for LdtkProject { + fn get_level_metadata_by_iid(&self, iid: &String) -> Option<&LevelMetadata> { + self.data.get_level_metadata_by_iid(iid) } } +/// Errors that can occur when loading an [`LdtkProject`] asset. +#[allow(dead_code)] +#[derive(Debug, Error)] +pub enum LdtkProjectLoaderError { + /// LDtk project uses internal levels, but the `internal_levels` feature is disabled. + #[error("LDtk project uses internal levels, but the internal_levels feature is disabled")] + InternalLevelsDisabled, + /// LDtk project uses external levels, but the `external_levels` feature is disabled. + #[error("LDtk project uses external levels, but the external_levels feature is disabled")] + ExternalLevelsDisabled, + /// LDtk project uses internal levels, but some level's `layer_instances` is null. + #[error("LDtk project uses internal levels, but some level's layer_instances is null")] + InternalLevelWithNullLayers, + /// LDtk project uses external levels, but some level's `external_rel_path` is null. + #[error("LDtk project uses external levels, but some level's external_rel_path is null")] + ExternalLevelWithNullPath, +} + +/// AssetLoader for [`LdtkProject`]. #[derive(Default)] pub struct LdtkProjectLoader; +struct LoadLevelMetadataResult<'a, L> { + dependent_asset_paths: Vec>, + level_metadata: L, +} + +fn load_level_metadata<'a>( + load_context: &LoadContext, + level_indices: LevelIndices, + level: &Level, + expect_level_loaded: bool, +) -> Result, LdtkProjectLoaderError> { + let (bg_image_path, bg_image) = level + .bg_rel_path + .as_ref() + .map(|rel_path| { + let asset_path = ldtk_path_to_asset_path(load_context.path(), rel_path); + + ( + Some(asset_path.clone()), + Some(load_context.get_handle(asset_path)), + ) + }) + .unwrap_or((None, None)); + + if expect_level_loaded && level.layer_instances.is_none() { + Err(LdtkProjectLoaderError::InternalLevelWithNullLayers)?; + } + + let level_metadata = LevelMetadata::new(bg_image, level_indices); + + Ok(LoadLevelMetadataResult { + dependent_asset_paths: bg_image_path.into_iter().collect(), + level_metadata, + }) +} + +#[cfg(feature = "external_levels")] +fn load_external_level_metadata<'a>( + load_context: &LoadContext, + level_indices: LevelIndices, + level: &Level, +) -> Result, LdtkProjectLoaderError> { + let LoadLevelMetadataResult { + level_metadata, + mut dependent_asset_paths, + } = load_level_metadata(load_context, level_indices, level, false)?; + + let external_level_path = ldtk_path_to_asset_path( + load_context.path(), + level + .external_rel_path + .as_ref() + .ok_or(LdtkProjectLoaderError::ExternalLevelWithNullPath)?, + ); + + let external_handle = load_context.get_handle(external_level_path.clone()); + dependent_asset_paths.push(external_level_path); + + Ok(LoadLevelMetadataResult { + level_metadata: ExternalLevelMetadata::new(level_metadata, external_handle), + dependent_asset_paths, + }) +} + impl AssetLoader for LdtkProjectLoader { fn load<'a>( &'a self, @@ -66,45 +240,14 @@ impl AssetLoader for LdtkProjectLoader { Box::pin(async move { let data: LdtkJson = serde_json::from_slice(bytes)?; - let mut external_level_paths = Vec::new(); - let mut level_map = HashMap::new(); - let mut background_images = Vec::new(); - if data.external_levels { - for level in data.iter_raw_levels() { - if let Some(external_rel_path) = &level.external_rel_path { - let asset_path = - ldtk_path_to_asset_path(load_context.path(), external_rel_path); - - external_level_paths.push(asset_path.clone()); - level_map.insert(level.iid.clone(), load_context.get_handle(asset_path)); - } - } - } else { - for level in data.iter_raw_levels() { - let label = level.identifier.as_ref(); - - let mut background_image = None; - if let Some(rel_path) = &level.bg_rel_path { - let asset_path = ldtk_path_to_asset_path(load_context.path(), rel_path); - background_images.push(asset_path.clone()); - background_image = Some(load_context.get_handle(asset_path)); - } - - let ldtk_level = LdtkLevel::new(level.clone(), background_image); - let level_handle = - load_context.set_labeled_asset(label, LoadedAsset::new(ldtk_level)); - - level_map.insert(level.iid.clone(), level_handle); - } - } + let mut dependent_asset_paths = Vec::new(); - let mut tileset_rel_paths = Vec::new(); - let mut tileset_map = HashMap::new(); + let mut tileset_map: HashMap> = HashMap::new(); for tileset in &data.defs.tilesets { if let Some(tileset_path) = &tileset.rel_path { let asset_path = ldtk_path_to_asset_path(load_context.path(), tileset_path); - tileset_rel_paths.push(asset_path.clone()); + dependent_asset_paths.push(asset_path.clone()); tileset_map.insert(tileset.uid, load_context.get_handle(asset_path)); } else if tileset.embed_atlas.is_some() { warn!("Ignoring LDtk's Internal_Icons. They cannot be displayed due to their license."); @@ -118,17 +261,62 @@ impl AssetLoader for LdtkProjectLoader { load_context.set_labeled_asset("int_grid_image", LoadedAsset::new(image)) }); - let ldtk_asset = LdtkProject { - data, - tileset_map, - level_map, - int_grid_image_handle, + let ldtk_project = if data.external_levels { + #[cfg(feature = "external_levels")] + { + let mut level_map = HashMap::new(); + + for (level_indices, level) in data.iter_raw_levels_with_indices() { + let LoadLevelMetadataResult { + level_metadata, + dependent_asset_paths: new_asset_paths, + } = load_external_level_metadata(load_context, level_indices, level)?; + + level_map.insert(level.iid.clone(), level_metadata); + dependent_asset_paths.extend(new_asset_paths); + } + + LdtkProject::new( + LdtkProjectData::Parent(LdtkJsonWithMetadata::new(data, level_map)), + tileset_map, + int_grid_image_handle, + ) + } + + #[cfg(not(feature = "external_levels"))] + { + Err(LdtkProjectLoaderError::ExternalLevelsDisabled)? + } + } else { + #[cfg(feature = "internal_levels")] + { + let mut level_map = HashMap::new(); + + for (level_indices, level) in data.iter_raw_levels_with_indices() { + let LoadLevelMetadataResult { + level_metadata, + dependent_asset_paths: new_asset_paths, + } = load_level_metadata(load_context, level_indices, level, true)?; + + level_map.insert(level.iid.clone(), level_metadata); + dependent_asset_paths.extend(new_asset_paths); + } + + LdtkProject::new( + LdtkProjectData::Standalone(LdtkJsonWithMetadata::new(data, level_map)), + tileset_map, + int_grid_image_handle, + ) + } + + #[cfg(not(feature = "internal_levels"))] + { + Err(LdtkProjectLoaderError::InternalLevelsDisabled)? + } }; + load_context.set_default_asset( - LoadedAsset::new(ldtk_asset) - .with_dependencies(tileset_rel_paths) - .with_dependencies(external_level_paths) - .with_dependencies(background_images), + LoadedAsset::new(ldtk_project).with_dependencies(dependent_asset_paths), ); Ok(()) }) @@ -141,25 +329,159 @@ impl AssetLoader for LdtkProjectLoader { #[cfg(test)] mod tests { - use fake::Fake; + use super::*; + use bevy::asset::HandleId; + use derive_more::Constructor; + use fake::{Dummy, Fake}; + use rand::Rng; - use crate::ldtk::fake::{LoadedLevelsFaker, MixedLevelsLdtkJsonFaker}; + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Constructor)] + pub struct LdtkProjectFaker + where + LdtkProjectData: Dummy, + { + ldtk_project_data_faker: F, + } - use super::*; + impl Dummy> for LdtkProject + where + LdtkProjectData: Dummy, + { + fn dummy_with_rng(config: &LdtkProjectFaker, rng: &mut R) -> Self { + let data: LdtkProjectData = config.ldtk_project_data_faker.fake_with_rng(rng); + let tileset_map = data + .json_data() + .defs + .tilesets + .iter() + .map(|tileset| (tileset.uid, Handle::weak(HandleId::random::()))) + .collect(); - #[test] - fn raw_level_accessor_implementation_is_transparent() { - let data: LdtkJson = - MixedLevelsLdtkJsonFaker::new(LoadedLevelsFaker::default(), 4..8).fake(); + LdtkProject { + data, + tileset_map, + int_grid_image_handle: Some(Handle::weak(HandleId::random::())), + } + } + } - let project = LdtkProject { - data: data.clone(), - tileset_map: HashMap::default(), - level_map: HashMap::default(), - int_grid_image_handle: None, + #[cfg(feature = "internal_levels")] + mod internal_levels { + use crate::{ + assets::{ + ldtk_json_with_metadata::tests::LdtkJsonWithMetadataFaker, + ldtk_project_data::internal_level_tests::StandaloneLdtkProjectDataFaker, + }, + ldtk::fake::{LoadedLevelsFaker, MixedLevelsLdtkJsonFaker}, }; - assert_eq!(project.root_levels(), data.root_levels()); - assert_eq!(project.worlds(), data.worlds()); + use super::*; + + impl Dummy for LdtkProject { + fn dummy_with_rng(_: &InternalLevels, rng: &mut R) -> Self { + LdtkProjectFaker { + ldtk_project_data_faker: InternalLevels, + } + .fake_with_rng(rng) + } + } + + #[test] + fn json_data_accessor_is_transparent() { + let project: LdtkProject = InternalLevels.fake(); + + assert_eq!(project.json_data(), project.data().json_data()); + } + + #[test] + fn raw_level_accessor_implementation_is_transparent() { + let project: LdtkProject = LdtkProjectFaker::new(StandaloneLdtkProjectDataFaker::new( + LdtkJsonWithMetadataFaker::new(MixedLevelsLdtkJsonFaker::new( + LoadedLevelsFaker::default(), + 4..8, + )), + )) + .fake(); + + assert_eq!(project.root_levels(), project.json_data().root_levels()); + assert_eq!(project.worlds(), project.json_data().worlds()); + } + + #[test] + fn level_metadata_accessor_implementation_is_transparent() { + let project: LdtkProject = InternalLevels.fake(); + + for level in &project.json_data().levels { + assert_eq!( + project.get_level_metadata_by_iid(&level.iid), + project.data().get_level_metadata_by_iid(&level.iid), + ); + } + + assert_eq!( + project.get_level_metadata_by_iid(&"This_level_doesnt_exist".to_string()), + None + ); + } + } + + #[cfg(feature = "external_levels")] + mod external_levels { + use crate::{ + assets::{ + ldtk_json_with_metadata::tests::LdtkJsonWithMetadataFaker, + ldtk_project_data::external_level_tests::ParentLdtkProjectDataFaker, + }, + ldtk::fake::{LoadedLevelsFaker, MixedLevelsLdtkJsonFaker}, + }; + + use super::*; + + impl Dummy for LdtkProject { + fn dummy_with_rng(_: &ExternalLevels, rng: &mut R) -> Self { + LdtkProjectFaker { + ldtk_project_data_faker: ExternalLevels, + } + .fake_with_rng(rng) + } + } + + #[test] + fn json_data_accessor_is_transparent() { + let project: LdtkProject = ExternalLevels.fake(); + + assert_eq!(project.json_data(), project.data().json_data()); + } + + #[test] + fn raw_level_accessor_implementation_is_transparent() { + let project: LdtkProject = LdtkProjectFaker::new(ParentLdtkProjectDataFaker::new( + LdtkJsonWithMetadataFaker::new(MixedLevelsLdtkJsonFaker::new( + LoadedLevelsFaker::default(), + 4..8, + )), + )) + .fake(); + + assert_eq!(project.root_levels(), project.json_data().root_levels()); + assert_eq!(project.worlds(), project.json_data().worlds()); + } + + #[test] + fn level_metadata_accessor_implementation_is_transparent() { + let project: LdtkProject = ExternalLevels.fake(); + + for level in &project.json_data().levels { + assert_eq!( + project.get_level_metadata_by_iid(&level.iid), + project.data().get_level_metadata_by_iid(&level.iid), + ); + } + + assert_eq!( + project.get_level_metadata_by_iid(&"This_level_doesnt_exist".to_string()), + None + ); + } } } diff --git a/src/assets/level_metadata.rs b/src/assets/level_metadata.rs index ae454e82..530748dc 100644 --- a/src/assets/level_metadata.rs +++ b/src/assets/level_metadata.rs @@ -3,7 +3,7 @@ use bevy::{prelude::*, reflect::Reflect}; use derive_getters::Getters; #[cfg(feature = "external_levels")] -use crate::assets::LdtkLevel; +use crate::assets::LdtkExternalLevel; /// Metadata produced for every level during [`LdtkProject`] loading. /// @@ -32,13 +32,13 @@ pub struct ExternalLevelMetadata { /// Common metadata for this level. metadata: LevelMetadata, /// Handle to this external level's asset data. - external_handle: Handle, + external_handle: Handle, } #[cfg(feature = "external_levels")] impl ExternalLevelMetadata { /// Construct a new [`ExternalLevelMetadata`]. - pub fn new(metadata: LevelMetadata, external_handle: Handle) -> Self { + pub fn new(metadata: LevelMetadata, external_handle: Handle) -> Self { ExternalLevelMetadata { metadata, external_handle, diff --git a/src/assets/mod.rs b/src/assets/mod.rs index c1fcb177..9cfe849c 100644 --- a/src/assets/mod.rs +++ b/src/assets/mod.rs @@ -1,13 +1,9 @@ -//! Assets for loading ldtk files. - -use bevy::asset::AssetPath; -use std::path::Path; +//! Assets and related items for loading LDtk files. mod ldtk_asset_plugin; pub use ldtk_asset_plugin::LdtkAssetPlugin; mod level_metadata; - pub use level_metadata::LevelMetadata; #[cfg(feature = "external_levels")] @@ -24,8 +20,11 @@ pub use level_locale::ExternalLevels; mod level_metadata_accessor; pub use level_metadata_accessor::LevelMetadataAccessor; -mod ldtk_level; -pub use ldtk_level::LdtkLevel; +#[cfg(feature = "external_levels")] +mod ldtk_external_level; + +#[cfg(feature = "external_levels")] +pub use ldtk_external_level::LdtkExternalLevel; mod ldtk_json_with_metadata; pub use ldtk_json_with_metadata::LdtkJsonWithMetadata; @@ -38,7 +37,3 @@ pub use ldtk_project::LdtkProject; mod level_indices; pub use level_indices::LevelIndices; - -fn ldtk_path_to_asset_path<'b>(ldtk_path: &Path, rel_path: &str) -> AssetPath<'b> { - ldtk_path.parent().unwrap().join(Path::new(rel_path)).into() -} diff --git a/src/components/mod.rs b/src/components/mod.rs index bb357f05..6842d19f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -11,6 +11,7 @@ pub use level_set::LevelSet; pub use crate::ldtk::EntityInstance; use crate::{ ldtk::{LayerInstance, Type}, + prelude::LdtkProject, utils::ldtk_grid_coords_to_grid_coords, }; use bevy::prelude::*; @@ -19,7 +20,6 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; #[allow(unused_imports)] use crate::{ - assets::LdtkLevel, prelude::{LdtkEntity, LdtkIntCell}, resources::LevelSelection, }; @@ -307,10 +307,12 @@ impl From<&LayerInstance> for LayerMetadata { /// [Component] that indicates that an LDtk level or world should respawn. /// -/// Inserting this component on an entity with either `Handle` or `Handle` +/// Inserting this component on an entity with either [`Handle`] or [`LevelIid`] /// components will cause it to respawn. /// This can be used to implement a simple level-restart feature. /// Internally, this is used to support the entire level spawning process +/// +/// [`Handle`]: crate::assets::LdtkProject #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Hash, Component, Reflect)] #[reflect(Component)] pub struct Respawn; @@ -331,23 +333,23 @@ pub(crate) struct EntityInstanceBundle { pub entity_instance: EntityInstance, } -/// [Bundle] for spawning LDtk worlds and their levels. The main bundle for using this plugin. +/// `Bundle` for spawning LDtk worlds and their levels. The main bundle for using this plugin. /// -/// After the ldtk file is done loading, the levels you've chosen with [LevelSelection] or -/// [LevelSet] will begin to spawn. -/// Each level is its own entity, with the [LdtkWorldBundle] as its parent. -/// Each level has a `Handle` component. +/// After the ldtk file is done loading, the levels you've chosen with [`LevelSelection`] or +/// [`LevelSet`] will begin to spawn. +/// Each level is its own entity, with the [`LdtkWorldBundle`] as its parent. +/// Each level has a [`LevelIid`] component. /// /// All non-Entity layers (IntGrid, Tile, and AutoTile) will also spawn as their own entities. /// Each layer's parent will be the level entity. -/// Each layer will have a [LayerMetadata] component, and are [bevy_ecs_tilemap::TilemapBundle]s. +/// Each layer will have a [`LayerMetadata`] component, and are bevy_ecs_tilemap TileMaps. /// Each tile in these layers will have the layer entity as its parent. /// /// For Entity layers, all LDtk entities in the level are spawned as children to the level entity, -/// unless marked by a [Worldly] component. +/// unless marked by a [`Worldly`] component. #[derive(Clone, Default, Bundle)] pub struct LdtkWorldBundle { - pub ldtk_handle: Handle, + pub ldtk_handle: Handle, pub level_set: LevelSet, pub transform: Transform, pub global_transform: GlobalTransform, diff --git a/src/ldtk/loaded_level.rs b/src/ldtk/loaded_level.rs index f896cf05..0da5a729 100644 --- a/src/ldtk/loaded_level.rs +++ b/src/ldtk/loaded_level.rs @@ -17,9 +17,11 @@ pub struct LevelNotLoaded; /// In particular, the main project file will have levels whose `layer_instances` field is null. /// The complete data for these levels will exist in a separate file. /// -/// Can be constructed via [`LoadedLevel::try_from`]. -/// This construction verifies that the `layer_instances` are not null. +/// Can be constructed via [`LoadedLevel::try_from`], or accessed via the [asset types]. +/// The construction verifies that the `layer_instances` are not null. /// As a result, the [`LoadedLevel::layer_instances`] accessor expects the `Option` away. +/// +/// [asset types]: crate::assets::LdtkProject#accessing-internal-and-external-loaded-levels #[derive(Copy, Clone, Debug, PartialEq)] pub struct LoadedLevel<'a> { level: &'a Level, diff --git a/src/level.rs b/src/level.rs index 9355a7cd..7d38afb6 100644 --- a/src/level.rs +++ b/src/level.rs @@ -5,11 +5,10 @@ use crate::{ LdtkEntity, LdtkEntityMap, LdtkIntCellMap, PhantomLdtkEntity, PhantomLdtkEntityTrait, PhantomLdtkIntCell, PhantomLdtkIntCellTrait, }, - assets::LdtkLevel, components::*, ldtk::{ - EntityDefinition, EnumTagValue, LayerDefinition, LayerInstance, LevelBackgroundPosition, - TileCustomMetadata, TileInstance, TilesetDefinition, Type, + loaded_level::LoadedLevel, EntityDefinition, EnumTagValue, LayerDefinition, LayerInstance, + LevelBackgroundPosition, TileCustomMetadata, TileInstance, TilesetDefinition, Type, }, resources::{IntGridRendering, LdtkSettings, LevelBackground}, tile_makers::*, @@ -206,7 +205,8 @@ fn tile_in_layer_bounds(tile: &TileInstance, layer_instance: &LayerInstance) -> #[allow(clippy::too_many_arguments)] pub fn spawn_level( - ldtk_level: &LdtkLevel, + level: LoadedLevel, + background_image: &Option>, commands: &mut Commands, asset_server: &AssetServer, images: &Assets, @@ -222,77 +222,104 @@ pub fn spawn_level( ldtk_entity: Entity, ldtk_settings: &LdtkSettings, ) { - let level = ldtk_level.data(); + let layer_instances = level.layer_instances(); - if let Some(layer_instances) = &level.layer_instances { - let mut layer_z = 0; + let mut layer_z = 0; - if ldtk_settings.level_background == LevelBackground::Rendered { - let translation = Vec3::new(level.px_wid as f32, level.px_hei as f32, 0.) / 2.; + if ldtk_settings.level_background == LevelBackground::Rendered { + let translation = Vec3::new(*level.px_wid() as f32, *level.px_hei() as f32, 0.) / 2.; - let background_entity = commands - .spawn(SpriteBundle { - sprite: Sprite { - color: level.bg_color, - custom_size: Some(Vec2::new(level.px_wid as f32, level.px_hei as f32)), - ..default() - }, - transform: Transform::from_translation(translation), + let background_entity = commands + .spawn(SpriteBundle { + sprite: Sprite { + color: *level.bg_color(), + custom_size: Some(Vec2::new(*level.px_wid() as f32, *level.px_hei() as f32)), ..default() - }) - .id(); - - commands.entity(ldtk_entity).add_child(background_entity); - - layer_z += 1; - - // Spawn background image - if let (Some(background_image_handle), Some(background_position)) = - (ldtk_level.background_image(), &level.bg_pos) - { - match background_image_sprite_sheet_bundle( - images, - texture_atlases, - background_image_handle, - background_position, - level.px_hei, - layer_z as f32, - ) { - Ok(sprite_sheet_bundle) => { - commands.entity(ldtk_entity).with_children(|parent| { - parent.spawn(sprite_sheet_bundle); - }); - - layer_z += 1; - } - Err(e) => warn!("{}", e), + }, + transform: Transform::from_translation(translation), + ..default() + }) + .id(); + + commands.entity(ldtk_entity).add_child(background_entity); + + layer_z += 1; + + // Spawn background image + if let (Some(background_image_handle), Some(background_position)) = + (background_image, level.bg_pos()) + { + match background_image_sprite_sheet_bundle( + images, + texture_atlases, + background_image_handle, + background_position, + *level.px_hei(), + layer_z as f32, + ) { + Ok(sprite_sheet_bundle) => { + commands.entity(ldtk_entity).with_children(|parent| { + parent.spawn(sprite_sheet_bundle); + }); + + layer_z += 1; } + Err(e) => warn!("{}", e), } } + } - for layer_instance in layer_instances.iter().rev() { - match layer_instance.layer_instance_type { - Type::Entities => { - commands.entity(ldtk_entity).with_children(|commands| { - for entity_instance in &layer_instance.entity_instances { - let transform = calculate_transform_from_entity_instance( - entity_instance, - entity_definition_map, - level.px_hei, - layer_z as f32, - ); - // Note: entities do not seem to be affected visually by layer offsets in - // the editor, so no layer offset is added to the transform here. + for layer_instance in layer_instances.iter().rev() { + match layer_instance.layer_instance_type { + Type::Entities => { + commands.entity(ldtk_entity).with_children(|commands| { + for entity_instance in &layer_instance.entity_instances { + let transform = calculate_transform_from_entity_instance( + entity_instance, + entity_definition_map, + *level.px_hei(), + layer_z as f32, + ); + // Note: entities do not seem to be affected visually by layer offsets in + // the editor, so no layer offset is added to the transform here. + + let (tileset, tileset_definition) = match &entity_instance.tile { + Some(t) => ( + tileset_map.get(&t.tileset_uid), + tileset_definition_map.get(&t.tileset_uid).copied(), + ), + None => (None, None), + }; - let (tileset, tileset_definition) = match &entity_instance.tile { - Some(t) => ( - tileset_map.get(&t.tileset_uid), - tileset_definition_map.get(&t.tileset_uid).copied(), - ), - None => (None, None), - }; + let predicted_worldly = Worldly::bundle_entity( + entity_instance, + layer_instance, + tileset, + tileset_definition, + asset_server, + texture_atlases, + ); - let predicted_worldly = Worldly::bundle_entity( + if !worldly_set.contains(&predicted_worldly) { + let default_ldtk_entity: Box = + Box::new(PhantomLdtkEntity::::new()); + let mut entity_commands = commands.spawn_empty(); + + // insert Name before evaluating LdtkEntitys so that user-provided + // names aren't overwritten + entity_commands.insert(( + EntityIid::new(entity_instance.iid.to_owned()), + Name::new(entity_instance.identifier.to_owned()), + )); + + ldtk_map_get_or_default( + layer_instance.identifier.clone(), + entity_instance.identifier.clone(), + &default_ldtk_entity, + ldtk_entity_map, + ) + .evaluate( + &mut entity_commands, entity_instance, layer_instance, tileset, @@ -301,385 +328,353 @@ pub fn spawn_level( texture_atlases, ); - if !worldly_set.contains(&predicted_worldly) { - let default_ldtk_entity: Box = - Box::new(PhantomLdtkEntity::::new()); - let mut entity_commands = commands.spawn_empty(); - - // insert Name before evaluating LdtkEntitys so that user-provided - // names aren't overwritten - entity_commands.insert(( - EntityIid::new(entity_instance.iid.to_owned()), - Name::new(entity_instance.identifier.to_owned()), - )); - - ldtk_map_get_or_default( - layer_instance.identifier.clone(), - entity_instance.identifier.clone(), - &default_ldtk_entity, - ldtk_entity_map, - ) - .evaluate( - &mut entity_commands, - entity_instance, - layer_instance, - tileset, - tileset_definition, - asset_server, - texture_atlases, - ); - - entity_commands.insert(SpatialBundle { - transform, - ..default() - }); - } + entity_commands.insert(SpatialBundle { + transform, + ..default() + }); } - }); - layer_z += 1; - } - _ => { - // The remaining layers have a lot of shared code. - // This is because: - // 1. There is virtually no difference between AutoTile and Tile layers - // 2. IntGrid layers can sometimes have AutoTile functionality - - let size = TilemapSize { - x: layer_instance.c_wid as u32, - y: layer_instance.c_hei as u32, - }; - - let tileset_definition = layer_instance - .tileset_def_uid - .map(|u| tileset_definition_map.get(&u).unwrap()); - - let tile_size = match tileset_definition { - Some(tileset_definition) => TilemapTileSize { - x: tileset_definition.tile_grid_size as f32, - y: tileset_definition.tile_grid_size as f32, - }, - None => TilemapTileSize { - x: layer_instance.grid_size as f32, - y: layer_instance.grid_size as f32, - }, - }; + } + }); + layer_z += 1; + } + _ => { + // The remaining layers have a lot of shared code. + // This is because: + // 1. There is virtually no difference between AutoTile and Tile layers + // 2. IntGrid layers can sometimes have AutoTile functionality + + let size = TilemapSize { + x: layer_instance.c_wid as u32, + y: layer_instance.c_hei as u32, + }; + + let tileset_definition = layer_instance + .tileset_def_uid + .map(|u| tileset_definition_map.get(&u).unwrap()); + + let tile_size = match tileset_definition { + Some(tileset_definition) => TilemapTileSize { + x: tileset_definition.tile_grid_size as f32, + y: tileset_definition.tile_grid_size as f32, + }, + None => TilemapTileSize { + x: layer_instance.grid_size as f32, + y: layer_instance.grid_size as f32, + }, + }; - let grid_size = match tileset_definition { - Some(_) => TilemapGridSize { - x: layer_instance.grid_size as f32, - y: layer_instance.grid_size as f32, - }, - None => TilemapGridSize { - x: tile_size.x, - y: tile_size.y, - }, - }; + let grid_size = match tileset_definition { + Some(_) => TilemapGridSize { + x: layer_instance.grid_size as f32, + y: layer_instance.grid_size as f32, + }, + None => TilemapGridSize { + x: tile_size.x, + y: tile_size.y, + }, + }; - let spacing = match tileset_definition { - Some(tileset_definition) if tileset_definition.spacing != 0 => { - // TODO: Check that this is still an issue with upcoming - // bevy_ecs_tilemap releases - #[cfg(not(feature = "atlas"))] - { - warn!( + let spacing = match tileset_definition { + Some(tileset_definition) if tileset_definition.spacing != 0 => { + // TODO: Check that this is still an issue with upcoming + // bevy_ecs_tilemap releases + #[cfg(not(feature = "atlas"))] + { + warn!( "Tile spacing on Tile and AutoTile layers requires the \"atlas\" feature" ); - TilemapSpacing::default() - } - - #[cfg(feature = "atlas")] - { - TilemapSpacing { - x: tileset_definition.spacing as f32, - y: tileset_definition.spacing as f32, - } - } - } - _ => TilemapSpacing::default(), - }; - - let texture = match (tileset_definition, int_grid_image_handle) { - (Some(tileset_definition), _) => TilemapTexture::Single( - tileset_map.get(&tileset_definition.uid).unwrap().clone(), - ), - (None, Some(handle)) => TilemapTexture::Single(handle.clone()), - _ => { - warn!("unable to render tilemap layer, it has no tileset and no intgrid layers were expected"); - continue; + TilemapSpacing::default() } - }; - - let metadata_map: HashMap = tileset_definition - .map(|tileset_definition| { - tileset_definition - .custom_data - .iter() - .map(|TileCustomMetadata { data, tile_id }| { - (*tile_id, TileMetadata { data: data.clone() }) - }) - .collect() - }) - .unwrap_or_default(); - let mut enum_tags_map: HashMap = HashMap::new(); - - if let Some(tileset_definition) = tileset_definition { - for EnumTagValue { - enum_value_id, - tile_ids, - } in tileset_definition.enum_tags.iter() + #[cfg(feature = "atlas")] { - for tile_id in tile_ids { - enum_tags_map - .entry(*tile_id) - .or_insert_with(|| TileEnumTags { - tags: Vec::new(), - source_enum_uid: tileset_definition.tags_source_enum_uid, - }) - .tags - .push(enum_value_id.clone()); + TilemapSpacing { + x: tileset_definition.spacing as f32, + y: tileset_definition.spacing as f32, } } } - - let mut grid_tiles = layer_instance.grid_tiles.clone(); - grid_tiles.extend(layer_instance.auto_layer_tiles.clone()); - - for (i, grid_tiles) in layer_grid_tiles(grid_tiles) - .into_iter() - // filter out tiles that are out of bounds - .map(|grid_tiles| { - grid_tiles - .into_iter() - .filter(|tile| tile_in_layer_bounds(tile, layer_instance)) - .collect::>() - }) - .enumerate() + _ => TilemapSpacing::default(), + }; + + let texture = match (tileset_definition, int_grid_image_handle) { + (Some(tileset_definition), _) => TilemapTexture::Single( + tileset_map.get(&tileset_definition.uid).unwrap().clone(), + ), + (None, Some(handle)) => TilemapTexture::Single(handle.clone()), + _ => { + warn!("unable to render tilemap layer, it has no tileset and no intgrid layers were expected"); + continue; + } + }; + + let metadata_map: HashMap = tileset_definition + .map(|tileset_definition| { + tileset_definition + .custom_data + .iter() + .map(|TileCustomMetadata { data, tile_id }| { + (*tile_id, TileMetadata { data: data.clone() }) + }) + .collect() + }) + .unwrap_or_default(); + + let mut enum_tags_map: HashMap = HashMap::new(); + + if let Some(tileset_definition) = tileset_definition { + for EnumTagValue { + enum_value_id, + tile_ids, + } in tileset_definition.enum_tags.iter() { - let layer_entity = commands.spawn_empty().id(); + for tile_id in tile_ids { + enum_tags_map + .entry(*tile_id) + .or_insert_with(|| TileEnumTags { + tags: Vec::new(), + source_enum_uid: tileset_definition.tags_source_enum_uid, + }) + .tags + .push(enum_value_id.clone()); + } + } + } - let tilemap_bundle = if layer_instance.layer_instance_type == Type::IntGrid - { - // The current spawning of IntGrid layers doesn't allow using - // LayerBuilder::new_batch(). - // So, the actual LayerBuilder usage diverges greatly here - let mut storage = TileStorage::empty(size); - - match tileset_definition { - Some(_) => { - set_all_tiles_with_func( - commands, - &mut storage, - size, - TilemapId(layer_entity), - tile_pos_to_tile_grid_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_int_grid_with_grid_tiles_tile_maker( - &grid_tiles, - &layer_instance.int_grid_csv, - layer_instance.c_wid, - layer_instance.c_hei, - layer_instance.grid_size, - i, - ), - layer_instance.opacity, + let mut grid_tiles = layer_instance.grid_tiles.clone(); + grid_tiles.extend(layer_instance.auto_layer_tiles.clone()); + + for (i, grid_tiles) in layer_grid_tiles(grid_tiles) + .into_iter() + // filter out tiles that are out of bounds + .map(|grid_tiles| { + grid_tiles + .into_iter() + .filter(|tile| tile_in_layer_bounds(tile, layer_instance)) + .collect::>() + }) + .enumerate() + { + let layer_entity = commands.spawn_empty().id(); + + let tilemap_bundle = if layer_instance.layer_instance_type == Type::IntGrid { + // The current spawning of IntGrid layers doesn't allow using + // LayerBuilder::new_batch(). + // So, the actual LayerBuilder usage diverges greatly here + let mut storage = TileStorage::empty(size); + + match tileset_definition { + Some(_) => { + set_all_tiles_with_func( + commands, + &mut storage, + size, + TilemapId(layer_entity), + tile_pos_to_tile_grid_bundle_maker( + tile_pos_to_transparent_tile_maker( + tile_pos_to_int_grid_with_grid_tiles_tile_maker( + &grid_tiles, + &layer_instance.int_grid_csv, + layer_instance.c_wid, + layer_instance.c_hei, + layer_instance.grid_size, + i, ), + layer_instance.opacity, ), - ); - } - None => { - let int_grid_value_defs = &layer_definition_map - .get(&layer_instance.layer_def_uid) - .expect("Encountered layer without definition") - .int_grid_values; - - match ldtk_settings.int_grid_rendering { - IntGridRendering::Colorful => { - set_all_tiles_with_func( - commands, - &mut storage, - size, - TilemapId(layer_entity), - tile_pos_to_tile_grid_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_int_grid_colored_tile_maker( - &layer_instance.int_grid_csv, - int_grid_value_defs, - layer_instance.c_wid, - layer_instance.c_hei, - ), - layer_instance.opacity, + ), + ); + } + None => { + let int_grid_value_defs = &layer_definition_map + .get(&layer_instance.layer_def_uid) + .expect("Encountered layer without definition") + .int_grid_values; + + match ldtk_settings.int_grid_rendering { + IntGridRendering::Colorful => { + set_all_tiles_with_func( + commands, + &mut storage, + size, + TilemapId(layer_entity), + tile_pos_to_tile_grid_bundle_maker( + tile_pos_to_transparent_tile_maker( + tile_pos_to_int_grid_colored_tile_maker( + &layer_instance.int_grid_csv, + int_grid_value_defs, + layer_instance.c_wid, + layer_instance.c_hei, ), + layer_instance.opacity, ), - ); - } - IntGridRendering::Invisible => { - set_all_tiles_with_func( - commands, - &mut storage, - size, - TilemapId(layer_entity), - tile_pos_to_tile_grid_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_tile_if_int_grid_nonzero_maker( - tile_pos_to_invisible_tile, - &layer_instance.int_grid_csv, - layer_instance.c_wid, - layer_instance.c_hei, - ), - layer_instance.opacity, + ), + ); + } + IntGridRendering::Invisible => { + set_all_tiles_with_func( + commands, + &mut storage, + size, + TilemapId(layer_entity), + tile_pos_to_tile_grid_bundle_maker( + tile_pos_to_transparent_tile_maker( + tile_pos_to_tile_if_int_grid_nonzero_maker( + tile_pos_to_invisible_tile, + &layer_instance.int_grid_csv, + layer_instance.c_wid, + layer_instance.c_hei, ), + layer_instance.opacity, ), - ); - } + ), + ); } } } + } - if i == 0 { - for (i, value) in layer_instance - .int_grid_csv - .iter() - .enumerate() - .filter(|(_, v)| **v != 0) - { - let grid_coords = int_grid_index_to_grid_coords( + if i == 0 { + for (i, value) in layer_instance + .int_grid_csv + .iter() + .enumerate() + .filter(|(_, v)| **v != 0) + { + let grid_coords = int_grid_index_to_grid_coords( i, layer_instance.c_wid as u32, layer_instance.c_hei as u32, ).expect("int_grid_csv indices should be within the bounds of 0..(layer_width * layer_height)"); - if let Some(tile_entity) = storage.get(&grid_coords.into()) { - let mut entity_commands = commands.entity(tile_entity); - - let default_ldtk_int_cell: Box< - dyn PhantomLdtkIntCellTrait, - > = Box::new(PhantomLdtkIntCell::::new()); - - ldtk_map_get_or_default( - layer_instance.identifier.clone(), - *value, - &default_ldtk_int_cell, - ldtk_int_cell_map, - ) - .evaluate( - &mut entity_commands, - IntGridCell { value: *value }, - layer_instance, - ); - } + if let Some(tile_entity) = storage.get(&grid_coords.into()) { + let mut entity_commands = commands.entity(tile_entity); + + let default_ldtk_int_cell: Box = + Box::new(PhantomLdtkIntCell::::new()); + + ldtk_map_get_or_default( + layer_instance.identifier.clone(), + *value, + &default_ldtk_int_cell, + ldtk_int_cell_map, + ) + .evaluate( + &mut entity_commands, + IntGridCell { value: *value }, + layer_instance, + ); } } + } - if !(metadata_map.is_empty() && enum_tags_map.is_empty()) { - insert_tile_metadata_for_layer( - commands, - &storage, - &grid_tiles, - layer_instance, - &metadata_map, - &enum_tags_map, - ); - } - - TilemapBundle { - grid_size, - size, - spacing, - storage, - texture: texture.clone(), - tile_size, - ..default() - } - } else { - let tile_bundle_maker = tile_pos_to_tile_grid_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_tile_maker( - &grid_tiles, - layer_instance.c_hei, - layer_instance.grid_size, - ), - layer_instance.opacity, - ), - ); - - // When we add metadata to tiles, we need to add additional - // components to them. - // This can't be accomplished using LayerBuilder::new_batch, - // so the logic for building layers with metadata is slower. - - let mut storage = TileStorage::empty(size); - - set_all_tiles_with_func( + if !(metadata_map.is_empty() && enum_tags_map.is_empty()) { + insert_tile_metadata_for_layer( commands, - &mut storage, - size, - TilemapId(layer_entity), - tile_bundle_maker, + &storage, + &grid_tiles, + layer_instance, + &metadata_map, + &enum_tags_map, ); + } - if !(metadata_map.is_empty() && enum_tags_map.is_empty()) { - insert_tile_metadata_for_layer( - commands, - &storage, + TilemapBundle { + grid_size, + size, + spacing, + storage, + texture: texture.clone(), + tile_size, + ..default() + } + } else { + let tile_bundle_maker = + tile_pos_to_tile_grid_bundle_maker(tile_pos_to_transparent_tile_maker( + tile_pos_to_tile_maker( &grid_tiles, - layer_instance, - &metadata_map, - &enum_tags_map, - ); - } + layer_instance.c_hei, + layer_instance.grid_size, + ), + layer_instance.opacity, + )); - TilemapBundle { - grid_size, - size, - spacing, - storage, - texture: texture.clone(), - tile_size, - ..default() - } - }; + // When we add metadata to tiles, we need to add additional + // components to them. + // This can't be accomplished using LayerBuilder::new_batch, + // so the logic for building layers with metadata is slower. - insert_spatial_bundle_for_layer_tiles( + let mut storage = TileStorage::empty(size); + + set_all_tiles_with_func( commands, - &tilemap_bundle.storage, - &tilemap_bundle.size, - layer_instance.grid_size, + &mut storage, + size, TilemapId(layer_entity), + tile_bundle_maker, ); - // Tile positions are anchored to the center of the tile. - // Applying this adjustment to the layer places the bottom-left corner of - // the layer at the origin of the level. - // Making this adjustment at the layer level, as opposed to using the - // tilemap's default positioning, ensures all layers have the same - // bottom-left corner placement regardless of grid_size. - let tilemap_adjustment = Vec3::new( - layer_instance.grid_size as f32, - layer_instance.grid_size as f32, - 0., - ) / 2.; - - let layer_offset = Vec3::new( - layer_instance.px_total_offset_x as f32, - -layer_instance.px_total_offset_y as f32, - layer_z as f32, - ); + if !(metadata_map.is_empty() && enum_tags_map.is_empty()) { + insert_tile_metadata_for_layer( + commands, + &storage, + &grid_tiles, + layer_instance, + &metadata_map, + &enum_tags_map, + ); + } - commands - .entity(layer_entity) - .insert(tilemap_bundle) - .insert(SpatialBundle::from_transform(Transform::from_translation( - layer_offset + tilemap_adjustment, - ))) - .insert(LayerMetadata::from(layer_instance)) - .insert(Name::new(layer_instance.identifier.to_owned())); + TilemapBundle { + grid_size, + size, + spacing, + storage, + texture: texture.clone(), + tile_size, + ..default() + } + }; - commands.entity(ldtk_entity).add_child(layer_entity); + insert_spatial_bundle_for_layer_tiles( + commands, + &tilemap_bundle.storage, + &tilemap_bundle.size, + layer_instance.grid_size, + TilemapId(layer_entity), + ); + + // Tile positions are anchored to the center of the tile. + // Applying this adjustment to the layer places the bottom-left corner of + // the layer at the origin of the level. + // Making this adjustment at the layer level, as opposed to using the + // tilemap's default positioning, ensures all layers have the same + // bottom-left corner placement regardless of grid_size. + let tilemap_adjustment = Vec3::new( + layer_instance.grid_size as f32, + layer_instance.grid_size as f32, + 0., + ) / 2.; + + let layer_offset = Vec3::new( + layer_instance.px_total_offset_x as f32, + -layer_instance.px_total_offset_y as f32, + layer_z as f32, + ); + + commands + .entity(layer_entity) + .insert(tilemap_bundle) + .insert(SpatialBundle::from_transform(Transform::from_translation( + layer_offset + tilemap_adjustment, + ))) + .insert(LayerMetadata::from(layer_instance)) + .insert(Name::new(layer_instance.identifier.to_owned())); + + commands.entity(ldtk_entity).add_child(layer_entity); - layer_z += 1; - } + layer_z += 1; } } } diff --git a/src/lib.rs b/src/lib.rs index 402222b0..12097d3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,7 +143,7 @@ pub mod prelude { pub use crate::{ app::{LdtkEntity, LdtkEntityAppExt, LdtkIntCell, LdtkIntCellAppExt}, - assets::{LdtkLevel, LdtkProject, LevelIndices, LevelMetadataAccessor}, + assets::{LdtkProject, LevelIndices, LevelMetadataAccessor}, components::{ EntityIid, EntityInstance, GridCoords, IntGridCell, LayerMetadata, LdtkWorldBundle, LevelIid, LevelSet, Respawn, TileEnumTags, TileMetadata, Worldly, @@ -161,4 +161,7 @@ pub mod prelude { #[cfg(feature = "derive")] pub use crate::{LdtkEntity, LdtkIntCell}; + + #[cfg(feature = "external_levels")] + pub use crate::assets::LdtkExternalLevel; } diff --git a/src/systems.rs b/src/systems.rs index 1944b258..772c5ab6 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -4,14 +4,17 @@ use crate::resources::SetClearColor; use crate::{ app::{LdtkEntityMap, LdtkIntCellMap}, - assets::{LdtkLevel, LdtkProject}, + assets::{LdtkProject, LdtkProjectData, LevelMetadataAccessor}, components::*, - ldtk::TilesetDefinition, + ldtk::{Level, TilesetDefinition}, level::spawn_level, resources::{LdtkSettings, LevelEvent, LevelSelection, LevelSpawnBehavior}, utils::*, }; +#[cfg(feature = "external_levels")] +use crate::assets::LdtkExternalLevel; + use bevy::{ecs::system::SystemState, prelude::*}; use std::collections::{HashMap, HashSet}; @@ -19,16 +22,16 @@ use std::collections::{HashMap, HashSet}; #[allow(clippy::too_many_arguments)] pub fn process_ldtk_assets( mut commands: Commands, - mut ldtk_events: EventReader>, + mut ldtk_project_events: EventReader>, ldtk_world_query: Query<(Entity, &Handle)>, #[cfg(feature = "render")] ldtk_settings: Res, #[cfg(feature = "render")] mut clear_color: ResMut, - #[cfg(feature = "render")] ldtk_assets: Res>, + #[cfg(feature = "render")] ldtk_project_assets: Res>, ) { let mut ldtk_handles_to_respawn = HashSet::new(); let mut ldtk_handles_for_clear_color = HashSet::new(); - for event in ldtk_events.iter() { + for event in ldtk_project_events.iter() { match event { AssetEvent::Created { handle } => { debug!("LDtk asset creation detected."); @@ -51,8 +54,8 @@ pub fn process_ldtk_assets( #[cfg(feature = "render")] if ldtk_settings.set_clear_color == SetClearColor::FromEditorBackground { for handle in ldtk_handles_for_clear_color.iter() { - if let Some(ldtk_asset) = ldtk_assets.get(handle) { - clear_color.0 = ldtk_asset.data().bg_color; + if let Some(project) = &ldtk_project_assets.get(handle) { + clear_color.0 = project.json_data().bg_color; } } } @@ -68,14 +71,14 @@ pub fn process_ldtk_assets( pub fn apply_level_selection( level_selection: Option>, ldtk_settings: Res, - ldtk_assets: Res>, + ldtk_project_assets: Res>, mut level_set_query: Query<(&Handle, &mut LevelSet)>, #[cfg(feature = "render")] mut clear_color: ResMut, ) { if let Some(level_selection) = level_selection { for (ldtk_handle, mut level_set) in level_set_query.iter_mut() { - if let Some(ldtk_asset) = ldtk_assets.get(ldtk_handle) { - if let Some(level) = ldtk_asset.get_level(&level_selection) { + if let Some(project) = &ldtk_project_assets.get(ldtk_handle) { + if let Some(level) = project.find_raw_level_by_level_selection(&level_selection) { let new_level_set = { let mut iids = HashSet::new(); iids.insert(LevelIid::new(level.iid.clone())); @@ -123,13 +126,13 @@ pub fn apply_level_set( Option<&Respawn>, )>, ldtk_level_query: Query<(&LevelIid, Entity)>, - ldtk_assets: Res>, + ldtk_project_assets: Res>, ldtk_settings: Res, mut level_events: EventWriter, ) { for (world_entity, level_set, children, ldtk_asset_handle, respawn) in ldtk_world_query.iter() { // Only apply level set if the asset has finished loading - if let Some(ldtk_asset) = ldtk_assets.get(ldtk_asset_handle) { + if let Some(project) = ldtk_project_assets.get(ldtk_asset_handle) { // Determine what levels are currently spawned let previous_level_maps = children .into_iter() @@ -145,9 +148,10 @@ pub fn apply_level_set( // Spawn levels that should be spawned but aren't let spawned_levels = level_set_as_ref .difference(&previous_iids) - .filter_map(|&iid| { - level_events.send(LevelEvent::SpawnTriggered(iid.clone())); - pre_spawn_level(&mut commands, ldtk_asset, iid.clone(), &ldtk_settings) + .filter_map(|&iid| project.get_raw_level_by_iid(iid.get())) + .map(|level| { + level_events.send(LevelEvent::SpawnTriggered(LevelIid::new(level.iid.clone()))); + pre_spawn_level(&mut commands, level, &ldtk_settings) }) .collect::>(); @@ -174,77 +178,56 @@ pub fn apply_level_set( } } -fn pre_spawn_level( - commands: &mut Commands, - ldtk_asset: &LdtkProject, - level_iid: LevelIid, - ldtk_settings: &LdtkSettings, -) -> Option { - ldtk_asset - .level_map() - .get(level_iid.get()) - .map(|level_handle| { - let mut translation = Vec3::ZERO; - - if let LevelSpawnBehavior::UseWorldTranslation { .. } = - ldtk_settings.level_spawn_behavior - { - if let Some(level) = ldtk_asset.get_level(&LevelSelection::iid(level_iid.get())) { - let level_coords = ldtk_pixel_coords_to_translation( - IVec2::new(level.world_x, level.world_y + level.px_hei), - 0, - ); - translation.x = level_coords.x; - translation.y = level_coords.y; - } - } +fn pre_spawn_level(commands: &mut Commands, level: &Level, ldtk_settings: &LdtkSettings) -> Entity { + let mut translation = Vec3::ZERO; - commands - .spawn(level_iid.clone()) - .insert(level_handle.clone()) - .insert(SpatialBundle { - transform: Transform::from_translation(translation), - ..default() - }) - .insert(Name::new( - ldtk_asset - .get_level(&LevelSelection::Iid(level_iid)) - .unwrap() - .identifier - .to_owned(), - )) - .id() + if let LevelSpawnBehavior::UseWorldTranslation { .. } = ldtk_settings.level_spawn_behavior { + let level_coords = ldtk_pixel_coords_to_translation( + IVec2::new(level.world_x, level.world_y + level.px_hei), + 0, + ); + translation.x = level_coords.x; + translation.y = level_coords.y; + } + + commands + .spawn(LevelIid::new(level.iid.clone())) + .insert(SpatialBundle { + transform: Transform::from_translation(translation), + ..default() }) + .insert(Name::new(level.identifier.clone())) + .id() } -/// Performs all the spawning of levels, layers, chunks, bundles, entities, tiles, etc. when an -/// LdtkLevelBundle is added. +/// Performs all the spawning of levels, layers, chunks, bundles, entities, tiles, etc. when a +/// LevelIid is added or respawned. #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub fn process_ldtk_levels( mut commands: Commands, asset_server: Res, images: ResMut>, mut texture_atlases: ResMut>, - ldtk_assets: Res>, - level_assets: Res>, + ldtk_project_assets: Res>, + #[cfg(feature = "external_levels")] level_assets: Res>, ldtk_entity_map: NonSend, ldtk_int_cell_map: NonSend, ldtk_query: Query<&Handle>, level_query: Query< ( Entity, - &Handle, + &LevelIid, &Parent, Option<&Respawn>, Option<&Children>, ), - Or<(Added>, With)>, + Or<(Added, With)>, >, worldly_query: Query<&Worldly>, mut level_events: EventWriter, ldtk_settings: Res, ) { - for (ldtk_entity, level_handle, parent, respawn, children) in level_query.iter() { + for (ldtk_entity, level_iid, parent, respawn, children) in level_query.iter() { // Checking if the level has any children is an okay method of checking whether it has // already been processed. // Users will most likely not be adding children to the level entity betwen its creation @@ -258,10 +241,10 @@ pub fn process_ldtk_levels( if !already_processed { if let Ok(ldtk_handle) = ldtk_query.get(parent.get()) { - if let Some(ldtk_asset) = ldtk_assets.get(ldtk_handle) { + if let Some(ldtk_project) = ldtk_project_assets.get(ldtk_handle) { // Commence the spawning - let tileset_definition_map: HashMap = ldtk_asset - .data() + let tileset_definition_map: HashMap = ldtk_project + .json_data() .defs .tilesets .iter() @@ -269,18 +252,44 @@ pub fn process_ldtk_levels( .collect(); let entity_definition_map = - create_entity_definition_map(&ldtk_asset.data().defs.entities); + create_entity_definition_map(&ldtk_project.json_data().defs.entities); let layer_definition_map = - create_layer_definition_map(&ldtk_asset.data().defs.layers); + create_layer_definition_map(&ldtk_project.json_data().defs.layers); - let int_grid_image_handle = &ldtk_asset.int_grid_image_handle(); + let int_grid_image_handle = &ldtk_project.int_grid_image_handle(); let worldly_set = worldly_query.iter().cloned().collect(); - if let Some(level) = level_assets.get(level_handle) { + let maybe_level_data = match ldtk_project.data() { + #[cfg(feature = "internal_levels")] + LdtkProjectData::Standalone(project) => project + .level_map() + .get(level_iid.get()) + .and_then(|level_metadata| { + let loaded_level = project + .get_loaded_level_at_indices(level_metadata.indices())?; + + Some((level_metadata, loaded_level)) + }), + #[cfg(feature = "external_levels")] + LdtkProjectData::Parent(project) => project + .level_map() + .get(level_iid.get()) + .and_then(|level_metadata| { + let loaded_level = project.get_external_level_at_indices( + &level_assets, + level_metadata.metadata().indices(), + )?; + + Some((level_metadata.metadata(), loaded_level)) + }), + }; + + if let Some((level_metadata, loaded_level)) = maybe_level_data { spawn_level( - level, + loaded_level, + level_metadata.bg_image(), &mut commands, &asset_server, &images, @@ -289,15 +298,16 @@ pub fn process_ldtk_levels( &ldtk_int_cell_map, &entity_definition_map, &layer_definition_map, - ldtk_asset.tileset_map(), + ldtk_project.tileset_map(), &tileset_definition_map, int_grid_image_handle, worldly_set, ldtk_entity, &ldtk_settings, ); - level_events - .send(LevelEvent::Spawned(LevelIid::new(level.data().iid.clone()))); + level_events.send(LevelEvent::Spawned(LevelIid::new( + loaded_level.iid().clone(), + ))); } if respawn.is_some() { @@ -318,10 +328,9 @@ pub fn clean_respawn_entities(world: &mut World) { #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Query<&Children, (With>, With)>, - Query<(Entity, &Handle), With>, - Query<&Handle, Without>, + Query<(Entity, &LevelIid), With>, + Query<&LevelIid, Without>, Query>, - Res>, EventWriter, )> = SystemState::new(world); @@ -334,7 +343,6 @@ pub fn clean_respawn_entities(world: &mut World) { ldtk_levels_to_clean, other_ldtk_levels, worldly_entities, - level_assets, mut level_events, ) = system_state.get_mut(world); @@ -345,24 +353,16 @@ pub fn clean_respawn_entities(world: &mut World) { { entities_to_despawn_recursively.push(*child); - if let Ok(level_handle) = other_ldtk_levels.get(*child) { - if let Some(level_asset) = level_assets.get(level_handle) { - level_events.send(LevelEvent::Despawned(LevelIid::new( - level_asset.data().iid.clone(), - ))); - } + if let Ok(level_iid) = other_ldtk_levels.get(*child) { + level_events.send(LevelEvent::Despawned(level_iid.clone())); } } } - for (level_entity, level_handle) in ldtk_levels_to_clean.iter() { + for (level_entity, level_iid) in ldtk_levels_to_clean.iter() { entities_to_despawn_descendants.push(level_entity); - if let Some(level_asset) = level_assets.get(level_handle) { - level_events.send(LevelEvent::Despawned(LevelIid::new( - level_asset.data().iid.clone(), - ))); - } + level_events.send(LevelEvent::Despawned(level_iid.clone())); } }