Improved ECS exporter with component-ids, attribute-ids, and symbol-ids

stinkhead7ds
Ashley N. 2023-09-23 16:11:01 -04:00
parent 5ba27514e7
commit 1f704e88ba
3 changed files with 83 additions and 163 deletions

View File

@ -1,6 +1,6 @@
use std::{error::Error, fs::File, io::Write, path::Path};
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, print_error, print_info}, 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}}};
use super::settings::{Args, Tools, TileOutputFormat, TileOrder};
pub fn run_command() -> Result<(), Box<dyn Error>> {
@ -65,7 +65,49 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
print_good( "all files converted successfully" );
}
Tools::Level { input_file, output_directory, console: _ } => {
Tools::Level { input_file, output_directory, component_ids, attribute_ids, symbol_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();
for pair in component_ids {
let components: Vec<&str> = pair.split( "=" ).collect();
if components.len() != 2 {
return Err( format!( "invalid format for component_ids: {}", pair ) )?;
}
let component_name = components[ 0 ];
let component_id: u8 = components[ 1 ].parse()?;
all_components.insert( component_name.to_owned(), component_id );
}
for pair in attribute_ids {
let components: Vec<&str> = pair.split( "=" ).collect();
if components.len() != 2 {
return Err( format!( "invalid format for attribute_ids: {}", pair ) )?;
}
let component_name = components[ 0 ];
let attributes: Vec<String> = components[ 1 ].split( "," ).map( | string | string.to_owned() ).collect();
all_attributes.insert( component_name.to_owned(), attributes );
}
for pair in symbol_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_symbols.insert( symbol_name.to_owned(), symbol_id );
}
let component_ids = all_components;
let attribute_ids = all_attributes;
let symbol_ids = all_symbols;
let tiled_file = get_tiled_tilemap( &input_file )?;
// Get tile and palette files
@ -88,7 +130,7 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
print_good( "exported collision.lvc" );
let mut nametables_bin = File::create( format!( "{}objects.ecs", output_directory ) )?;
nametables_bin.write_all( &get_ecs( &tiled_file )? )?;
nametables_bin.write_all( &get_ecs( &tiled_file, &component_ids, &attribute_ids, &symbol_ids )? )?;
print_good( "exported objects.ecs" );
let mut code_asm = File::create( format!( "{}level.asm", output_directory ) )?;

View File

@ -109,6 +109,18 @@ pub enum Tools {
#[arg(short, long, default_value_t=String::from("./"))]
output_directory: String,
/// Zero or more ECS component ID definitions (format: <component_name>=<u8>)
#[arg(long, num_args(0..))]
component_ids: Vec<String>,
/// Zero or more ECS component attributes (format: <component_name>=<attribute_1>, <attribute_2>, ...)
#[arg(long, num_args(0..))]
attribute_ids: Vec<String>,
/// Zero or more ECS symbol ID definitions (format: <symbol_name>=<u8>)
#[arg(long, num_args(0..))]
symbol_ids: Vec<String>,
/// Console system type
#[arg(short, long, value_enum, default_value_t=SystemType::Md)]
console: SystemType

View File

@ -1,4 +1,4 @@
use std::{error::Error, convert::TryInto, cmp::max, num::ParseIntError};
use std::{error::Error, convert::TryInto, cmp::max, num::ParseIntError, collections::HashMap};
use image::GenericImageView;
use linked_hash_map::LinkedHashMap;
use linked_hash_set::LinkedHashSet;
@ -220,25 +220,18 @@ pub fn get_collision_map( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Er
/**
* Get the entity-component system defined. If none is defined return an empty vec.
*/
pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
pub fn get_ecs( tilemap: &TiledTilemap, component_ids: &HashMap<String, u8>, attribute_ids: &HashMap<String, Vec<String>>, symbol_ids: &HashMap<String, u16> ) -> Result<Vec<u8>, Box<dyn Error>> {
if tilemap.ecs.is_empty() {
return Ok( vec![] )
}
let mut result: Vec<u8> = Vec::new();
// Build a complete set of component IDs and types
let mut component_ids: LinkedHashSet<String> = LinkedHashSet::new();
// Build a complete set of types
let mut types: LinkedHashSet<LinkedHashSet<String>> = LinkedHashSet::new();
for entity in &tilemap.ecs {
// Get the list of components attached to this entity
// Sort alphabetically as order matters in LinkedHashSets
let mut components: Vec<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
components.sort();
let components: LinkedHashSet<String> = components.into_iter().collect();
// Assign an index to each unique component id by inserting it into `component_ids`
component_ids.extend( components.clone() );
let components: LinkedHashSet<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
// Assign this unique combination of components a type id
// by inserting it into `types`
@ -264,7 +257,7 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
for component in ecs_type {
type_component_ids.push(
component_ids.iter().position( | value | component == value ).ok_or( "internal error: no component" )? as u8
*component_ids.get( component ).ok_or( format!( "invalid file: when building type table: undefined component \"{}\"", component ) )?
);
}
@ -301,27 +294,6 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
result.push( 0xFF );
}
// Component Attribute Table
// Index all attribute values that do not parse to u16
let mut attribute_value_ids: LinkedHashSet<String> = LinkedHashSet::new();
for entity in &tilemap.ecs {
for ( _, component ) in &entity.components {
for ( _, attribute_value ) in &component.attributes {
// Handling various types from Tiled Editor
// * If it is "true" or "false", leave it alone. It will be articulated as 1 or 0.
// * If it can be parsed as a u16, leave it alone.
// * In all other cases, add to attribute_value_ids to create an index.
if attribute_value != "true" && attribute_value != "false" {
let try_as_u16: Result<u16, ParseIntError> = attribute_value.parse();
if let Err( _ ) = try_as_u16 {
attribute_value_ids.insert_if_absent( attribute_value.clone() );
print_info( &format!( "Attribute Value ID {}: \"{}\"", attribute_value_ids.len() - 1, attribute_value ) );
}
}
}
}
}
// Find the largest size of a combination of attributes (the component attribute union size)
let largest_type_attributes_size = tilemap.ecs.iter()
.map( | entity | {
@ -341,35 +313,32 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
// For (Object Table Size):
for entity in &tilemap.ecs {
// (Component Attribute Union Size) bytes: Component attribute settings
// Sort components in this entity alphabetically, ascending
let mut components: Vec<(&String, &Component)> = entity.components.iter().collect();
components.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );
let mut written_attributes = 0;
for ( _, component ) in &components {
// Sort attributes in each component alphabetically
let mut attributes: Vec<(&String, &String)> = component.attributes.iter().collect();
attributes.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );
for ( _, attribute_value ) in attributes {
written_attributes += 1;
match attribute_value.as_str() {
"true" => result.extend( ( 1 as u16 ).to_be_bytes() ),
"false" => result.extend( ( 0 as u16 ).to_be_bytes() ),
string => {
let try_as_u16: Result<u16, ParseIntError> = string.parse();
if let Ok( numeric ) = try_as_u16 {
result.extend( numeric.to_be_bytes() );
} else {
let id: u16 = attribute_value_ids.iter()
.position( | attribute_value | attribute_value == &string )
.ok_or( "internal error: no type id" )? as u16;
for ( component_id, component ) in &entity.components {
let attributes_in_order = attribute_ids.get( component_id );
if let Some( attributes_in_order ) = attributes_in_order {
for attribute_id in attributes_in_order {
let attribute_value = component.attributes.get( attribute_id ).ok_or( format!( "invalid file: undefined attribute \"{}\" for component \"{}\"", attribute_id, component_id ) )?;
result.extend( id.to_be_bytes() );
written_attributes += 1;
match attribute_value.as_str() {
"true" => result.extend( ( 1 as u16 ).to_be_bytes() ),
"false" => result.extend( ( 0 as u16 ).to_be_bytes() ),
string => {
let try_as_u16: Result<u16, ParseIntError> = string.parse();
if let Ok( numeric ) = try_as_u16 {
result.extend( numeric.to_be_bytes() );
} else {
let id: u16 = *symbol_ids.get( string ).ok_or( format!( "invalid file: undefined symbol \"{}\"", string ) )?;
result.extend( id.to_be_bytes() );
}
}
}
}
} else {
print_info( &format!( "component \"{}\" has no defined attributes (command line option --attribute-ids)", component_id ) );
}
}
@ -388,46 +357,6 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
}
}
// Output IDs to terminal
let component_ids: Vec<String> = component_ids.into_iter().collect();
for i in 0..component_ids.len() {
print_info( &format!( "Component ID {}: {}", i, component_ids[ i ] ) );
}
// Output type structures to terminal
let mut visited_types: LinkedHashSet<LinkedHashSet<String>> = LinkedHashSet::new();
for entity in &tilemap.ecs {
// Do the thing again
let mut components: Vec<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
components.sort();
let components: LinkedHashSet<String> = components.into_iter().collect();
if visited_types.insert( components.clone() ) {
// What type id is this?
let type_id: u8 = types.iter().position( | ecs_type | ecs_type == &components ).ok_or( "internal error: no type id" )? as u8;
print_info( &format!( "Type ID {} Components and Attributes:", type_id ) );
let mut components: Vec<(&String, &Component)> = entity.components.iter().collect();
components.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );
for ( component_name, component ) in components {
print_info( &format!( "\tComponent {}", component_name ) );
let mut attributes: Vec<(&String, &String)> = component.attributes.iter().collect();
attributes.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );
let mut index = 0;
for ( attribute_name, _ ) in &attributes {
print_info( &format!( "\t\tAttribute ID {}: {}", index, attribute_name ) );
index += 1;
}
if attributes.is_empty() {
print_info( "\t\tNo attributes" );
}
}
}
}
Ok( result )
}
@ -437,7 +366,6 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -> Result<String, Box<dyn Error>> {
let version = env!( "CARGO_PKG_VERSION" );
let level_label = symbol_to_pascal( level_name );
let level_label_const_caps = level_label.to_uppercase();
let ( width, height ) = ( tilemap.width, tilemap.height );
let num_tiles = {
let mut total_tiles = 0;
@ -448,71 +376,9 @@ pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -
total_tiles
};
let mut constants: String = String::new();
// Output IDs for each type defined in the map
let mut component_ids: LinkedHashSet<String> = LinkedHashSet::new();
let mut attribute_ids: LinkedHashMap<String, LinkedHashSet<String>> = LinkedHashMap::new();
let mut attribute_string_values: LinkedHashSet<String> = LinkedHashSet::new();
for entity in &tilemap.ecs {
let mut components: Vec<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
components.sort();
let components: LinkedHashSet<String> = components.into_iter().collect();
component_ids.extend( components );
for ( component_name, component ) in &entity.components {
for ( _, attribute_value ) in &component.attributes {
match attribute_value.as_str() {
"true" | "false" => { /* nothing */ },
string => {
let try_as_u16: Result<u16, ParseIntError> = string.parse();
if let Err( _ ) = try_as_u16 {
attribute_string_values.insert_if_absent( attribute_value.to_owned() );
}
}
}
}
let mut attributes: Vec<String> = component.attributes.keys().map( | id | id.to_lowercase() ).collect();
attributes.sort();
let attribues: LinkedHashSet<String> = attributes.into_iter().collect();
attribute_ids.entry( component_name.to_owned() ).or_default().extend( attribues );
}
}
let attribute_string_values: Vec<String> = attribute_string_values.into_iter().collect();
for i in 0..attribute_string_values.len() {
let attr_val = &attribute_string_values[ i ];
match attr_val.as_str() {
"true" | "false" => { /* nothing */ },
string => {
let try_as_u16: Result<u16, ParseIntError> = string.parse();
if let Err( _ ) = try_as_u16 {
constants += &format!( "{}_ATTR_{} = {}\n", level_label_const_caps, attribute_string_values[ i ].to_uppercase(), i );
}
}
}
}
let component_ids: Vec<String> = component_ids.into_iter().collect();
for i in 0..component_ids.len() {
let component_id = &component_ids[ i ];
constants += &format!( "{}_{} = {}\n", level_label_const_caps, component_id.to_uppercase(), i );
let attribute_ids: Vec<String> = attribute_ids[ component_id ].iter().map( | string | string.clone() ).collect();
for i in 0..attribute_ids.len() {
constants += &format!( "{}_{}_{} = {}\n", level_label_const_caps, component_id.to_uppercase(), attribute_ids[ i ].to_uppercase(), i );
}
}
let file = format!( r#"; Level definition file
; Generated by reskit v{version}
{constants}
{level_label}Tiles:
incbin '{path_prefix}{level_name}/tiles.bin'