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())); } }