WIP - New metatile converter

stinkhead7ds-wip1
Ashley N. 2023-10-30 17:00:15 -04:00
parent 18f1e20aa1
commit 6fe634c864
3 changed files with 252 additions and 96 deletions

View File

@ -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<dyn Error>> {
@ -110,7 +110,7 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
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 ) )?;

View File

@ -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<TiledTileset>,
pub metatiles: Vec<Metatile>,
pub layers: Vec<Layer>,
pub objects: Vec<Object>,
pub collision: Vec<Rect<u16>>,
@ -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<u32>
}
#[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<Option<u8>>,
pub tile_order: TileOrder,
@ -47,7 +57,7 @@ pub enum SystemPlane {
#[derive(Debug)]
pub enum Layer {
Tile {
Metatile {
system_plane: SystemPlane,
tiles: Vec<u32>
}
@ -95,11 +105,11 @@ fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result<Optio
let tiles: Vec<u32> = data.into_iter().map( | string | Ok( string.trim().parse()? ) ).collect::< Result< Vec<u32>, Box<dyn Error> > >()?;
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<Optio
}
}
fn get_tiles( tileset: Node, first_gid: usize, working_directory: &str ) -> Result<TiledTileset, Box<dyn Error>> {
// Get the image for the tileset
fn get_tiles( tileset: Node, seen_sources: &mut HashSet<String>, working_directory: &str ) -> Result<Option<TiledTileset>, Box<dyn Error>> {
// 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) <tileset>
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<Option<Metatile>, Box<dyn Error>> {
// 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 <map> 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) <tileset>
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<u32> = values.into_iter().map( | string | {
let val = string.trim().parse::<u32>().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<Vec<Object>, Box<dyn Error>> {
@ -306,25 +405,34 @@ pub fn get_tiled_tilemap( path: &str, object_fields: &Vec<&str> ) -> Result<Tile
// Build tilesets (current version assumes one tileset per level)
let mut tilesets: Vec<TiledTileset> = vec![];
let mut metatiles: Vec<Metatile> = vec![];
let mut seen_sources: HashSet<String> = 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<Tile
Vec::new()
};
Ok(
TiledTilemap {
tileset,
layers,
objects,
collision,
width,
height
}
)
Ok( TiledTilemap { tileset, metatiles, layers, objects, collision, width, height } )
} else {
Err( "invalid file: this does not appear to be valid Tiled .tmx file" )?
}

View File

@ -1,4 +1,4 @@
use std::{error::Error, convert::TryInto, collections::HashMap};
use std::{error::Error, collections::HashMap};
use image::GenericImageView;
use crate::reskit::{tileset::image_to_tiles, utility::symbol_to_pascal, cli::settings::TileOrder};
use super::converter::{TiledTilemap, Layer, SystemPlane, TiledTileset};
@ -109,90 +109,147 @@ pub fn get_tiles( tilemap: &TiledTilemap ) -> Result<(Vec<u8>, Vec<u8>), Box<dyn
}
/**
* Output the .map file defining the hardware tilemap for the given Tiled Tilemap.
* In `--system md`, this outputs tilemap B and then tilemap A
* Get the .map file containing metatile definitions, as well as B and A metatile-based maps.
*
* Format:
* <<< HEADER >>>
* 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<Vec<u8>, Box<dyn Error>> {
let mut all_palettes: Vec<Option<u8>> = vec![];
for tileset in &tilemap.tileset {
all_palettes.extend( tileset.palettes.iter() );
pub fn get_metatile_maps( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
// Assemble header of offsets (as we go)
let header_offset = ( tilemap.metatiles.len() * 2 ) + 2;
let mut header: Vec<u8> = vec![];
// Assemble definitions
let mut definitions: Vec<u8> = vec![];
for metatile in &tilemap.metatiles {
let mut subdefinitions: Vec<u8> = 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<u8> = vec![];
// Each entry in a `--system md` tilemap is 16 bits
let mut total_nametable: Vec<u16> = Vec::new();
let mut nametable: Vec<u16> = 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<u8> = 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<u16> = 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<u8> = 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<u8> = 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<u8> = 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<TiledTileset> ) -> Result<(usize, Vec<Option<u8>>), Box<dyn Error>> {
// 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)
*/