diff --git a/src/reskit/level/converter.rs b/src/reskit/level/converter.rs index 0a44134..30a34ea 100644 --- a/src/reskit/level/converter.rs +++ b/src/reskit/level/converter.rs @@ -1,23 +1,23 @@ use std::{error::Error, fs::read_to_string}; -use image::DynamicImage; +use image::{DynamicImage, GenericImageView}; use roxmltree::Node; use crate::reskit::utility::print_warning; use super::ecs; #[derive(Debug)] pub struct TiledTilemap { - tileset: TiledTileset, - layers: Vec, + pub tileset: TiledTileset, + pub layers: Vec, ecs: Vec, - width: usize, - height: usize, - tile_width: usize, - tile_height: usize + pub width: usize, + pub height: usize, + pub tile_width: usize, + pub tile_height: usize } #[derive(Debug)] pub struct TiledTileset { - image: DynamicImage + pub image: DynamicImage } #[derive(Debug)] @@ -156,6 +156,10 @@ pub fn get_tiled_tilemap( path: &str ) -> Result> { let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?; let image = image::open( image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )? )?; + // Image must be a multiple of 8 (md system) + if image.width() % 8 != 0 { return Err( "invalid file: tileset width not multiple of 8" )? } + if image.height() % 8 != 0 { return Err( "invalid file: tileset height not multiple of 8" )? } + // Get the layers let layers: Vec = map.descendants() .filter( | node | node.tag_name() == "layer".into() ) diff --git a/src/reskit/level/mod.rs b/src/reskit/level/mod.rs index 4f42881..0621a95 100644 --- a/src/reskit/level/mod.rs +++ b/src/reskit/level/mod.rs @@ -1,2 +1,3 @@ pub mod converter; -pub mod ecs; \ No newline at end of file +pub mod ecs; +pub mod system; \ No newline at end of file diff --git a/src/reskit/level/system.rs b/src/reskit/level/system.rs new file mode 100644 index 0000000..8ae8ad0 --- /dev/null +++ b/src/reskit/level/system.rs @@ -0,0 +1,152 @@ +use std::{error::Error, convert::TryInto}; +use crate::reskit::tileset::image_to_tiles; +use super::converter::{TiledTilemap, Layer, SystemPlane}; + +/** + * Output the .bin file (using `tileset` tool to build it) containing each of the tiles + * in the Tiled Editor tileset. + */ +pub fn get_tiles( tilemap: &TiledTilemap ) -> Result, Box> { + let mut palette: [u16; 16] = [ 0; 16 ]; + let mut all_tiles: Vec = Vec::new(); + + for tile_y in 0..tilemap.tile_height { + for tile_x in 0..tilemap.tile_width { + let tile = tilemap.tileset.image.clone().crop( + tile_x as u32, + tile_y as u32, + tilemap.tile_width as u32, + tilemap.tile_height as u32 + ); + + let tile_bin = image_to_tiles( + &tile, + &mut palette, + "tile" + ); + + all_tiles.extend( tile_bin ); + } + } + + // Define result and write palette to top of result + let mut result: Vec = Vec::new(); + for i in 0..palette.len() { + let bytes = palette[ i ].to_be_bytes(); + for i in 0..2 { + result.push( bytes[ i ] ); + } + } + + // Extend all tiles onto result + result.extend( all_tiles ); + + Ok( result ) +} + +/** + * Output the .map file defining the hardware tilemap for the given Tiled Tilemap. + * In `--system md`, this outputs tilemap B and then tilemap A + */ +pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result, Box> { + 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 md_height_per_tile = tilemap.tile_height / 8; + let md_width_per_tile = tilemap.tile_width / 8; + + // Each entry in a `--system md` tilemap is 16 bits + let mut nametable: Vec = vec![ 0; ( tilemap.width * md_width_per_tile ) * ( tilemap.height * md_height_per_tile ) ]; + + 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" )? + }; + + 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 wide_tile_size = md_height_per_tile as u16 * md_width_per_tile as u16; + let source_tile = ( target_tile - 1 ) * wide_tile_size; + let x = x * md_width_per_tile; + let y = y * md_height_per_tile; + + // From the starting point of x, y "stamp" the target tile's indices + let mut revolving_counter = 0; + for y_stamp in y..( y + md_height_per_tile ) { + for x_stamp in x..( x + md_width_per_tile ) { + nametable[ ( y_stamp * ( tilemap.width * md_width_per_tile ) ) + x_stamp ] = source_tile + revolving_counter; + revolving_counter = ( revolving_counter + 1 ) % wide_tile_size; + } + } + } + } + } + } + + // Just do the same for layer a + // Copy pasted because i'm lazy and tired + 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" )? + }; + + 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 wide_tile_size = md_height_per_tile as u16 * md_width_per_tile as u16; + let source_tile = ( target_tile - 1 ) * wide_tile_size; + let x = x * md_width_per_tile; + let y = y * md_height_per_tile; + + // From the starting point of x, y "stamp" the target tile's indices + let mut revolving_counter = 0; + for y_stamp in y..( y + md_height_per_tile ) { + for x_stamp in x..( x + md_width_per_tile ) { + nametable[ ( y_stamp * ( tilemap.width * md_width_per_tile ) ) + x_stamp ] = source_tile + revolving_counter; + revolving_counter = ( revolving_counter + 1 ) % wide_tile_size; + } + } + } + } + } + } + + // Convert the u16's to a series of u8 data + let mut result: Vec = Vec::new(); + for i in 0..nametable.len() { + let bytes = nametable[ i ].to_be_bytes(); + for i in 0..2 { + result.push( bytes[ i ] ); + } + } + Ok( result ) +} + +/** + * Get the .lvc collision map (u8 sized for the map dimensions, collision areas are either 0 or 1) + */ +pub fn get_collision_map( tilemap: &TiledTilemap ) -> Result, Box> { + let mut result: Vec = Vec::new(); + + let collision: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Collision { tiles: _ } ) ); + if let Some( collision ) = collision { + let collision = match collision { + Layer::Collision { tiles } => tiles, + _ => return Err( "internal error: invalid object type" )? + }; + + for collision_data in collision { + let collision_data: u8 = if *collision_data > 0 { 1 } else { 0 }; + result.push( collision_data ); + } + } + + Ok( result ) +} \ No newline at end of file diff --git a/src/reskit/tileset.rs b/src/reskit/tileset.rs index c97f976..c510476 100644 --- a/src/reskit/tileset.rs +++ b/src/reskit/tileset.rs @@ -1,11 +1,11 @@ use crate::reskit::utility; -use std::process::exit; +use std::{process::exit, error::Error}; use std::fs; use std::fs::File; use std::io::Write; use image::{ GenericImageView, DynamicImage }; -fn color_to_palette( r: u16, g: u16, b: u16, palette: &mut [u16; 16] ) -> u32 { +pub fn color_to_palette( r: u16, g: u16, b: u16, palette: &mut [u16; 16] ) -> u32 { let final_val = ( ( r & 0x00F0 ) >> 4 ) | ( g & 0x00F0 ) | @@ -41,7 +41,7 @@ fn get_pixel( image: &DynamicImage, palette: &mut [u16; 16], x: u32, y: u32 ) -> color_to_palette( pixel[ 0 ].into(), pixel[ 1 ].into(), pixel[ 2 ].into(), palette ) } -fn output_bin( image_filename: &str, output_filename: &str, palette: [u16; 16], body: Vec ) { +pub fn output_bin( output_filename: &str, palette: [u16; 16], body: Vec ) -> Result<(), Box> { let mut output_palette: Vec< u8 > = Vec::new(); for i in 0..palette.len() { let bytes = palette[ i ].to_be_bytes(); @@ -54,13 +54,14 @@ fn output_bin( image_filename: &str, output_filename: &str, palette: [u16; 16], if let Ok( mut output_file ) = output_try { output_file.write( &output_palette ).unwrap(); output_file.write( &body ).unwrap(); - utility::print_good( format!( "converted file {}", image_filename ).as_str() ); + + Ok( () ) } else { - utility::print_error( format!( "could not open filename for output {}", output_filename ).as_str() ); + return Err( format!( "could not open filename for output {}", output_filename ).as_str() )?; } } -fn output_inc( image_filename: &str, output_filename: &str, palette: [u16; 16], body: Vec ) { +pub fn output_inc( output_filename: &str, palette: [u16; 16], body: Vec ) -> Result<(), Box> { let mut output_palette: Vec< u8 > = Vec::new(); for i in 0..palette.len() { let bytes = palette[ i ].to_be_bytes(); @@ -111,71 +112,85 @@ fn output_inc( image_filename: &str, output_filename: &str, palette: [u16; 16], fs::write( output_filename.to_string() + ".h", output_h ).expect( "Could not write header file" ); fs::write( output_filename.to_string() + ".c", output_c ).expect( "Could not write source file" ); - utility::print_good( format!( "converted file {}", image_filename ).as_str() ); + Ok( () ) +} + +pub fn image_to_tiles( img: &DynamicImage, palette: &mut [u16; 16], tile_order: &str ) -> Vec { + let ( mut max_x, mut max_y ) = img.dimensions(); + if max_x % 8 != 0 { max_x = ( 8 * ( max_x / 8 ) ) + ( 8 - ( max_x % 8 ) ); } + if max_y % 8 != 0 { max_y = ( 8 * ( max_y / 8 ) ) + ( 8 - ( max_y % 8 ) ); } + + let mut body: Vec = Vec::new(); + + if tile_order == "sprite" { + /* + * Sprite order: + * 1 3 + * 2 4 + */ + for x in ( 0..max_x ).step_by( 8 ) { + for y in ( 0..max_y ).step_by( 8 ) { + for cell_y in 0..8 { + let mut series: u32 = 0; + + for cell_x in 0..8 { + let nibble: u32 = get_pixel( &img, palette, cell_x + x, cell_y + y ) << ( ( 7 - cell_x ) * 4 ); + series = series | nibble; + } + + let bytes = series.to_be_bytes(); + for i in 0..4 { + body.push( bytes[ i ] ); + } + } + } + } + } else { + /* + * Tile order: + * 1 2 + * 3 4 + */ + for y in ( 0..max_y ).step_by( 8 ) { + for x in ( 0..max_x ).step_by( 8 ) { + for cell_y in 0..8 { + let mut series: u32 = 0; + + for cell_x in 0..8 { + let nibble: u32 = get_pixel( &img, palette, cell_x + x, cell_y + y ) << ( ( 7 - cell_x ) * 4 ); + series = series | nibble; + } + + let bytes = series.to_be_bytes(); + for i in 0..4 { + body.push( bytes[ i ] ); + } + } + } + } + } + + body } pub fn generate( image_filename: &str, output_filename: &str, output_mode: &str, tile_order: &str ) { let img = image::open( image_filename ); if let Ok( img ) = img { - let ( mut max_x, mut max_y ) = img.dimensions(); - if max_x % 8 != 0 { max_x = ( 8 * ( max_x / 8 ) ) + ( 8 - ( max_x % 8 ) ); } - if max_y % 8 != 0 { max_y = ( 8 * ( max_y / 8 ) ) + ( 8 - ( max_y % 8 ) ); } - let mut palette: [u16; 16] = [ 0; 16 ]; - let mut body: Vec< u8 > = Vec::new(); - - if tile_order == "sprite" { - /* - * Tile order: - * 1 3 - * 2 4 - */ - for x in ( 0..max_x ).step_by( 8 ) { - for y in ( 0..max_y ).step_by( 8 ) { - for cell_y in 0..8 { - let mut series: u32 = 0; - - for cell_x in 0..8 { - let nibble: u32 = get_pixel( &img, &mut palette, cell_x + x, cell_y + y ) << ( ( 7 - cell_x ) * 4 ); - series = series | nibble; - } - - let bytes = series.to_be_bytes(); - for i in 0..4 { - body.push( bytes[ i ] ); - } - } - } - } - } else { - /* - * Tile order: - * 1 2 - * 3 4 - */ - for y in ( 0..max_y ).step_by( 8 ) { - for x in ( 0..max_x ).step_by( 8 ) { - for cell_y in 0..8 { - let mut series: u32 = 0; - - for cell_x in 0..8 { - let nibble: u32 = get_pixel( &img, &mut palette, cell_x + x, cell_y + y ) << ( ( 7 - cell_x ) * 4 ); - series = series | nibble; - } - - let bytes = series.to_be_bytes(); - for i in 0..4 { - body.push( bytes[ i ] ); - } - } - } - } - } + let body = image_to_tiles( &img, &mut palette, tile_order ); if output_mode == "bin" { - output_bin( image_filename, output_filename, palette, body ); + if let Err( err ) = output_bin( output_filename, palette, body ) { + utility::print_error( &format!( "{}", err ) ); + } + + utility::print_good( format!( "converted file {}", image_filename ).as_str() ); } else if output_mode == "inc" { - output_inc( image_filename, output_filename, palette, body ); + if let Err( err ) = output_inc( output_filename, palette, body ) { + utility::print_error( &format!( "{}", err ) ); + } + + utility::print_good( format!( "converted file {}", image_filename ).as_str() ); } else { utility::print_error( format!( "invalid output mode {}", output_mode ).as_str() ); }