Improved ECS exporter with component-ids, attribute-ids, and symbol-ids
parent
5ba27514e7
commit
1f704e88ba
|
@ -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 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};
|
use super::settings::{Args, Tools, TileOutputFormat, TileOrder};
|
||||||
|
|
||||||
pub fn run_command() -> Result<(), Box<dyn Error>> {
|
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" );
|
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 )?;
|
let tiled_file = get_tiled_tilemap( &input_file )?;
|
||||||
|
|
||||||
// Get tile and palette files
|
// Get tile and palette files
|
||||||
|
@ -88,7 +130,7 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
|
||||||
print_good( "exported collision.lvc" );
|
print_good( "exported collision.lvc" );
|
||||||
|
|
||||||
let mut nametables_bin = File::create( format!( "{}objects.ecs", output_directory ) )?;
|
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" );
|
print_good( "exported objects.ecs" );
|
||||||
|
|
||||||
let mut code_asm = File::create( format!( "{}level.asm", output_directory ) )?;
|
let mut code_asm = File::create( format!( "{}level.asm", output_directory ) )?;
|
||||||
|
|
|
@ -109,6 +109,18 @@ pub enum Tools {
|
||||||
#[arg(short, long, default_value_t=String::from("./"))]
|
#[arg(short, long, default_value_t=String::from("./"))]
|
||||||
output_directory: String,
|
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
|
/// Console system type
|
||||||
#[arg(short, long, value_enum, default_value_t=SystemType::Md)]
|
#[arg(short, long, value_enum, default_value_t=SystemType::Md)]
|
||||||
console: SystemType
|
console: SystemType
|
||||||
|
|
|
@ -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 image::GenericImageView;
|
||||||
use linked_hash_map::LinkedHashMap;
|
use linked_hash_map::LinkedHashMap;
|
||||||
use linked_hash_set::LinkedHashSet;
|
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.
|
* 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() {
|
if tilemap.ecs.is_empty() {
|
||||||
return Ok( vec![] )
|
return Ok( vec![] )
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result: Vec<u8> = Vec::new();
|
let mut result: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
// Build a complete set of component IDs and types
|
// Build a complete set of types
|
||||||
let mut component_ids: LinkedHashSet<String> = LinkedHashSet::new();
|
|
||||||
let mut types: LinkedHashSet<LinkedHashSet<String>> = LinkedHashSet::new();
|
let mut types: LinkedHashSet<LinkedHashSet<String>> = LinkedHashSet::new();
|
||||||
for entity in &tilemap.ecs {
|
for entity in &tilemap.ecs {
|
||||||
// Get the list of components attached to this entity
|
// Get the list of components attached to this entity
|
||||||
// Sort alphabetically as order matters in LinkedHashSets
|
let components: LinkedHashSet<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
|
||||||
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() );
|
|
||||||
|
|
||||||
// Assign this unique combination of components a type id
|
// Assign this unique combination of components a type id
|
||||||
// by inserting it into `types`
|
// 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 {
|
for component in ecs_type {
|
||||||
type_component_ids.push(
|
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 );
|
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)
|
// Find the largest size of a combination of attributes (the component attribute union size)
|
||||||
let largest_type_attributes_size = tilemap.ecs.iter()
|
let largest_type_attributes_size = tilemap.ecs.iter()
|
||||||
.map( | entity | {
|
.map( | entity | {
|
||||||
|
@ -341,18 +313,14 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
|
|
||||||
// For (Object Table Size):
|
// For (Object Table Size):
|
||||||
for entity in &tilemap.ecs {
|
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;
|
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 {
|
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 ) )?;
|
||||||
|
|
||||||
written_attributes += 1;
|
written_attributes += 1;
|
||||||
match attribute_value.as_str() {
|
match attribute_value.as_str() {
|
||||||
"true" => result.extend( ( 1 as u16 ).to_be_bytes() ),
|
"true" => result.extend( ( 1 as u16 ).to_be_bytes() ),
|
||||||
|
@ -362,15 +330,16 @@ pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
if let Ok( numeric ) = try_as_u16 {
|
if let Ok( numeric ) = try_as_u16 {
|
||||||
result.extend( numeric.to_be_bytes() );
|
result.extend( numeric.to_be_bytes() );
|
||||||
} else {
|
} else {
|
||||||
let id: u16 = attribute_value_ids.iter()
|
let id: u16 = *symbol_ids.get( string ).ok_or( format!( "invalid file: undefined symbol \"{}\"", string ) )?;
|
||||||
.position( | attribute_value | attribute_value == &string )
|
|
||||||
.ok_or( "internal error: no type id" )? as u16;
|
|
||||||
|
|
||||||
result.extend( id.to_be_bytes() );
|
result.extend( id.to_be_bytes() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print_info( &format!( "component \"{}\" has no defined attributes (command line option --attribute-ids)", component_id ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the remainder of the union with 0xFF, if needed
|
// Fill the remainder of the union with 0xFF, if needed
|
||||||
|
@ -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 )
|
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>> {
|
pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -> Result<String, Box<dyn Error>> {
|
||||||
let version = env!( "CARGO_PKG_VERSION" );
|
let version = env!( "CARGO_PKG_VERSION" );
|
||||||
let level_label = symbol_to_pascal( level_name );
|
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 ( width, height ) = ( tilemap.width, tilemap.height );
|
||||||
let num_tiles = {
|
let num_tiles = {
|
||||||
let mut total_tiles = 0;
|
let mut total_tiles = 0;
|
||||||
|
@ -448,71 +376,9 @@ pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -
|
||||||
|
|
||||||
total_tiles
|
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
|
let file = format!( r#"; Level definition file
|
||||||
; Generated by reskit v{version}
|
; Generated by reskit v{version}
|
||||||
|
|
||||||
{constants}
|
|
||||||
{level_label}Tiles:
|
{level_label}Tiles:
|
||||||
incbin '{path_prefix}{level_name}/tiles.bin'
|
incbin '{path_prefix}{level_name}/tiles.bin'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue