Level tool

stinkhead7ds
Ashley N. 2023-09-10 00:15:53 -04:00
parent 264c2df1e8
commit 0543d9adfc
6 changed files with 192 additions and 6 deletions

View File

@ -17,3 +17,4 @@ pitch_shift = "1.0.0"
linked_hash_set = "0.1.4" linked_hash_set = "0.1.4"
linked-hash-map = "0.5.6" linked-hash-map = "0.5.6"
convert_case = "0.6.0" convert_case = "0.6.0"
roxmltree = "0.18.0"

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};
use clap::Parser; use clap::Parser;
use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::{print_good, print_error}}; use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::{print_good, print_error}, level::converter::get_tiled_tilemap};
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,8 +65,10 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
print_good( "all files converted successfully" ); print_good( "all files converted successfully" );
} }
Tools::Level { input_file, configuration_file, console, tile_size } => { Tools::Level { input_file, output_directory, console, tile_size } => {
let tiled_file = get_tiled_tilemap( &input_file )?;
print_good( "file opened without errors" );
print_error( "unimplemented" ); print_error( "unimplemented" );
} }
}; };

View File

@ -105,9 +105,9 @@ pub enum Tools {
#[arg(short, long)] #[arg(short, long)]
input_file: String, input_file: String,
/// Configuration file (TOML, see doc) /// Output directory for artifacts
#[arg(short, long)] #[arg(short, long, default_value_t=String::from("./"))]
configuration_file: String, output_directory: 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)]

View File

@ -0,0 +1,181 @@
use std::{error::Error, fs::read_to_string};
use image::DynamicImage;
use roxmltree::Node;
use crate::reskit::utility::print_warning;
pub struct TiledTilemap {
tileset: TiledTileset,
layers: Vec<Layer>,
width: usize,
height: usize,
tile_width: usize,
tile_height: usize
}
pub struct TiledTileset {
image: DynamicImage
}
pub enum SystemPlane {
MdPlaneA,
MdPlaneB
}
pub enum Layer {
Tile {
system_plane: SystemPlane,
tiles: Vec<u32>
},
Collision {
tiles: Vec<u32>
}
}
fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result<Option<Layer>, Box<dyn Error>> {
let layer_id = layer.attribute( "id" ).ok_or( "invalid file: on layer: no id" )?;
let layer_name = layer.attribute( "name" ).unwrap_or( "<no name>" );
let layer_name = format!( "({}, ID: {})", layer_name, layer_id );
// Validate layer is same as total map size
let ( layer_width, layer_height ): ( usize, usize ) = (
layer.attribute( "width" ).ok_or( format!( "invalid file: on layer {}: no width attribute", layer_name ) )?.parse()?,
layer.attribute( "height" ).ok_or( format!( "invalid file: on layer {}: no height attribute", layer_name ) )?.parse()?
);
if layer_width != map_width || layer_height != map_height {
return Err( format!( "invalid file: on layer {}: layer width must match map width", layer_name ) )?
}
let properties = layer.descendants().find( | node | node.tag_name() == "properties".into() );
if let Some( properties ) = properties {
let properties: Vec<Node> = properties.descendants().filter( | node | {
if let Some( name ) = node.attribute( "name" ) {
name == "reskit-layer"
} else {
false
}
} ).collect();
// Should be either one or none
if let Some( layer_property ) = properties.first() {
let layer_type = layer_property.attribute( "value" ).ok_or( format!( "invalid file: on layer {}: no value for property", layer_name ) )?;
let data = layer.descendants()
.find( | node | node.tag_name() == "data".into() )
.ok_or( format!( "invalid file: on layer {}: no data for layer", layer_name ) )?;
let encoding = data.attribute( "encoding" ).ok_or( format!( "invalid file: on layer {}: no encoding attribute", layer_name ) )?;
if encoding != "csv" {
return Err( format!( "invalid file: on layer {}: only csv is supported for layer encoding", layer_name ) )?
}
let data: Vec<&str> = data.text().ok_or( format!( "invalid file: on layer {}: no layer data", layer_name ) )?.split( "," ).collect();
let tiles: Vec<u32> = data.into_iter().map( | string | Ok( string.trim().parse()? ) ).collect::< Result< Vec<u32>, Box<dyn Error> > >()?;
match layer_type.to_lowercase().as_str() {
"a" => Ok( Some( Layer::Tile {
system_plane: SystemPlane::MdPlaneA,
tiles
} ) ),
"b" => Ok( Some( Layer::Tile {
system_plane: SystemPlane::MdPlaneB,
tiles
} ) ),
"collision" => Ok( Some( Layer::Collision { tiles } ) ),
_ => {
print_warning( &format!( "on layer {}: invalid reskit-layer value {}; ignoring this layer", layer_name, layer_type ) );
Ok( None )
}
}
} else {
print_warning( &format!( "on layer {}: no reskit-layer property defining hardware layer or collision; ignoring this layer", layer_name ) );
Ok( None )
}
} else {
print_warning( &format!( "on layer {}: no properties defining hardware layer or collision; ignoring this layer", layer_name ) );
Ok( None )
}
}
pub fn get_tiled_tilemap( path: &str ) -> Result<TiledTilemap, Box<dyn Error>> {
let file = read_to_string( path )?;
let document = roxmltree::Document::parse( &file )?;
let map = document.descendants().find( | node | node.tag_name() == "map".into() );
if let Some( map ) = map {
// Validate version
let version = map.attribute( "version" ).ok_or( "invalid file: no version attribute" )?;
if version < "1.10" || !version.starts_with( "1." ) {
return Err( "invalid file: unsupported version" )?
}
// Validate orientation and render order
let orientation = map.attribute( "orientation" ).ok_or( "invalid file: no orientation attribute" )?;
if orientation != "orthogonal" {
return Err( "invalid file: only orthogonal orientation is supported" )?
}
let render_order = map.attribute( "renderorder" ).ok_or( "invalid file: no renderorder attribute" )?;
if render_order != "left-down" {
return Err( "invalid file: only left-down orientation is supported" )?
}
let ( width, height ): ( usize, usize ) = (
map.attribute( "width" ).ok_or( "invalid file: no width attribute" )?.parse()?,
map.attribute( "height" ).ok_or( "invalid file: no height attribute" )?.parse()?
);
let ( tile_width, tile_height ): ( usize, usize ) = (
map.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?,
map.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()?
);
// Build tileset (current version assumes one tileset per level)
let tileset = map.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset" )?;
let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?;
let tileset_file = read_to_string( tileset_source_path )?;
let tileset_document = roxmltree::Document::parse( &tileset_file )?;
let tileset = tileset_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset origin object" )?;
// Validate referenced tileset dimensions match map dimensions
// E.g. if your .tmx specifies 16x16, the acompanying .tsx file should agree
let ( tsx_tile_width, tsx_tile_height ): ( usize, usize ) = (
tileset.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?,
tileset.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()?
);
if tsx_tile_width != tile_width || tsx_tile_height != tile_height {
return Err( "invalid file: referenced tilemap doesn't have same per-tile width and height of parent tilemap" )?
}
// Get the image for the tileset
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" )? )?;
// Get the layers
let layers: Vec<Layer> = map.descendants()
.filter( | node | node.tag_name() == "layer".into() )
.map( | node | get_layer( node, width, height ) )
.collect::< Result< Vec<Option<Layer>>, Box<dyn Error> > >()?
.into_iter()
.filter_map( | option | option )
.collect();
// Print warning if there is no collision layer
if let None = layers.iter().find( | layer | matches!( layer, Layer::Collision { tiles: _ } ) ) {
print_warning( "tile map has no \"collision\" layer: this probably is not what you want" );
}
Ok(
TiledTilemap {
tileset: TiledTileset { image },
layers,
width,
height,
tile_width,
tile_height
}
)
} else {
Err( "invalid file: this does not appear to be valid Tiled .tmx file" )?
}
}

1
src/reskit/level/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod converter;

View File

@ -1,4 +1,5 @@
pub mod cli; pub mod cli;
pub mod level;
pub mod soundtrack; pub mod soundtrack;
pub mod tileset; pub mod tileset;
pub mod utility; pub mod utility;