use std::io;
use crate::{
assets::{
LdtkJsonWithMetadata, LdtkProjectData, LevelIndices, LevelMetadata, LevelMetadataAccessor,
},
ldtk::{raw_level_accessor::RawLevelAccessor, LdtkJson, Level},
};
use bevy::{
asset::{io::Reader, AssetLoader, AssetPath, LoadContext, ParseAssetPathError},
prelude::*,
reflect::Reflect,
};
use derive_getters::Getters;
use derive_more::From;
use std::collections::HashMap;
use thiserror::Error;
#[cfg(feature = "internal_levels")]
use crate::assets::InternalLevels;
#[cfg(feature = "external_levels")]
use crate::assets::{ExternalLevelMetadata, ExternalLevels};
fn ldtk_path_to_asset_path<'b>(
ldtk_path: &AssetPath<'b>,
rel_path: &str,
) -> Result<AssetPath<'b>, ParseAssetPathError> {
ldtk_path.resolve_embed(rel_path)
}
#[derive(Clone, Debug, PartialEq, From, Getters, Reflect, Asset)]
pub struct LdtkProject {
data: LdtkProjectData,
tileset_map: HashMap<i32, Handle<Image>>,
int_grid_image_handle: Option<Handle<Image>>,
}
impl LdtkProject {
fn new(
data: LdtkProjectData,
tileset_map: HashMap<i32, Handle<Image>>,
int_grid_image_handle: Option<Handle<Image>>,
) -> LdtkProject {
LdtkProject {
data,
tileset_map,
int_grid_image_handle,
}
}
pub fn json_data(&self) -> &LdtkJson {
self.data.json_data()
}
#[cfg(feature = "internal_levels")]
pub fn as_standalone(&self) -> &LdtkJsonWithMetadata<InternalLevels> {
self.data.as_standalone()
}
#[cfg(feature = "external_levels")]
pub fn as_parent(&self) -> &LdtkJsonWithMetadata<ExternalLevels> {
self.data.as_parent()
}
}
impl RawLevelAccessor for LdtkProject {
fn worlds(&self) -> &[crate::ldtk::World] {
self.data.worlds()
}
fn root_levels(&self) -> &[Level] {
self.data.root_levels()
}
}
impl LevelMetadataAccessor for LdtkProject {
fn get_level_metadata_by_iid(&self, iid: &String) -> Option<&LevelMetadata> {
self.data.get_level_metadata_by_iid(iid)
}
}
#[allow(dead_code)]
#[derive(Debug, Error)]
pub enum LdtkProjectLoaderError {
#[error("encountered IO error reading LDtk project: {0}")]
Io(#[from] io::Error),
#[error("unable to deserialize LDtk project: {0}")]
Deserialize(#[from] serde_json::Error),
#[error("LDtk project uses internal levels, but the internal_levels feature is disabled")]
InternalLevelsDisabled,
#[error("LDtk project uses external levels, but the external_levels feature is disabled")]
ExternalLevelsDisabled,
#[error("LDtk project uses internal levels, but some level's layer_instances is null")]
InternalLevelWithNullLayers,
#[error("LDtk project uses external levels, but some level's external_rel_path is null")]
ExternalLevelWithNullPath,
#[error("unable to parse relative path in LDtk file: {0}")]
ParseRelativePath(#[from] ParseAssetPathError),
}
#[derive(Default, TypePath)]
pub struct LdtkProjectLoader;
fn load_level_metadata(
load_context: &mut LoadContext,
level_indices: LevelIndices,
level: &Level,
expect_level_loaded: bool,
) -> Result<LevelMetadata, LdtkProjectLoaderError> {
let bg_image = level
.bg_rel_path
.as_ref()
.map(|rel_path| {
ldtk_path_to_asset_path(load_context.path(), rel_path)
.map(|asset_path| load_context.load(asset_path))
})
.transpose()?;
if expect_level_loaded && level.layer_instances.is_none() {
Err(LdtkProjectLoaderError::InternalLevelWithNullLayers)?;
}
let level_metadata = LevelMetadata::new(bg_image, level_indices);
Ok(level_metadata)
}
#[cfg(feature = "external_levels")]
fn load_external_level_metadata(
load_context: &mut LoadContext,
level_indices: LevelIndices,
level: &Level,
) -> Result<ExternalLevelMetadata, LdtkProjectLoaderError> {
let level_metadata = 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.load(external_level_path.clone());
Ok(ExternalLevelMetadata::new(level_metadata, external_handle))
}
impl AssetLoader for LdtkProjectLoader {
type Asset = LdtkProject;
type Settings = ();
type Error = LdtkProjectLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let data: LdtkJson = serde_json::from_slice(&bytes)?;
let mut tileset_map: HashMap<i32, Handle<Image>> = 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_map.insert(tileset.uid, load_context.load(asset_path));
} else if tileset.embed_atlas.is_some() {
warn!("Ignoring LDtk's Internal_Icons. They cannot be displayed due to their license.");
} else {
let identifier = &tileset.identifier;
warn!("{identifier} tileset cannot be loaded, it has a null relative path.");
}
}
let int_grid_image_handle = data
.defs
.create_int_grid_image()
.map(|image| load_context.add_labeled_asset("int_grid_image".to_string(), image));
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 level_metadata =
load_external_level_metadata(load_context, level_indices, level)?;
level_map.insert(level.iid.clone(), level_metadata);
}
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 level_metadata =
load_level_metadata(load_context, level_indices, level, true)?;
level_map.insert(level.iid.clone(), level_metadata);
}
LdtkProject::new(
LdtkProjectData::Standalone(LdtkJsonWithMetadata::new(data, level_map)),
tileset_map,
int_grid_image_handle,
)
}
#[cfg(not(feature = "internal_levels"))]
{
Err(LdtkProjectLoaderError::InternalLevelsDisabled)?
}
};
Ok(ldtk_project)
}
fn extensions(&self) -> &[&str] {
&["ldtk"]
}
}
#[cfg(test)]
mod tests {
use std::marker::PhantomData;
use std::path::Path;
use super::*;
use derive_more::Constructor;
use fake::{uuid::UUIDv4, Dummy, Fake};
use rand::Rng;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Constructor)]
pub struct LdtkProjectFaker<F>
where
LdtkProjectData: Dummy<F>,
{
ldtk_project_data_faker: F,
}
impl<F> Dummy<LdtkProjectFaker<F>> for LdtkProject
where
LdtkProjectData: Dummy<F>,
{
fn dummy_with_rng<R: Rng + ?Sized>(config: &LdtkProjectFaker<F>, 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::Uuid(UUIDv4.fake(), PhantomData)))
.collect();
LdtkProject {
data,
tileset_map,
int_grid_image_handle: Some(Handle::Uuid(UUIDv4.fake(), PhantomData)),
}
}
}
#[test]
fn normalizes_asset_paths() {
let resolve_path = |project_path: &'static str, rel_path| {
let asset_path = ldtk_path_to_asset_path(&project_path.into(), rel_path).unwrap();
asset_path.path().to_owned()
};
assert_eq!(
resolve_path("project.ldtk", "images/tiles.png"),
Path::new("images/tiles.png")
);
assert_eq!(
resolve_path("projects/sub/project.ldtk", "../images/tiles.png"),
Path::new("projects/images/tiles.png")
);
assert_eq!(
resolve_path("projects/sub/project.ldtk", "../../tiles.png"),
Path::new("tiles.png")
);
}
#[cfg(target_os = "windows")]
#[test]
fn normalizes_windows_asset_paths() {
let resolve_path = |project_path: &'static str, rel_path| {
let asset_path = ldtk_path_to_asset_path(&project_path.into(), rel_path).unwrap();
asset_path.path().to_owned()
};
assert_eq!(
resolve_path("projects\\sub/project.ldtk", "../images/tiles.png"),
Path::new("projects/images/tiles.png")
);
assert_eq!(
resolve_path("projects\\sub/project.ldtk", "../../images/tiles.png"),
Path::new("images/tiles.png")
);
assert_eq!(
resolve_path("projects/sub\\project.ldtk", "../../tiles.png"),
Path::new("tiles.png")
);
}
#[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},
};
use super::*;
impl Dummy<InternalLevels> for LdtkProject {
fn dummy_with_rng<R: Rng + ?Sized>(_: &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<ExternalLevels> for LdtkProject {
fn dummy_with_rng<R: Rng + ?Sized>(_: &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
);
}
}
}