Support multiple tilesets per tilemap
parent
32874109d8
commit
e6e1500fd4
|
@ -11,7 +11,7 @@ pub struct Args {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, ValueEnum)]
|
#[derive(Clone, Debug, ValueEnum)]
|
||||||
pub enum TileOrder {
|
pub enum TileOrder {
|
||||||
Tile,
|
Tile,
|
||||||
Sprite
|
Sprite
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow};
|
use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow};
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::{DynamicImage, GenericImageView};
|
||||||
use roxmltree::Node;
|
use roxmltree::Node;
|
||||||
use crate::reskit::utility::print_warning;
|
use crate::reskit::{utility::print_warning, cli::settings::TileOrder};
|
||||||
use super::ecs;
|
use super::ecs;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -15,8 +15,10 @@ pub struct TiledTilemap {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TiledTileset {
|
pub struct TiledTileset {
|
||||||
|
pub first_gid: usize,
|
||||||
pub image: DynamicImage,
|
pub image: DynamicImage,
|
||||||
pub palettes: Vec<Option<u8>>
|
pub palettes: Vec<Option<u8>>,
|
||||||
|
pub tile_order: TileOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -102,7 +104,7 @@ fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result<Optio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tiles( tileset: Node, working_directory: &str ) -> Result<TiledTileset, Box<dyn Error>> {
|
fn get_tiles( tileset: Node, first_gid: usize, working_directory: &str ) -> Result<TiledTileset, Box<dyn Error>> {
|
||||||
// Get the image for the tileset
|
// 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 = 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" )? );
|
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<TiledTileset, B
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok( TiledTileset { image, palettes } )
|
// Retrieve the tile order as specified by reskit-tile-order (if none is specified, fallback on Tile)
|
||||||
|
let tile_order = if let Some( properties ) = tileset.descendants().find( | node | node.tag_name() == "properties".into() ) {
|
||||||
|
if let Some( tile_order_property ) = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-tile-order" ) ) {
|
||||||
|
let tile_order_property = tile_order_property.attribute( "value" ).expect( "internal error: no reskit-tile-order value" );
|
||||||
|
match tile_order_property.to_lowercase().as_str() {
|
||||||
|
"sprite" => 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<TiledTilemap, Box<dyn Error>> {
|
pub fn get_tiled_tilemap( path: &str ) -> Result<TiledTilemap, Box<dyn Error>> {
|
||||||
|
@ -189,25 +210,33 @@ pub fn get_tiled_tilemap( path: &str ) -> Result<TiledTilemap, Box<dyn Error>> {
|
||||||
return Err( "invalid file: tile height is not 8 for --system md" )?
|
return Err( "invalid file: tile height is not 8 for --system md" )?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build tileset (current version assumes one tileset per level)
|
// Build tilesets (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 mut tilesets: Vec<TiledTileset> = vec![];
|
||||||
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" )?;
|
|
||||||
|
|
||||||
// Validate referenced tileset dimensions match map dimensions
|
for tileset in map.descendants().filter( | node | node.tag_name() == "tileset".into() ) {
|
||||||
// E.g. if your .tmx specifies 16x16, the acompanying .tsx file should agree
|
let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?;
|
||||||
let ( tsx_tile_width, tsx_tile_height ): ( usize, usize ) = (
|
let tileset_file = read_to_string( format!( "{}/{}", working_directory, tileset_source_path ) )?;
|
||||||
tileset.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?,
|
let tileset_document = roxmltree::Document::parse( &tileset_file )?;
|
||||||
tileset.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()?
|
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" )?;
|
||||||
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" )?
|
// 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 = tilesets;
|
||||||
let tileset = vec![ tileset ];
|
if tileset.is_empty() {
|
||||||
|
return Err( "invalid file: at least one tileset must be present in file" )?
|
||||||
|
}
|
||||||
|
|
||||||
// Get the layers
|
// Get the layers
|
||||||
let layers: Vec<Layer> = map.descendants()
|
let layers: Vec<Layer> = map.descendants()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{error::Error, convert::TryInto};
|
use std::{error::Error, convert::TryInto};
|
||||||
use image::GenericImageView;
|
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};
|
use super::converter::{TiledTilemap, Layer, SystemPlane};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,40 +16,80 @@ pub fn get_tiles( tilemap: &TiledTilemap ) -> Result<(Vec<u8>, Vec<u8>), Box<dyn
|
||||||
];
|
];
|
||||||
let mut all_tiles: Vec<u8> = vec![0; 32]; // --system md, start with a blank buffer tile
|
let mut all_tiles: Vec<u8> = vec![0; 32]; // --system md, start with a blank buffer tile
|
||||||
|
|
||||||
let tileset = tilemap.tileset.first().ok_or( "internal error: no tileset" )?;
|
for tileset in &tilemap.tileset {
|
||||||
let tiles_height = tileset.image.height() / 8; // --system md
|
let tiles_height = tileset.image.height() / 8; // --system md
|
||||||
let tiles_width = tileset.image.width() / 8; // --system md
|
let tiles_width = tileset.image.width() / 8; // --system md
|
||||||
|
|
||||||
for tile_y in 0..tiles_height {
|
// all this copy pasted code, it's so over
|
||||||
for tile_x in 0..tiles_width {
|
if matches!( tileset.tile_order, TileOrder::Sprite ) {
|
||||||
let tile_index = ( tile_y * tiles_width ) + tile_x;
|
// Sprite iteration order
|
||||||
let tile = tileset.image.clone().crop( tile_x * 8, tile_y * 8, 8, 8 );
|
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)
|
// Fake palette (see below)
|
||||||
let mut fake: [u16; 16] = [ 0; 16 ];
|
let mut fake: [u16; 16] = [ 0; 16 ];
|
||||||
let selected_pal = tileset.palettes[ tile_index as usize ];
|
let selected_pal = tileset.palettes[ tile_index as usize ];
|
||||||
|
|
||||||
let tile_bin = image_to_tiles(
|
let tile_bin = image_to_tiles(
|
||||||
&tile,
|
&tile,
|
||||||
{
|
{
|
||||||
// Determine if palette is used here or it is a dummy palette
|
// Determine if palette is used here or it is a dummy palette
|
||||||
if let Some( selected_pal ) = selected_pal {
|
if let Some( selected_pal ) = selected_pal {
|
||||||
&mut system_pals[ selected_pal as usize ]
|
&mut system_pals[ selected_pal as usize ]
|
||||||
} else {
|
} else {
|
||||||
// Fake-a-palette
|
// Fake-a-palette
|
||||||
// You will get an error in get_tilemap if you try to use this palette-less tile
|
// You will get an error in get_tilemap if you try to use this palette-less tile
|
||||||
&mut fake
|
&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 {
|
if let Ok( tile_bin ) = tile_bin {
|
||||||
return Err( format!( "palette {:?} full (try moving tile {} to another palette)", selected_pal, tile_index ) )?
|
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 {
|
// Fake palette (see below)
|
||||||
all_tiles.extend( tile_bin );
|
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<u8>, Vec<u8>), Box<dyn
|
||||||
* In `--system md`, this outputs tilemap B and then tilemap A
|
* In `--system md`, this outputs tilemap B and then tilemap A
|
||||||
*/
|
*/
|
||||||
pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
|
pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let tileset = tilemap.tileset.first().ok_or( "internal error: no tileset" )?;
|
let mut all_palettes: Vec<Option<u8>> = 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_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: _ } ) );
|
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<Vec<u8>, Box<dyn Error>>
|
||||||
|
|
||||||
// From the starting point of x, y "stamp" the target tile's indices
|
// From the starting point of x, y "stamp" the target tile's indices
|
||||||
let nametable_entry = target_tile;
|
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 {
|
if let Some( selected_pal ) = selected_pal {
|
||||||
nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry;
|
nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry;
|
||||||
} else {
|
} else {
|
||||||
|
@ -127,7 +170,7 @@ pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>>
|
||||||
|
|
||||||
// From the starting point of x, y "stamp" the target tile's indices
|
// From the starting point of x, y "stamp" the target tile's indices
|
||||||
let nametable_entry = target_tile;
|
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 {
|
if let Some( selected_pal ) = selected_pal {
|
||||||
nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry;
|
nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry;
|
||||||
} else {
|
} else {
|
||||||
|
@ -176,12 +219,18 @@ pub fn get_collision_map( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Er
|
||||||
* Get a helper .asm or .c file that ties all the level components together
|
* Get a helper .asm or .c file that ties all the level components together
|
||||||
*/
|
*/
|
||||||
pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -> Result<String, Box<dyn Error>> {
|
pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -> Result<String, Box<dyn Error>> {
|
||||||
let tileset = tilemap.tileset.first().ok_or( "internal error: no tileset" )?;
|
|
||||||
|
|
||||||
let version = env!( "CARGO_PKG_VERSION" );
|
let version = env!( "CARGO_PKG_VERSION" );
|
||||||
let level_label = symbol_to_pascal( level_name );
|
let level_label = symbol_to_pascal( level_name );
|
||||||
let ( width, height ) = ( tilemap.width, tilemap.height );
|
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
|
let file = format!( r#"; Level definition file
|
||||||
; Generated by reskit v{version}
|
; Generated by reskit v{version}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue