From 6fe634c8647fee9156c55495a01f202fdb595d5f Mon Sep 17 00:00:00 2001 From: ashley Date: Mon, 30 Oct 2023 17:00:15 -0400 Subject: [PATCH] WIP - New metatile converter --- src/reskit/cli/evaluator.rs | 4 +- src/reskit/level/converter.rs | 161 ++++++++++++++++++++++++------ src/reskit/level/system.rs | 183 ++++++++++++++++++++++------------ 3 files changed, 252 insertions(+), 96 deletions(-) diff --git a/src/reskit/cli/evaluator.rs b/src/reskit/cli/evaluator.rs index 5b6f9c9..4d4c39a 100644 --- a/src/reskit/cli/evaluator.rs +++ b/src/reskit/cli/evaluator.rs @@ -1,6 +1,6 @@ use std::{error::Error, fs::File, io::Write, path::Path, collections::HashMap}; use clap::Parser; -use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::print_good, level::{converter::get_tiled_tilemap, system::{get_tiles, get_code, get_tilemap, get_collision_map, get_objs, get_sprites}}, easing::get_cubic_bezier}; +use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::print_good, level::{converter::get_tiled_tilemap, system::{get_tiles, get_code, get_metatile_maps, get_collision_map, get_objs, get_sprites}}, easing::get_cubic_bezier}; use super::settings::{Args, Tools, TileOutputFormat, TileOrder}; pub fn run_command() -> Result<(), Box> { @@ -110,7 +110,7 @@ pub fn run_command() -> Result<(), Box> { print_good( "exported palettes.pal" ); let mut nametables_bin = File::create( format!( "{}nametables.map", output_directory ) )?; - nametables_bin.write_all( &get_tilemap( &tiled_file )? )?; + nametables_bin.write_all( &get_metatile_maps( &tiled_file )? )?; print_good( "exported nametables.map" ); let mut nametables_bin = File::create( format!( "{}collision.lvc", output_directory ) )?; diff --git a/src/reskit/level/converter.rs b/src/reskit/level/converter.rs index 81eb24e..9ff8f3c 100644 --- a/src/reskit/level/converter.rs +++ b/src/reskit/level/converter.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow}; +use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow, collections::HashSet}; use euclid::default::Rect; use image::{DynamicImage, GenericImageView}; use linked_hash_map::LinkedHashMap; @@ -8,6 +8,7 @@ use crate::reskit::{utility::print_warning, cli::settings::TileOrder}; #[derive(Debug)] pub struct TiledTilemap { pub tileset: Vec, + pub metatiles: Vec, pub layers: Vec, pub objects: Vec, pub collision: Vec>, @@ -15,6 +16,15 @@ pub struct TiledTilemap { pub height: usize } +#[derive(Debug, PartialEq)] +pub struct Metatile { + pub id: u32, + pub source: String, + pub width: u16, + pub height: u16, + pub tiles: Vec +} + #[derive(Debug)] pub struct Object { pub id: String, @@ -32,7 +42,7 @@ pub struct SpriteMetadata { #[derive(Debug)] pub struct TiledTileset { - pub first_gid: usize, + pub source: String, pub image: DynamicImage, pub palettes: Vec>, pub tile_order: TileOrder, @@ -47,7 +57,7 @@ pub enum SystemPlane { #[derive(Debug)] pub enum Layer { - Tile { + Metatile { system_plane: SystemPlane, tiles: Vec } @@ -95,11 +105,11 @@ fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result = data.into_iter().map( | string | Ok( string.trim().parse()? ) ).collect::< Result< Vec, Box > >()?; match layer_type.to_lowercase().as_str() { - "a" => Ok( Some( Layer::Tile { + "a" => Ok( Some( Layer::Metatile { system_plane: SystemPlane::MdPlaneA, tiles } ) ), - "b" => Ok( Some( Layer::Tile { + "b" => Ok( Some( Layer::Metatile { system_plane: SystemPlane::MdPlaneB, tiles } ) ), @@ -118,11 +128,51 @@ fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result Result> { - // Get the image for the tileset +fn get_tiles( tileset: Node, seen_sources: &mut HashSet, working_directory: &str ) -> Result, Box> { + // Get the image for the tileset and its source 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 = image::open( image_path )?; + let source = image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )?.to_string(); + + // If the file path ends in .tmx, it's a metatile definition. We need to hop to the .tmx file in source, then redefine layer + // to the single tileset in that metatile .tmx. + let full_path = format!( "{}/{}", working_directory, source ); + let formatted_path = Path::new( &full_path ); + if let Some( extension ) = formatted_path.extension() { + let extension = extension.to_string_lossy(); + if extension == "tmx" { + // Case where source is a nested tileset. Open the .tmx file + let file = read_to_string( full_path.clone() )?; + let tmx_document = roxmltree::Document::parse( &file )?; + + // Obtain the single (first) + let tileset = tmx_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: metatile has no tileset" )?; + let tileset_source = tileset.attribute( "source" ).ok_or( "invalid file: metatile tileset has no source" )?.to_string(); + + // tileset_source should already point to the _actual_ source we need. Was this tileset source already seen? If so, bail + if seen_sources.contains( &tileset_source ) { + return Ok( None ) + } else { + seen_sources.insert( tileset_source.clone() ); + } + + // Load nested tileset into roxmltree document + let file = read_to_string( format!( "{}/{}", working_directory, tileset_source ) )?; + let nested_tileset = roxmltree::Document::parse( &file )?; + let tileset = nested_tileset.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: metatile tileset has no tileset" )?; + + // Return nested tileset from tmx file + let result = get_tiles( tileset, seen_sources, working_directory )?; + if let Some( mut result ) = result { + result.source = tileset_source.to_owned(); + return Ok( Some( result ) ) + } + + return Ok( result ) + } + } + + // Case where source is an actual image + let image = image::open( full_path )?; // Image must be a multiple of 8 (--system md) if image.width() % 8 != 0 { return Err( "invalid file: tileset width not multiple of 8" )? } @@ -206,7 +256,56 @@ fn get_tiles( tileset: Node, first_gid: usize, working_directory: &str ) -> Resu TileOrder::Tile }; - Ok( TiledTileset { first_gid, image, palettes, tile_order, sprite_metadata } ) + Ok( Some( TiledTileset { source: format!( "" ), image, palettes, tile_order, sprite_metadata } ) ) +} + +pub fn get_metatile( id: u32, tileset: Node, working_directory: &str ) -> Result, Box> { + // Get the image for the tileset and its source + let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?; + let source = image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )?.to_string(); + let full_path = format!( "{}/{}", working_directory, source ); + let formatted_path = Path::new( &full_path ); + if let Some( extension ) = formatted_path.extension() { + let extension = extension.to_string_lossy(); + if extension == "tmx" { + // Case where source is a nested tileset. Open the .tmx file + let file = read_to_string( full_path.clone() )?; + let tmx_document = roxmltree::Document::parse( &file )?; + + // Get width and height from item + let map_item = tmx_document.descendants().find( | node | node.tag_name() == "map".into() ).ok_or( "invalid file: no map element in metatile" )?; + let width: u16 = map_item.attribute( "width" ).ok_or( "invalid file: map of metatile has no width attribute" )?.parse()?; + let height: u16 = map_item.attribute( "height" ).ok_or( "invalid file: map of metatile has no height attribute" )?.parse()?; + + // Obtain the single (first) + let tileset_count = tmx_document.descendants().filter( | node | node.tag_name() == "tileset".into() ).count(); + if tileset_count > 1 { + return Err( "invalid file: only a single tileset is valid for a metatile file" )? + } + let tileset = tmx_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: metatile has no tileset" )?; + let source = tileset.attribute( "source" ).ok_or( "invalid file: metatile tileset has no source" )?.to_string(); + + // Create metatile object here + let data = tmx_document.descendants().find( | node | node.tag_name() == "data".into() ).ok_or( "invalid file: metatile has no data" )?; + if data.attribute( "encoding" ) != Some( "csv" ) { + return Err( "invalid file: metatile data not in csv format" )? + } + let data = data.text().ok_or( "invalid file: data node has no text" )?; + let values: Vec<&str> = data.split( "," ).collect(); + // Subtract 1 to each value to account for firstgid. This is a sane assumption because a valid metatile file only contains a single tileset. + let tiles: Vec = values.into_iter().map( | string | { + let val = string.trim().parse::().expect( "fatal: non-u32 value in metatile" ); + if val == 0 { + val + } else { + val - 1 + } + } ).collect(); + return Ok( Some( Metatile { id, source, width, height, tiles } ) ) + } + } + + Ok( None ) } pub fn get_objs( node: &Node, object_fields: &Vec<&str> ) -> Result, Box> { @@ -306,25 +405,34 @@ pub fn get_tiled_tilemap( path: &str, object_fields: &Vec<&str> ) -> Result = vec![]; - + let mut metatiles: Vec = vec![]; + let mut seen_sources: HashSet = HashSet::new(); for tileset in map.descendants().filter( | node | node.tag_name() == "tileset".into() ) { + let tileset_first_gid = tileset.attribute( "firstgid" ).ok_or( "invalid file: no tileset firstgid" )?; 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 may instead contain metatiles + // First, load tilesets instead of metatiles + // If tileset_source_path was seen, don't proceed + if !seen_sources.contains( tileset_source_path ) { + seen_sources.insert( tileset_source_path.to_owned() ); + + if let Some( mut tiles ) = get_tiles( tileset.clone(), &mut seen_sources, &working_directory )? { + if tiles.source == "" { + tiles.source = tileset_source_path.to_owned(); + } + + tilesets.push( tiles ); + } } - tilesets.push( get_tiles( tileset, first_gid, &working_directory )? ); + // Then, check for metatiles + if let Some( metatile ) = get_metatile( tileset_first_gid.parse()?, tileset, &working_directory )? { + metatiles.push( metatile ); + } } let tileset = tilesets; @@ -381,16 +489,7 @@ pub fn get_tiled_tilemap( path: &str, object_fields: &Vec<&str> ) -> Result Result<(Vec, Vec), Box>> + * 2 bytes: The offset to the instances table + * (n) 2 byte offsets: For the number of defined metatiles, 16-bit offsets into the file + * pointing to the metatile definition. Index into this table = the metatile ID. + * + * <<< DEFINITIONS >>> + * For each metatile definition: + * 2 bytes: Width of the metatile, in 16-bit words. + * 2 bytes: Height of the metatile, in 16-bit words. + * (n) bytes: The nametable definition for the metatile. Stamp this into vram on each layer. + * + * <<< INSTANCES >>> + * For B, then A layers: + * 2 bytes: Number of metatile instances in this layer. + * For each metatile instance: + * 2 bytes: Metatile ID. + * 2 bytes: X coordinate (world, divide by 8 to get tile coordinate). + * 2 bytes: Y coordinate (world, divide by 8 to get tile coordinate). */ -pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result, Box> { - let mut all_palettes: Vec> = vec![]; - for tileset in &tilemap.tileset { - all_palettes.extend( tileset.palettes.iter() ); +pub fn get_metatile_maps( tilemap: &TiledTilemap ) -> Result, Box> { + // Assemble header of offsets (as we go) + let header_offset = ( tilemap.metatiles.len() * 2 ) + 2; + let mut header: Vec = vec![]; + + // Assemble definitions + let mut definitions: Vec = vec![]; + for metatile in &tilemap.metatiles { + let mut subdefinitions: Vec = vec![]; + subdefinitions.extend( metatile.width.to_be_bytes() ); + subdefinitions.extend( metatile.height.to_be_bytes() ); + + let ( tilemap_index, palettes ) = metatile_tilemap_index( &metatile.source, &tilemap.tileset )?; + for tile in &metatile.tiles { + let palette = { + if *tile == 0 { + 0 + } else { + let palette = palettes.get( *tile as usize ).ok_or( "internal error: no correlation between tile index and palette index" )?; + if let Ok( palette ) = palette.ok_or( "" ) { + palette + } else { + println!( "{:?}", palettes ); + return Err( format!( "invalid file: tile \"{}\" in metatile tilemap does not name reskit-palette attribute", tile ) )?; + } + } + }; + + let nametable_entry: u16 = ( ( palette as u16 ) << 13 ) | ( ( tile + tilemap_index as u32 ) as u16 ); + subdefinitions.extend( nametable_entry.to_be_bytes() ); + } + + let this_offset = header_offset + definitions.len(); + header.extend( ( this_offset as u16 ).to_be_bytes() ); + + definitions.extend( subdefinitions ); } - 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: _ } ) ); + // Assemble instances + let mut instances: Vec = vec![]; - // Each entry in a `--system md` tilemap is 16 bits - let mut total_nametable: Vec = Vec::new(); - - let mut nametable: Vec = vec![ 0; tilemap.width * tilemap.height ]; + let layer_b: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Metatile { system_plane: SystemPlane::MdPlaneB, tiles: _ } ) ); if let Some( layer_b ) = layer_b { - let layer_b = match layer_b { - Layer::Tile { system_plane: _, tiles } => tiles, - _ => return Err( "internal error: invalid object type" )? - }; + let tiles = match layer_b { Layer::Metatile { system_plane: _, tiles } => tiles }; + let mut subdefinitions: Vec = vec![]; - for y in 0..tilemap.height { - for x in 0..tilemap.width { - let target_tile: u32 = *layer_b.get( ( y * tilemap.width ) + x ).ok_or( "internal error: invalid data in tilemap" )?; - let target_tile: u16 = target_tile.try_into()?; - if target_tile > 0 { - let source_tile = target_tile - 1; + for tile_y in 0..tilemap.height { + for tile_x in 0..tilemap.width { + let item_at = tiles.get( ( tile_y * tilemap.width ) + tile_x ).ok_or( "internal error: tilemap does not correlate to width/height" )?; + if *item_at != 0 { + // What index-ID was that? + let index_id = tilemap.metatiles.iter().position( | metatile | metatile.id == *item_at ).ok_or( "invalid file: metatile id not found" )?; - // From the starting point of x, y "stamp" the target tile's indices - let nametable_entry = target_tile; - 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 { - return Err( format!( "invalid setting: tile {} in tileset has no defined palette", source_tile ) )? - } + // Write the data to subdefinitions + subdefinitions.extend( ( index_id as u16 ).to_be_bytes() ); + subdefinitions.extend( ( ( tile_x * 8 ) as u16 ).to_be_bytes() ); + subdefinitions.extend( ( ( tile_y * 8 ) as u16 ).to_be_bytes() ); } } } - } - total_nametable.extend( nametable.iter() ); - // Just do the same for layer a - // Copy pasted because i'm lazy and tired - let mut nametable: Vec = vec![ 0; tilemap.width * tilemap.height ]; + instances.extend( ( ( subdefinitions.len() / 6 ) as u16 ).to_be_bytes() ); + instances.extend( subdefinitions ); + } else { + instances.extend( ( 0 as u16 ).to_be_bytes() ); + } + + let layer_a: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Metatile { system_plane: SystemPlane::MdPlaneA, tiles: _ } ) ); if let Some( layer_a ) = layer_a { - let layer_a = match layer_a { - Layer::Tile { system_plane: _, tiles } => tiles, - _ => return Err( "internal error: invalid object type" )? - }; + let tiles = match layer_a { Layer::Metatile { system_plane: _, tiles } => tiles }; + let mut subdefinitions: Vec = vec![]; - for y in 0..tilemap.height { - for x in 0..tilemap.width { - let target_tile: u32 = *layer_a.get( ( y * tilemap.width ) + x ).ok_or( "internal error: invalid data in tilemap" )?; - let target_tile: u16 = target_tile.try_into()?; - if target_tile > 0 { - let source_tile = target_tile - 1; + for tile_y in 0..tilemap.height { + for tile_x in 0..tilemap.width { + let item_at = tiles.get( ( tile_y * tilemap.width ) + tile_x ).ok_or( "internal error: tilemap does not correlate to width/height" )?; + if *item_at != 0 { + // What index-ID was that? + let index_id = tilemap.metatiles.iter().position( | metatile | metatile.id == *item_at ).ok_or( "invalid file: metatile id not found" )?; - // From the starting point of x, y "stamp" the target tile's indices - let nametable_entry = target_tile; - 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 { - return Err( format!( "invalid setting: tile {} in tileset has no defined palette", source_tile ) )? - } + // Write the data to subdefinitions + subdefinitions.extend( ( index_id as u16 ).to_be_bytes() ); + subdefinitions.extend( ( ( tile_x * 8 ) as u16 ).to_be_bytes() ); + subdefinitions.extend( ( ( tile_y * 8 ) as u16 ).to_be_bytes() ); } } } - } - total_nametable.extend( nametable.iter() ); - // Convert the u16's to a series of u8 data - let mut result: Vec = Vec::new(); - for i in 0..total_nametable.len() { - let bytes = total_nametable[ i ].to_be_bytes(); - for i in 0..2 { - result.push( bytes[ i ] ); - } + instances.extend( ( ( subdefinitions.len() / 6 ) as u16 ).to_be_bytes() ); + instances.extend( subdefinitions ); + } else { + instances.extend( ( 0 as u16 ).to_be_bytes() ); } + + // Write the final result + let mut result: Vec = vec![]; + let offset_to_instances = header_offset + definitions.len(); + result.extend( ( offset_to_instances as u16 ).to_be_bytes() ); + result.extend( header ); + result.extend( definitions ); + result.extend( instances ); + Ok( result ) } +fn metatile_tilemap_index( target_source: &str, tilesets: &Vec ) -> Result<(usize, Vec>), Box> { + // Everything starts at 1 for the blank buffer tile + let mut index: usize = 1; + + for tileset in tilesets { + if tileset.source == target_source { + return Ok( ( index, tileset.palettes.clone() ) ) + } else { + index += ( ( tileset.image.width() / 8 ) * ( tileset.image.height() / 8 ) ) as usize; + } + } + + Err( format!( "internal error: could not find metatile with target source \"{}\"", target_source ) )? +} + /** * Get the .lvc collision map (u8 sized for the map dimensions, collision areas are either 0 or 1) */