From 73b5ba6cc71493fdd58d36799a9b42862d52416d Mon Sep 17 00:00:00 2001 From: ashley Date: Sun, 24 Sep 2023 13:52:37 -0400 Subject: [PATCH] Export sprite table --- src/reskit/cli/evaluator.rs | 21 +++++++++++-- src/reskit/cli/settings.rs | 4 +++ src/reskit/level/converter.rs | 53 ++++++++++++++++++++++++++++---- src/reskit/level/system.rs | 58 +++++++++++++++++++++++++++++++++-- 4 files changed, 125 insertions(+), 11 deletions(-) diff --git a/src/reskit/cli/evaluator.rs b/src/reskit/cli/evaluator.rs index c93cd2f..439789a 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_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> { @@ -65,11 +65,12 @@ pub fn run_command() -> Result<(), Box> { 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 = HashMap::new(); let mut all_symbols: HashMap = HashMap::new(); let mut all_attributes: HashMap> = HashMap::new(); + let mut all_sprites: HashMap = 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> { 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> { 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" ); diff --git a/src/reskit/cli/settings.rs b/src/reskit/cli/settings.rs index ad28414..22e9145 100644 --- a/src/reskit/cli/settings.rs +++ b/src/reskit/cli/settings.rs @@ -121,6 +121,10 @@ pub enum Tools { #[arg(long, num_args(0..))] symbol_ids: Vec, + /// Zero or more sprite ID definitions (format: =, =, ... ) + #[arg(long, num_args(0..))] + sprite_ids: Vec, + /// Console system type #[arg(short, long, value_enum, default_value_t=SystemType::Md)] console: SystemType diff --git a/src/reskit/level/converter.rs b/src/reskit/level/converter.rs index 0b96a4e..1b36a09 100644 --- a/src/reskit/level/converter.rs +++ b/src/reskit/level/converter.rs @@ -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 +} + #[derive(Debug)] pub struct TiledTileset { pub first_gid: usize, pub image: DynamicImage, pub palettes: Vec>, - pub tile_order: TileOrder + pub tile_order: TileOrder, + pub sprite_metadata: Option } #[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> { diff --git a/src/reskit/level/system.rs b/src/reskit/level/system.rs index bd7f1ad..52e677e 100644 --- a/src/reskit/level/system.rs +++ b/src/reskit/level/system.rs @@ -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, att Ok( result ) } +/** + * Get the sprite table. + */ +pub fn get_sprites( tilemap: &TiledTilemap, sprite_ids: &HashMap ) -> Result, Box> { + let mut result: Vec = 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 ) } \ No newline at end of file