Export sprite table

stinkhead7ds
Ashley N. 2023-09-24 13:52:37 -04:00
parent 485d1d0304
commit 73b5ba6cc7
4 changed files with 125 additions and 11 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_ecs}}};
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_ecs, get_sprites}}};
use super::settings::{Args, Tools, TileOutputFormat, TileOrder};
pub fn run_command() -> Result<(), Box<dyn Error>> {
@ -65,11 +65,12 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
print_good( "all files converted successfully" );
}
Tools::Level { input_file, output_directory, component_ids, attribute_ids, symbol_ids, console: _ } => {
Tools::Level { input_file, output_directory, component_ids, attribute_ids, symbol_ids, sprite_ids, console: _ } => {
// Clap can't do it. Sad!
let mut all_components: HashMap<String, u8> = HashMap::new();
let mut all_symbols: HashMap<String, u16> = HashMap::new();
let mut all_attributes: HashMap<String, Vec<String>> = HashMap::new();
let mut all_sprites: HashMap<String, u16> = HashMap::new();
for pair in component_ids {
let components: Vec<&str> = pair.split( "=" ).collect();
if components.len() != 2 {
@ -103,10 +104,22 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
all_symbols.insert( symbol_name.to_owned(), symbol_id );
}
for pair in sprite_ids {
let symbols: Vec<&str> = pair.split( "=" ).collect();
if symbols.len() != 2 {
return Err( format!( "invalid format for symbol_ids: {}", pair ) )?;
}
let symbol_name = symbols[ 0 ];
let symbol_id: u16 = symbols[ 1 ].parse()?;
all_sprites.insert( symbol_name.to_owned(), symbol_id );
}
let component_ids = all_components;
let attribute_ids = all_attributes;
let symbol_ids = all_symbols;
let sprite_ids = all_sprites;
let tiled_file = get_tiled_tilemap( &input_file )?;
@ -133,6 +146,10 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
nametables_bin.write_all( &get_ecs( &tiled_file, &component_ids, &attribute_ids, &symbol_ids )? )?;
print_good( "exported objects.ecs" );
let mut sprites_bin = File::create( format!( "{}sprites.spt", output_directory ) )?;
sprites_bin.write_all( &get_sprites( &tiled_file, &sprite_ids )? )?;
print_good( "exported sprites.spt" );
let mut code_asm = File::create( format!( "{}level.asm", output_directory ) )?;
code_asm.write_all( &get_code( &tiled_file, "testlevel", "levels/" )?.as_bytes() )?;
print_good( "exported level.asm" );

View File

@ -121,6 +121,10 @@ pub enum Tools {
#[arg(long, num_args(0..))]
symbol_ids: Vec<String>,
/// Zero or more sprite ID definitions (format: <sprite_name_1>=<sprite_id_1>, <sprite_name_2>=<sprite_id_2>, ... )
#[arg(long, num_args(0..))]
sprite_ids: Vec<String>,
/// Console system type
#[arg(short, long, value_enum, default_value_t=SystemType::Md)]
console: SystemType

View File

@ -13,12 +13,21 @@ pub struct TiledTilemap {
pub height: usize
}
#[derive(Debug)]
pub struct SpriteMetadata {
pub id: String,
pub width: u8,
pub height: u8,
pub anim_interval: Option<u16>
}
#[derive(Debug)]
pub struct TiledTileset {
pub first_gid: usize,
pub image: DynamicImage,
pub palettes: Vec<Option<u8>>,
pub tile_order: TileOrder
pub tile_order: TileOrder,
pub sprite_metadata: Option<SpriteMetadata>
}
#[derive(Debug)]
@ -138,26 +147,58 @@ fn get_tiles( tileset: Node, first_gid: usize, working_directory: &str ) -> Resu
}
}
let sprite_metadata;
// 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" );
let tile_order_property = tile_order_property.attribute( "value" ).expect( "invalid file: no reskit-tile-order value" );
match tile_order_property.to_lowercase().as_str() {
"sprite" => TileOrder::Sprite,
"tile" => TileOrder::Tile,
"sprite" => {
// If sprite, reskit-sprite-height and reskit-sprite-width must be defined
let reskit_sprite_height = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-sprite-height" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-sprite-height and reskit-sprite-width must be defined." )?;
let reskit_sprite_width = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-sprite-width" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-sprite-height and reskit-sprite-width must be defined." )?;
let reskit_sprite_id = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-sprite-id" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-sprite-id must be defined." )?;
let reskit_anim_interval = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-anim-interval" ) );
let reskit_sprite_height = reskit_sprite_height.attribute( "value" ).ok_or( "invalid file: no reskit-sprite-height value" )?;
let reskit_sprite_width = reskit_sprite_width.attribute( "value" ).ok_or( "invalid file: no reskit-sprite-width value" )?;
let id: String = reskit_sprite_id.attribute( "value" ).ok_or( "invalid file: no reskit-sprite-id value" )?.to_owned();
let width: u8 = reskit_sprite_width.parse()?;
let height: u8 = reskit_sprite_height.parse()?;
let anim_interval = if let Some( property ) = reskit_anim_interval {
let value = property.attribute( "value" ).ok_or( "invalid file: no reskit-anim-interval value" )?;
Some( value.parse()? )
} else {
None
};
sprite_metadata = Some( SpriteMetadata { id, width, height, anim_interval } );
TileOrder::Sprite
},
"tile" => {
sprite_metadata = None;
TileOrder::Tile
},
invalid => {
print_warning( &format!( "invalid setting for property reskit-tile-order: {}. falling back on \"sprite\"", invalid ) );
sprite_metadata = None;
print_warning( &format!( "invalid setting for property reskit-tile-order: {}. falling back on \"tile\"", invalid ) );
TileOrder::Tile
}
}
} else {
sprite_metadata = None;
TileOrder::Tile
}
} else {
sprite_metadata = None;
TileOrder::Tile
};
Ok( TiledTileset { first_gid, image, palettes, tile_order } )
Ok( TiledTileset { first_gid, image, palettes, tile_order, sprite_metadata } )
}
pub fn get_tiled_tilemap( path: &str ) -> Result<TiledTilemap, Box<dyn Error>> {

View File

@ -2,7 +2,7 @@ use std::{error::Error, convert::TryInto, cmp::max, num::ParseIntError, collecti
use image::GenericImageView;
use linked_hash_set::LinkedHashSet;
use crate::reskit::{tileset::image_to_tiles, utility::{symbol_to_pascal, print_info}, cli::settings::TileOrder};
use super::converter::{TiledTilemap, Layer, SystemPlane};
use super::converter::{TiledTilemap, Layer, SystemPlane, TiledTileset};
/**
* Output the .bin and .pal file (using `tileset` tool to build it) containing each of the tiles
@ -359,6 +359,55 @@ pub fn get_ecs( tilemap: &TiledTilemap, component_ids: &HashMap<String, u8>, att
Ok( result )
}
/**
* Get the sprite table.
*/
pub fn get_sprites( tilemap: &TiledTilemap, sprite_ids: &HashMap<String, u16> ) -> Result<Vec<u8>, Box<dyn Error>> {
let mut result: Vec<u8> = Vec::new();
let tiles_before = {
let mut total_tiles = 0;
for tileset in &tilemap.tileset {
total_tiles += ( tileset.image.width() / 8 ) * ( tileset.image.height() / 8 );
}
total_tiles
};
let mut tiles_after: u32 = 0;
let sprites: Vec<&TiledTileset> = tilemap.tileset.iter().filter( | tileset | matches!( tileset.tile_order, TileOrder::Sprite ) ).collect();
result.extend( ( sprites.len() as u16 ).to_be_bytes() );
for sprite in sprites {
let sprite_metadata = sprite.sprite_metadata.as_ref().ok_or( "internal error: tile order is sprite but no sprite data" )?;
let sprite_id = sprite_ids.get( &sprite_metadata.id ).ok_or( format!( "invalid file: undefined sprite id \"{}\"", sprite_metadata.id ) )?;
result.extend( sprite_id.to_be_bytes() );
let tile_index_location = tiles_before + tiles_after;
result.extend( ( tile_index_location as u16 ).to_be_bytes() );
tiles_after += ( sprite_metadata.width * sprite_metadata.height ) as u32;
result.push( sprite_metadata.width );
result.push( sprite_metadata.height );
// One animation per .tsx file
// Animations run like a filmstrip across, never down
let tiles_across = sprite.image.width() / 8;
let frames: u16 = ( tiles_across / ( sprite_metadata.width as u32 ) ) as u16;
result.extend( frames.to_be_bytes() );
if let Some( jiffies ) = sprite_metadata.anim_interval {
result.extend( jiffies.to_be_bytes() );
} else {
result.extend( ( 0 as u16 ).to_be_bytes() );
};
}
Ok( result )
}
/**
* Get a helper .asm or .c file that ties all the level components together
*/
@ -393,14 +442,17 @@ pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -
{level_label}Objects:
incbin '{path_prefix}{level_name}/objects.ecs'
{level_label}Sprites:
incbin '{path_prefix}{level_name}/sprites.spt'
{level_label}:
dc.w {width}, {height}, {num_tiles}
dc.l {level_label}Tiles
dc.l {level_label}Palettes
dc.l {level_label}Nametables
dc.l {level_label}Collision
dc.l {level_label}Objects"# );
dc.l {level_label}Objects
dc.l {level_label}Sprites"# );
Ok( file )
}