Skip to content
Snippets Groups Projects
mod.rs 8.6 KiB
Newer Older
#[cfg(feature = "ldtk_1_0_0")]
mod data_1_0_0;
#[cfg(any(feature = "ldtk_1_1_1", feature = "ldtk_1_1_0"))]
mod data_1_1_0;
#[cfg(any(feature = "ldtk_1_1_3", feature = "ldtk_1_1_2"))]
mod data_1_1_2;
#[cfg(any(feature = "ldtk_1_2_1", feature = "ldtk_1_2_0"))]
mod data_1_2_1;
#[cfg(any(feature = "ldtk_1_2_3", feature = "ldtk_1_2_2"))]
mod data_1_2_2;
#[cfg(feature = "ldtk_1_2_4")]
mod data_1_2_4;
#[cfg(feature = "ldtk_1_2_5")]
mod data_1_2_5;
Louis's avatar
Louis committed
#[cfg(feature = "ldtk_1_3_0")]
mod data_1_3_0;
#[cfg(any(feature = "ldtk_1_4_1", feature = "ldtk_1_4_0"))]
mod data_1_4_0;
Louis's avatar
Louis committed
#[cfg(any(feature = "ldtk_1_5_3"))]
mod data_1_5_3;
use bevy::asset::io::Reader;
Louis's avatar
Louis committed
use bevy::asset::{AssetLoader, AsyncReadExt, LoadContext, UntypedAssetId, VisitAssetDependencies};
use bevy::prelude::{Asset, Handle};
Louis's avatar
Louis committed
use bevy::reflect::TypePath;
Louis's avatar
Louis committed
use crate::ldtk;
#[cfg(feature = "ldtk_1_0_0")]
pub use data_1_0_0::*;
#[cfg(any(feature = "ldtk_1_1_1", feature = "ldtk_1_1_0"))]
pub use data_1_1_0::*;
#[cfg(any(feature = "ldtk_1_1_3", feature = "ldtk_1_1_2"))]
pub use data_1_1_2::*;
#[cfg(any(feature = "ldtk_1_2_1", feature = "ldtk_1_2_0"))]
pub use data_1_2_1::*;
#[cfg(any(feature = "ldtk_1_2_3", feature = "ldtk_1_2_2"))]
pub use data_1_2_2::*;
#[cfg(feature = "ldtk_1_2_4")]
pub use data_1_2_4::*;
#[cfg(feature = "ldtk_1_2_5")]
pub use data_1_2_5::*;
Louis's avatar
Louis committed
#[cfg(feature = "ldtk_1_3_0")]
pub use data_1_3_0::*;
#[cfg(any(feature = "ldtk_1_4_1", feature = "ldtk_1_4_0"))]
pub use data_1_4_0::*;
#[cfg(any(feature = "ldtk_1_5_3"))]
pub use data_1_5_3::*;
Louis's avatar
Louis committed
use serde::Deserialize;
#[derive(thiserror::Error, Debug)]
pub enum ParseError {
	#[error("Failed to parse file: {0}")]
	SerdeError(String),
Louis's avatar
Louis committed
pub trait LdtkFromBytes<'a>: Deserialize<'a> {
	fn from_bytes(bytes: &'a [u8]) -> Result<Self, ParseError> {
		serde_json::from_slice(bytes).map_err(|e| ParseError::SerdeError(format!("{}", e)))
	}
}

macro_rules! impl_from_bytes {
	($type: tt) => {
		impl<'a> From<&'a [u8]> for $type {
			fn from(value: &'a [u8]) -> Self {
				#[cfg(feature = "no_panic")]
				{
					match $type::from_bytes(value) {
						Ok(val) => val,
						Err(e) => {
							log::error!("{}", e);
							std::process::abort();
						}
					}
				}

				#[cfg(not(feature = "no_panic"))]
				{
					$type::from_bytes(value).expect("Failed to parse ldtk file")
				}
			}
		}
	};
}

impl<'a> LdtkFromBytes<'a> for Level {}
impl<'a> LdtkFromBytes<'a> for Project {}

impl_from_bytes!(Level);
impl_from_bytes!(Project);

impl TypePath for Project {
	fn type_path() -> &'static str {
		"micro_ldtk::ldtk::Project"
	}

	fn short_type_path() -> &'static str {
		"Project"
	}
}
impl VisitAssetDependencies for Project {
	fn visit_dependencies(&self, _visit: &mut impl FnMut(UntypedAssetId)) {}
}

impl Asset for Project {}

Louis's avatar
Louis committed
impl TypePath for Level {
	fn type_path() -> &'static str {
		"micro_ld0tk::ldtk::Level"
Louis's avatar
Louis committed

Louis's avatar
Louis committed
	fn short_type_path() -> &'static str {
		"Level"
	}
}

impl VisitAssetDependencies for Level {
	fn visit_dependencies(&self, _visit: &mut impl FnMut(UntypedAssetId)) {}
}

impl Asset for Level {}

Louis's avatar
Louis committed
impl Project {
Louis's avatar
Louis committed
	pub fn get_all_levels(&self) -> Vec<&Level> {
		if !self.worlds.is_empty() {
Louis's avatar
Louis committed
			self.worlds
				.iter()
				.flat_map(|world| world.levels.iter())
				.collect()
		} else {
			self.levels.iter().collect()
		}
	}

	#[cfg(any(
		feature = "ldtk_1_2_5",
		feature = "ldtk_1_2_4",
		feature = "ldtk_1_2_3",
		feature = "ldtk_1_2_2",
		feature = "ldtk_1_2_1",
		feature = "ldtk_1_2_0",
		feature = "ldtk_1_1_3",
		feature = "ldtk_1_1_2",
		feature = "ldtk_1_1_1",
		feature = "ldtk_1_1_0",
		feature = "ldtk_1_0_0"
	))]
	pub fn get_world_levels(&self, identifier: impl ToString) -> Vec<&Level> {
		vec![]
	}

	#[cfg(any(feature = "ldtk_1_3_0", feature = "ldtk_1_4_0", feature = "ldtk_1_4_1"))]
Louis's avatar
Louis committed
	pub fn get_world_levels(&self, identifier: impl ToString) -> Vec<&Level> {
		let id = identifier.to_string();
		self.worlds
			.iter()
			.find(|world| world.identifier == id)
			.map(|list| list.levels.iter().collect())
			.unwrap_or_else(Vec::new)
	}
#[derive(Debug, thiserror::Error)]
pub enum LdtkLoadError {
	#[error(transparent)]
	Io(#[from] std::io::Error),
	#[error(transparent)]
	Serde(#[from] serde_json::Error),
	#[error(transparent)]
	Ldtk(#[from] ldtk::ParseError),
}

Louis's avatar
Louis committed
#[derive(Asset, TypePath)]
pub(crate) struct LevelSet(pub Vec<Handle<Level>>);

#[derive(Default)]
pub struct LdtkLoader;
impl AssetLoader for LdtkLoader {
	type Asset = Project;
	type Settings = ();
	type Error = LdtkLoadError;

Louis's avatar
Louis committed
	async fn load<'a>(
Louis's avatar
Louis committed
		reader: &'a mut Reader<'_>,
		_settings: &'a Self::Settings,
Louis's avatar
Louis committed
		load_context: &'a mut LoadContext<'_>,
	) -> Result<Self::Asset, Self::Error> {
		let mut bytes = Vec::new();
		reader.read_to_end(&mut bytes).await?;
		let project = Project::from_bytes(bytes.as_slice())?;

		let levels = project
			.levels
			.iter()
			.flat_map(|level| {
				log::debug!(
					"Checking if level is external: {} [{}]",
					level.identifier,
					level.external_rel_path.is_some()
				);

				level
					.external_rel_path
					.as_ref()
					.map(|path| (level.identifier.clone(), path))
			})
			.collect::<Vec<(String, &String)>>();

		let parent_path = load_context.path().parent().map(|pp| pp.to_path_buf());
		let mut level_set = Vec::with_capacity(levels.len());

		for (_, path) in levels {
			level_set.push(match &parent_path {
				Some(parent) => load_context.load::<Level>(parent.join(path)),
				None => load_context.load::<Level>(path),
			});
		}
Louis's avatar
Louis committed
		load_context.add_labeled_asset(
			format!("{}ExternalLevels", project.iid),
			LevelSet(level_set),
		);
Louis's avatar
Louis committed
		Ok(project)
Louis's avatar
Louis committed
#[derive(Default)]
pub struct LdtkLevelLoader;
impl AssetLoader for LdtkLevelLoader {
	type Asset = Level;
	type Settings = ();
	type Error = LdtkLoadError;

Louis's avatar
Louis committed
	async fn load<'a>(
Louis's avatar
Louis committed
		&'a self,
Louis's avatar
Louis committed
		reader: &'a mut Reader<'_>,
		_settings: &'a Self::Settings,
Louis's avatar
Louis committed
		_load_context: &'a mut LoadContext<'_>,
	) -> Result<Self::Asset, Self::Error> {
		let mut bytes = Vec::new();
		reader.read_to_end(&mut bytes).await?;
		let level = Level::from_bytes(bytes.as_slice())?;
		Ok(level)
Louis's avatar
Louis committed
	}
	fn extensions(&self) -> &[&str] {
		&["ldtkl"]
	}
}
#[cfg(feature = "autotile")]
mod autotile_support {
	use micro_autotile::{AutoRuleSet, AutoTileRule, TileMatcher, TileOutput, TileStatus};

	use crate::ldtk::{AutoLayerRuleDefinition, AutoLayerRuleGroup, Project};

	#[cfg(feature = "_optional_tile_list")]
	fn create_output(rule: &AutoLayerRuleDefinition) -> TileOutput {
		TileOutput::Random(
			rule.tile_rects_ids
				.iter()
				.flatten()
				.map(|val| *val as usize)
				.collect(),
		)
	}

	#[cfg(not(feature = "_optional_tile_list"))]
	fn create_output(rule: &AutoLayerRuleDefinition) -> TileOutput {
		TileOutput::Random(rule.tile_ids.iter().map(|val| *val as usize).collect())
	}

	impl From<&AutoLayerRuleGroup> for AutoRuleSet {
		fn from(value: &AutoLayerRuleGroup) -> Self {
			let set = value
				.rules
				.iter()
				.filter_map(|rule| match rule.size {
					1 => Some(AutoTileRule {
						chance: rule.chance as f32,
						output: create_output(rule),
						matcher: TileMatcher::single_match(TileStatus::from_ldtk_value(
							rule.pattern[0],
						)),
					}),
					3 => {
						if rule.pattern.len() == 9 {
							let matcher = TileMatcher([
								TileStatus::from_ldtk_value(rule.pattern[0]),
								TileStatus::from_ldtk_value(rule.pattern[1]),
								TileStatus::from_ldtk_value(rule.pattern[2]),
								TileStatus::from_ldtk_value(rule.pattern[3]),
								TileStatus::from_ldtk_value(rule.pattern[4]),
								TileStatus::from_ldtk_value(rule.pattern[5]),
								TileStatus::from_ldtk_value(rule.pattern[6]),
								TileStatus::from_ldtk_value(rule.pattern[7]),
								TileStatus::from_ldtk_value(rule.pattern[8]),
							]);

							let rule = AutoTileRule {
								chance: rule.chance as f32,
								matcher,
								output: create_output(rule),
							};

							Some(rule)
						} else {
							None
						}
					}
					_ => None,
				})
				.collect();

			AutoRuleSet(set)
		}
	}

	impl From<&Project> for AutoRuleSet {
		fn from(value: &Project) -> Self {
			let mut base_set = AutoRuleSet::default();

			for layers in value.defs.layers.iter() {
				for rule_group in layers.auto_rule_groups.iter() {
					base_set = base_set + rule_group.into();
				}
			}

			base_set
		}
	}
}

#[cfg(test)]
mod test {
Louis's avatar
Louis committed
	#![allow(dead_code)]
Louis's avatar
Louis committed
	use crate::ldtk::{LdtkFromBytes, Project};
Louis's avatar
Louis committed
	#[cfg_attr(feature = "ldtk_1_2_5", test)]
	pub fn load_project() {
Louis's avatar
Louis committed
		const PROJECT_DATA: &[u8] = include_bytes!("./test_data/ver_1_2_5.ldtk");
Louis's avatar
Louis committed
		let project = Project::from_bytes(PROJECT_DATA).expect("Failed to parse project file");
		for layer in project.defs.layers.iter() {
Louis's avatar
Louis committed
			for _auto_rule_group in layer.auto_rule_groups.iter() {}