From e6e1500fd45caa4e6d66165796f18cda9197136b Mon Sep 17 00:00:00 2001 From: ashley Date: Tue, 19 Sep 2023 20:39:54 -0400 Subject: [PATCH] Support multiple tilesets per tilemap --- src/reskit/cli/settings.rs | 2 +- src/reskit/level/converter.rs | 69 ++++++++++++++------ src/reskit/level/system.rs | 117 ++++++++++++++++++++++++---------- 3 files changed, 133 insertions(+), 55 deletions(-) diff --git a/src/reskit/cli/settings.rs b/src/reskit/cli/settings.rs index c23281e..381a99d 100644 --- a/src/reskit/cli/settings.rs +++ b/src/reskit/cli/settings.rs @@ -11,7 +11,7 @@ pub struct Args { } -#[derive(Clone, ValueEnum)] +#[derive(Clone, Debug, ValueEnum)] pub enum TileOrder { Tile, Sprite diff --git a/src/reskit/level/converter.rs b/src/reskit/level/converter.rs index f2d8e5d..a44bc6d 100644 --- a/src/reskit/level/converter.rs +++ b/src/reskit/level/converter.rs @@ -1,7 +1,7 @@ use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow}; use image::{DynamicImage, GenericImageView}; use roxmltree::Node; -use crate::reskit::utility::print_warning; +use crate::reskit::{utility::print_warning, cli::settings::TileOrder}; use super::ecs; #[derive(Debug)] @@ -15,8 +15,10 @@ pub struct TiledTilemap { #[derive(Debug)] pub struct TiledTileset { + pub first_gid: usize, pub image: DynamicImage, - pub palettes: Vec> + pub palettes: Vec>, + pub tile_order: TileOrder } #[derive(Debug)] @@ -102,7 +104,7 @@ fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result Result> { +fn get_tiles( tileset: Node, first_gid: usize, working_directory: &str ) -> Result> { // Get the image for the tileset let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?; let image_path = format!( "{}/{}", working_directory, image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )? ); @@ -136,7 +138,26 @@ fn get_tiles( tileset: Node, working_directory: &str ) -> Result TileOrder::Sprite, + "tile" => TileOrder::Tile, + invalid => { + print_warning( &format!( "invalid setting for property reskit-tile-order: {}. falling back on \"sprite\"", invalid ) ); + TileOrder::Tile + } + } + } else { + TileOrder::Tile + } + } else { + TileOrder::Tile + }; + + Ok( TiledTileset { first_gid, image, palettes, tile_order } ) } pub fn get_tiled_tilemap( path: &str ) -> Result> { @@ -189,25 +210,33 @@ pub fn get_tiled_tilemap( path: &str ) -> Result> { return Err( "invalid file: tile height is not 8 for --system md" )? } - // Build tileset (current version assumes one tileset per level) - let tileset = map.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset" )?; - let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?; - let tileset_file = read_to_string( format!( "{}/{}", working_directory, tileset_source_path ) )?; - let tileset_document = roxmltree::Document::parse( &tileset_file )?; - let tileset = tileset_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset origin object" )?; + // Build tilesets (current version assumes one tileset per level) + let mut tilesets: Vec = vec![]; - // Validate referenced tileset dimensions match map dimensions - // E.g. if your .tmx specifies 16x16, the acompanying .tsx file should agree - let ( tsx_tile_width, tsx_tile_height ): ( usize, usize ) = ( - tileset.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?, - tileset.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()? - ); - if tsx_tile_width != tile_width || tsx_tile_height != tile_height { - return Err( "invalid file: referenced tilemap doesn't have same per-tile width and height of parent tilemap" )? + for tileset in map.descendants().filter( | node | node.tag_name() == "tileset".into() ) { + let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?; + let tileset_file = read_to_string( format!( "{}/{}", working_directory, tileset_source_path ) )?; + let tileset_document = roxmltree::Document::parse( &tileset_file )?; + let first_gid = tileset.attribute( "firstgid" ).ok_or( "invalid file: no firstgid attribute" )?.parse()?; + let tileset = tileset_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset origin object" )?; + + // Validate referenced tileset dimensions match map dimensions + // E.g. if your .tmx specifies 16x16, the acompanying .tsx file should agree + let ( tsx_tile_width, tsx_tile_height ): ( usize, usize ) = ( + tileset.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?, + tileset.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()? + ); + if tsx_tile_width != tile_width || tsx_tile_height != tile_height { + return Err( "invalid file: referenced tilemap doesn't have same per-tile width and height of parent tilemap" )? + } + + tilesets.push( get_tiles( tileset, first_gid, &working_directory )? ); } - let tileset = get_tiles( tileset, &working_directory )?; - let tileset = vec![ tileset ]; + let tileset = tilesets; + if tileset.is_empty() { + return Err( "invalid file: at least one tileset must be present in file" )? + } // Get the layers let layers: Vec = map.descendants() diff --git a/src/reskit/level/system.rs b/src/reskit/level/system.rs index 1061948..fa89041 100644 --- a/src/reskit/level/system.rs +++ b/src/reskit/level/system.rs @@ -1,6 +1,6 @@ use std::{error::Error, convert::TryInto}; use image::GenericImageView; -use crate::reskit::{tileset::image_to_tiles, utility::symbol_to_pascal}; +use crate::reskit::{tileset::image_to_tiles, utility::symbol_to_pascal, cli::settings::TileOrder}; use super::converter::{TiledTilemap, Layer, SystemPlane}; /** @@ -16,40 +16,80 @@ pub fn get_tiles( tilemap: &TiledTilemap ) -> Result<(Vec, Vec), Box = vec![0; 32]; // --system md, start with a blank buffer tile - let tileset = tilemap.tileset.first().ok_or( "internal error: no tileset" )?; - let tiles_height = tileset.image.height() / 8; // --system md - let tiles_width = tileset.image.width() / 8; // --system md + for tileset in &tilemap.tileset { + let tiles_height = tileset.image.height() / 8; // --system md + let tiles_width = tileset.image.width() / 8; // --system md - for tile_y in 0..tiles_height { - for tile_x in 0..tiles_width { - let tile_index = ( tile_y * tiles_width ) + tile_x; - let tile = tileset.image.clone().crop( tile_x * 8, tile_y * 8, 8, 8 ); + // all this copy pasted code, it's so over + if matches!( tileset.tile_order, TileOrder::Sprite ) { + // Sprite iteration order + for tile_x in 0..tiles_width { + for tile_y in 0..tiles_height { + let tile_index = ( tile_y * tiles_width ) + tile_x; + let tile = tileset.image.clone().crop( tile_x * 8, tile_y * 8, 8, 8 ); - // Fake palette (see below) - let mut fake: [u16; 16] = [ 0; 16 ]; - let selected_pal = tileset.palettes[ tile_index as usize ]; + // Fake palette (see below) + let mut fake: [u16; 16] = [ 0; 16 ]; + let selected_pal = tileset.palettes[ tile_index as usize ]; - let tile_bin = image_to_tiles( - &tile, - { - // Determine if palette is used here or it is a dummy palette - if let Some( selected_pal ) = selected_pal { - &mut system_pals[ selected_pal as usize ] - } else { - // Fake-a-palette - // You will get an error in get_tilemap if you try to use this palette-less tile - &mut fake + let tile_bin = image_to_tiles( + &tile, + { + // Determine if palette is used here or it is a dummy palette + if let Some( selected_pal ) = selected_pal { + &mut system_pals[ selected_pal as usize ] + } else { + // Fake-a-palette + // You will get an error in get_tilemap if you try to use this palette-less tile + &mut fake + } + }, + "tile" // this is not set to "sprite" because we're only doing one tile at a time + ); + + if let Err( _ ) = tile_bin { + return Err( format!( "palette {:?} full (try moving tile {} to another palette)", selected_pal, tile_index ) )? } - }, - "tile" - ); - if let Err( _ ) = tile_bin { - return Err( format!( "palette {:?} full (try moving tile {} to another palette)", selected_pal, tile_index ) )? + if let Ok( tile_bin ) = tile_bin { + all_tiles.extend( tile_bin ); + } + } } + } else { + // Tile iteration order + for tile_y in 0..tiles_height { + for tile_x in 0..tiles_width { + let tile_index = ( tile_y * tiles_width ) + tile_x; + let tile = tileset.image.clone().crop( tile_x * 8, tile_y * 8, 8, 8 ); - if let Ok( tile_bin ) = tile_bin { - all_tiles.extend( tile_bin ); + // Fake palette (see below) + let mut fake: [u16; 16] = [ 0; 16 ]; + let selected_pal = tileset.palettes[ tile_index as usize ]; + + let tile_bin = image_to_tiles( + &tile, + { + // Determine if palette is used here or it is a dummy palette + if let Some( selected_pal ) = selected_pal { + &mut system_pals[ selected_pal as usize ] + } else { + // Fake-a-palette + // You will get an error in get_tilemap if you try to use this palette-less tile + &mut fake + } + }, + "tile" + ); + + if let Err( _ ) = tile_bin { + return Err( format!( "palette {:?} full (try moving tile {} to another palette)", selected_pal, tile_index ) )? + } + + if let Ok( tile_bin ) = tile_bin { + all_tiles.extend( tile_bin ); + } + } } } } @@ -73,7 +113,10 @@ pub fn get_tiles( tilemap: &TiledTilemap ) -> Result<(Vec, Vec), Box Result, Box> { - let tileset = tilemap.tileset.first().ok_or( "internal error: no tileset" )?; + let mut all_palettes: Vec> = vec![]; + for tileset in &tilemap.tileset { + all_palettes.extend( tileset.palettes.iter() ); + } let layer_b: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Tile { system_plane: SystemPlane::MdPlaneB, tiles: _ } ) ); let layer_a: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Tile { system_plane: SystemPlane::MdPlaneA, tiles: _ } ) ); @@ -97,7 +140,7 @@ pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result, Box> // From the starting point of x, y "stamp" the target tile's indices let nametable_entry = target_tile; - let selected_pal = tileset.palettes[ source_tile as usize ]; + let selected_pal = all_palettes[ source_tile as usize ]; if let Some( selected_pal ) = selected_pal { nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry; } else { @@ -127,7 +170,7 @@ pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result, Box> // From the starting point of x, y "stamp" the target tile's indices let nametable_entry = target_tile; - let selected_pal = tileset.palettes[ source_tile as usize ]; + let selected_pal = all_palettes[ source_tile as usize ]; if let Some( selected_pal ) = selected_pal { nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry; } else { @@ -176,12 +219,18 @@ pub fn get_collision_map( tilemap: &TiledTilemap ) -> Result, Box Result> { - let tileset = tilemap.tileset.first().ok_or( "internal error: no tileset" )?; - let version = env!( "CARGO_PKG_VERSION" ); let level_label = symbol_to_pascal( level_name ); let ( width, height ) = ( tilemap.width, tilemap.height ); - let num_tiles = ( tileset.image.width() / 8 ) * ( tileset.image.height() / 8 ); // --system md + let num_tiles = { + let mut total_tiles = 0; + + for tileset in &tilemap.tileset { + total_tiles += ( tileset.image.width() / 8 ) * ( tileset.image.height() / 8 ); // --system md + } + + total_tiles + }; let file = format!( r#"; Level definition file ; Generated by reskit v{version}