#[cfg(feature = "user_properties")]
use std::ops::Deref;
use std::sync::Arc;
use crate::{
prelude::*,
tiled::{
cache::TiledResourceCache, helpers::iso_projection, map::asset::TiledMapTileset,
reader::BytesResourceReader,
},
};
use bevy::{
asset::{io::Reader, AssetLoader, AssetPath, LoadContext},
platform::collections::HashMap,
prelude::*,
};
#[derive(TypePath)]
struct TiledMapLoader {
cache: TiledResourceCache,
#[cfg(feature = "user_properties")]
registry: bevy::reflect::TypeRegistryArc,
}
pub(crate) fn tileset_label(tileset: &tiled::Tileset) -> Option<String> {
tileset
.source
.to_str()
.map(|s| format!("{s}#{}", tileset.name))
}
impl FromWorld for TiledMapLoader {
fn from_world(world: &mut World) -> Self {
Self {
cache: world.resource::<TiledResourceCache>().clone(),
#[cfg(feature = "user_properties")]
registry: world.resource::<AppTypeRegistry>().0.clone(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum TiledMapLoaderError {
#[error("Could not load Tiled file: {0}")]
Io(#[from] std::io::Error),
}
impl AssetLoader for TiledMapLoader {
type Asset = TiledMapAsset;
type Settings = ();
type Error = TiledMapLoaderError;
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?;
debug!("Start loading map '{}'", load_context.path());
let map_path = load_context.path().path().to_path_buf();
let map = {
let mut loader = tiled::Loader::with_cache_and_reader(
self.cache.clone(),
BytesResourceReader::new(&bytes, load_context),
);
loader
.load_tmx_map(map_path)
.map_err(|e| std::io::Error::other(format!("Could not load TMX map: {e}")))?
};
let map_type = tilemap_type_from_map(&map);
let grid_size = grid_size_from_map(&map);
let mut tilesets = HashMap::default();
let mut tilesets_label_by_index = HashMap::<u32, String>::default();
for (tileset_index, tileset) in map.tilesets().iter().enumerate() {
debug!(
"Loading tileset (index={:?} name={:?}) from {:?}",
tileset_index, tileset.name, tileset.source
);
let Some(label) = tileset_label(tileset) else {
continue;
};
let Some(tiled_map_tileset) =
tileset_to_tiled_map_tileset(tileset.clone(), load_context, &label)
else {
continue;
};
tilesets_label_by_index.insert(tileset_index as u32, label.to_owned());
tilesets.insert(label.to_owned(), tiled_map_tileset);
}
let mut images = HashMap::default();
let mut largest_tile_size = TilemapTileSize::new(grid_size.x, grid_size.y);
for (layer_index, layer) in map.layers().enumerate() {
match layer.layer_type() {
tiled::LayerType::Tiles(tiles_layer) => {
match tiles_layer {
tiled::TileLayer::Finite(tiles_finite_layer) => {
for x in 0..tiles_finite_layer.width() as i32 {
for y in 0..tiles_finite_layer.height() as i32 {
let Some(tile) = tiles_finite_layer
.get_tile(x, y)
.and_then(|t| t.get_tile())
else {
continue;
};
let tile_size = tile_size(&tile);
if tile_size > largest_tile_size {
debug!("Update tile_size: {tile_size:?}");
largest_tile_size = tile_size;
}
}
}
}
tiled::TileLayer::Infinite(tiles_infinite_layer) => {
for (_, chunk) in tiles_infinite_layer.chunks() {
for x in 0..tiled::ChunkData::WIDTH as i32 {
for y in 0..tiled::ChunkData::HEIGHT as i32 {
let Some(tile) =
chunk.get_tile(x, y).and_then(|t| t.get_tile())
else {
continue;
};
let tile_size = tile_size(&tile);
if tile_size > largest_tile_size {
debug!("Update tile_size: {tile_size:?}");
largest_tile_size = tile_size;
}
}
}
}
}
}
}
tiled::LayerType::Objects(object_layer) => {
for object_data in object_layer.objects() {
let Some(tile) = object_data.get_tile() else {
continue;
};
let tiled::TilesetLocation::Template(tileset) = tile.tileset_location()
else {
continue;
};
let Some(label) = tileset_label(tileset) else {
continue;
};
if tilesets.contains_key(&label) {
continue;
}
let Some(tiled_map_tileset) =
tileset_to_tiled_map_tileset(tileset.clone(), load_context, &label)
else {
continue;
};
tilesets.insert(label.to_owned(), tiled_map_tileset);
}
}
tiled::LayerType::Image(image_layer) => {
let Some(image) = &image_layer.image else {
continue;
};
let asset_path = AssetPath::from(image.source.clone());
let handle: Handle<Image> = load_context.load(asset_path);
images.insert(layer_index as u32, handle);
}
_ => continue,
}
}
let mut infinite = false;
let mut topleft = (999999, 999999);
for layer in map.layers() {
if let tiled::LayerType::Tiles(tiled::TileLayer::Infinite(layer)) = layer.layer_type() {
topleft = layer.chunks().fold(topleft, |acc, (pos, _)| {
(acc.0.min(pos.0), acc.1.min(pos.1))
});
infinite = true;
}
}
let mut bottomright = (0, 0);
for layer in map.layers() {
if let tiled::LayerType::Tiles(tiled::TileLayer::Infinite(layer)) = layer.layer_type() {
bottomright = layer.chunks().fold(bottomright, |acc, (pos, _)| {
(acc.0.max(pos.0), acc.1.max(pos.1))
});
infinite = true;
}
}
let (tilemap_size, tiled_offset) = if infinite {
debug!(
"(infinite map) topleft = {:?}, bottomright = {:?}",
topleft, bottomright
);
(
TilemapSize {
x: (bottomright.0 - topleft.0 + 1) as u32 * tiled::ChunkData::WIDTH,
y: (bottomright.1 - topleft.1 + 1) as u32 * tiled::ChunkData::HEIGHT,
},
match map_type {
TilemapType::Square => Vec2 {
x: -topleft.0 as f32 * tiled::ChunkData::WIDTH as f32 * grid_size.x,
y: topleft.1 as f32 * tiled::ChunkData::HEIGHT as f32 * grid_size.y,
},
TilemapType::Hexagon(HexCoordSystem::ColumnOdd)
| TilemapType::Hexagon(HexCoordSystem::ColumnEven) => Vec2 {
x: -topleft.0 as f32 * tiled::ChunkData::WIDTH as f32 * grid_size.x * 0.75,
y: topleft.1 as f32 * tiled::ChunkData::HEIGHT as f32 * grid_size.y,
},
TilemapType::Hexagon(HexCoordSystem::RowOdd)
| TilemapType::Hexagon(HexCoordSystem::RowEven) => Vec2 {
x: -topleft.0 as f32 * tiled::ChunkData::WIDTH as f32 * grid_size.x,
y: topleft.1 as f32 * tiled::ChunkData::HEIGHT as f32 * grid_size.y * 0.75,
},
TilemapType::Isometric(IsoCoordSystem::Diamond) => Vec2 {
x: -topleft.0 as f32 * tiled::ChunkData::WIDTH as f32 * grid_size.y,
y: -topleft.1 as f32 * tiled::ChunkData::HEIGHT as f32 * grid_size.y,
},
TilemapType::Isometric(IsoCoordSystem::Staggered) => {
panic!("Isometric (Staggered) map is not supported");
}
_ => unreachable!(),
},
)
} else {
topleft = (0, 0);
bottomright = (0, 0);
(
TilemapSize {
x: map.width,
y: map.height,
},
Vec2::ZERO,
)
};
let rect = Rect {
min: Vec2::ZERO,
max: match map_type {
TilemapType::Square => Vec2 {
x: tilemap_size.x as f32 * grid_size.x,
y: tilemap_size.y as f32 * grid_size.y,
},
TilemapType::Hexagon(HexCoordSystem::ColumnOdd)
| TilemapType::Hexagon(HexCoordSystem::ColumnEven) => Vec2 {
x: tilemap_size.x as f32 * grid_size.x * 0.75,
y: tilemap_size.y as f32 * grid_size.y,
},
TilemapType::Hexagon(HexCoordSystem::RowOdd)
| TilemapType::Hexagon(HexCoordSystem::RowEven) => Vec2 {
x: tilemap_size.x as f32 * grid_size.x,
y: tilemap_size.y as f32 * grid_size.y * 0.75,
},
TilemapType::Isometric(IsoCoordSystem::Diamond) => {
let topleft = iso_projection(Vec2::ZERO, &tilemap_size, &grid_size);
let topright = iso_projection(
Vec2 {
x: tilemap_size.x as f32 * grid_size.y,
y: 0.,
},
&tilemap_size,
&grid_size,
);
2. * (topright - topleft)
}
TilemapType::Isometric(IsoCoordSystem::Staggered) => {
panic!("Isometric (Staggered) map is not supported");
}
_ => unreachable!(),
},
};
#[cfg(feature = "user_properties")]
let properties = crate::tiled::properties::load::DeserializedMapProperties::load(
&map,
self.registry.read().deref(),
load_context,
);
#[cfg(feature = "user_properties")]
trace!(?properties, "user properties");
trace!(?tilesets, "tilesets");
let asset_map = TiledMapAsset {
map,
tilemap_size,
largest_tile_size,
tiled_offset,
rect,
topleft_chunk: topleft,
bottomright_chunk: bottomright,
tilesets,
tilesets_label_by_index,
images,
#[cfg(feature = "user_properties")]
properties,
};
debug!("Loaded map '{}': {:?}", load_context.path(), &asset_map,);
Ok(asset_map)
}
fn extensions(&self) -> &[&str] {
static EXTENSIONS: &[&str] = &["tmx"];
EXTENSIONS
}
}
fn tileset_to_tiled_map_tileset(
tileset: Arc<tiled::Tileset>,
load_context: &mut LoadContext<'_>,
path: &str,
) -> Option<TiledMapTileset> {
#[cfg(not(feature = "atlas"))]
let tileset_path = tileset.source.to_str()?;
let mut texture_atlas_layout_handle = None;
#[cfg(not(feature = "atlas"))]
let mut tile_image_offsets = HashMap::default();
let (usable_for_tiles_layer, tilemap_texture) = match &tileset.image {
None => {
#[cfg(feature = "atlas")]
{
info!("Skipping image collection tileset '{}' which is incompatible with atlas feature", tileset.name);
return None;
}
#[cfg(not(feature = "atlas"))]
{
let mut usable_for_tiles_layer = true;
let mut image_size: Option<(i32, i32)> = None;
let mut tile_images: Vec<Handle<Image>> = Vec::new();
for (tile_id, tile) in tileset.tiles() {
if let Some(img) = &tile.image {
let asset_path = AssetPath::from(img.source.clone());
trace!("Loading tile image from {asset_path:?} as image ({tileset_path}, {tile_id})");
let texture: Handle<Image> = load_context.load(asset_path.clone());
tile_image_offsets.insert(tile_id, tile_images.len() as u32);
tile_images.push(texture.clone());
if usable_for_tiles_layer {
if let Some(image_size) = image_size {
if img.width != image_size.0 || img.height != image_size.1 {
usable_for_tiles_layer = false;
}
} else {
image_size = Some((img.width, img.height));
}
}
}
}
if !usable_for_tiles_layer {
debug!(
"Tileset (path={:?}) have non constant image size and cannot be used for tiles layer",
tileset_path
);
}
(usable_for_tiles_layer, TilemapTexture::Vector(tile_images))
}
}
Some(img) => {
let asset_path = AssetPath::from(img.source.clone());
let texture: Handle<Image> = load_context.load(asset_path);
let columns = (img.width as u32 - tileset.margin + tileset.spacing)
/ (tileset.tile_width + tileset.spacing);
if columns > 0 {
texture_atlas_layout_handle = load_context
.labeled_asset_scope(path.to_owned(), |_| {
Ok::<_, ()>(TextureAtlasLayout::from_grid(
UVec2::new(tileset.tile_width, tileset.tile_height),
columns,
tileset.tilecount / columns,
Some(UVec2::splat(tileset.spacing)),
Some(UVec2::splat(tileset.margin)),
))
})
.ok();
}
(true, TilemapTexture::Single(texture.clone()))
}
};
Some(TiledMapTileset {
usable_for_tiles_layer,
tilemap_texture,
texture_atlas_layout_handle,
#[cfg(not(feature = "atlas"))]
tile_image_offsets,
})
}
pub(crate) fn plugin(app: &mut App) {
app.init_asset_loader::<TiledMapLoader>();
}