Level tool
parent
264c2df1e8
commit
0543d9adfc
|
@ -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"
|
|
@ -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" );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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" )?
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod converter;
|
|
@ -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;
|
Loading…
Reference in New Issue