Level exports - tiles, nametables, collision

stinkhead7ds
Ashley N. 2023-09-13 00:58:41 -04:00
parent dc10213f75
commit f7b9216680
4 changed files with 243 additions and 71 deletions

View File

@ -1,23 +1,23 @@
use std::{error::Error, fs::read_to_string}; use std::{error::Error, fs::read_to_string};
use image::DynamicImage; use image::{DynamicImage, GenericImageView};
use roxmltree::Node; use roxmltree::Node;
use crate::reskit::utility::print_warning; use crate::reskit::utility::print_warning;
use super::ecs; use super::ecs;
#[derive(Debug)] #[derive(Debug)]
pub struct TiledTilemap { pub struct TiledTilemap {
tileset: TiledTileset, pub tileset: TiledTileset,
layers: Vec<Layer>, pub layers: Vec<Layer>,
ecs: Vec<ecs::Entity>, ecs: Vec<ecs::Entity>,
width: usize, pub width: usize,
height: usize, pub height: usize,
tile_width: usize, pub tile_width: usize,
tile_height: usize pub tile_height: usize
} }
#[derive(Debug)] #[derive(Debug)]
pub struct TiledTileset { pub struct TiledTileset {
image: DynamicImage pub image: DynamicImage
} }
#[derive(Debug)] #[derive(Debug)]
@ -156,6 +156,10 @@ pub fn get_tiled_tilemap( path: &str ) -> Result<TiledTilemap, Box<dyn Error>> {
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 = image::open( image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )? )?; 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 // Get the layers
let layers: Vec<Layer> = map.descendants() let layers: Vec<Layer> = map.descendants()
.filter( | node | node.tag_name() == "layer".into() ) .filter( | node | node.tag_name() == "layer".into() )

View File

@ -1,2 +1,3 @@
pub mod converter; pub mod converter;
pub mod ecs; pub mod ecs;
pub mod system;

152
src/reskit/level/system.rs Normal file
View File

@ -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<Vec<u8>, Box<dyn Error>> {
let mut palette: [u16; 16] = [ 0; 16 ];
let mut all_tiles: Vec<u8> = 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<u8> = 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<Vec<u8>, Box<dyn Error>> {
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<u16> = 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<u8> = 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<Vec<u8>, Box<dyn Error>> {
let mut result: Vec<u8> = 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 )
}

View File

@ -1,11 +1,11 @@
use crate::reskit::utility; use crate::reskit::utility;
use std::process::exit; use std::{process::exit, error::Error};
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use image::{ GenericImageView, DynamicImage }; 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 = let final_val =
( ( r & 0x00F0 ) >> 4 ) | ( ( r & 0x00F0 ) >> 4 ) |
( g & 0x00F0 ) | ( 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 ) 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<u8> ) { pub fn output_bin( output_filename: &str, palette: [u16; 16], body: Vec<u8> ) -> Result<(), Box<dyn Error>> {
let mut output_palette: Vec< u8 > = Vec::new(); let mut output_palette: Vec< u8 > = Vec::new();
for i in 0..palette.len() { for i in 0..palette.len() {
let bytes = palette[ i ].to_be_bytes(); 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 { if let Ok( mut output_file ) = output_try {
output_file.write( &output_palette ).unwrap(); output_file.write( &output_palette ).unwrap();
output_file.write( &body ).unwrap(); output_file.write( &body ).unwrap();
utility::print_good( format!( "converted file {}", image_filename ).as_str() );
Ok( () )
} else { } 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<u8> ) { pub fn output_inc( output_filename: &str, palette: [u16; 16], body: Vec<u8> ) -> Result<(), Box<dyn Error>> {
let mut output_palette: Vec< u8 > = Vec::new(); let mut output_palette: Vec< u8 > = Vec::new();
for i in 0..palette.len() { for i in 0..palette.len() {
let bytes = palette[ i ].to_be_bytes(); 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() + ".h", output_h ).expect( "Could not write header file" );
fs::write( output_filename.to_string() + ".c", output_c ).expect( "Could not write source 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<u8> {
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<u8> = 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 ) { pub fn generate( image_filename: &str, output_filename: &str, output_mode: &str, tile_order: &str ) {
let img = image::open( image_filename ); let img = image::open( image_filename );
if let Ok( img ) = img { 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 palette: [u16; 16] = [ 0; 16 ];
let mut body: Vec< u8 > = Vec::new(); let body = image_to_tiles( &img, &mut palette, tile_order );
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 ] );
}
}
}
}
}
if output_mode == "bin" { 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" { } 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 { } else {
utility::print_error( format!( "invalid output mode {}", output_mode ).as_str() ); utility::print_error( format!( "invalid output mode {}", output_mode ).as_str() );
} }