mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'src/exe/edit.rs')
| -rw-r--r-- | src/exe/edit.rs | 2680 |
1 files changed, 1352 insertions, 1328 deletions
diff --git a/src/exe/edit.rs b/src/exe/edit.rs index 4b0ec94..d89e0b2 100644 --- a/src/exe/edit.rs +++ b/src/exe/edit.rs @@ -1,1466 +1,1490 @@ use std::borrow::Cow; use std::env::Args; -use std::io::{self, Write}; use std::fs; +use std::io::{self, Write}; -use plandustry::block::{BlockRegistry, build_registry, Rotation}; +use plandustry::block::{build_registry, BlockRegistry, Rotation}; use plandustry::data::dynamic::DynData; -use plandustry::data::{base64, DataRead, Serializer, DataWrite, GridPos}; use plandustry::data::schematic::{Placement, ResizeError, Schematic, SchematicSerializer}; +use plandustry::data::{base64, DataRead, DataWrite, GridPos, Serializer}; use plandustry::registry::RegistryEntry; +use crate::args::{self, ArgCount, ArgOption, OptionHandler}; use crate::print::print_schematic; use crate::print_err; -use crate::args::{self, ArgCount, ArgOption, OptionHandler}; -struct State<'l> -{ - reg: &'l BlockRegistry<'l>, - schematic: Option<Schematic<'l>>, - unsaved: bool, - subregion: Option<Schematic<'l>>, - quit: bool, +struct State<'l> { + reg: &'l BlockRegistry<'l>, + schematic: Option<Schematic<'l>>, + unsaved: bool, + subregion: Option<Schematic<'l>>, + quit: bool, } -pub fn main(mut args: Args, arg_off: usize) -{ - let mut handler = OptionHandler::new(); - let opt_file = handler.add(ArgOption::new(Some('f'), Some(Cow::Borrowed("file")), ArgCount::Required(1))).unwrap(); - if let Err(e) = args::parse(&mut args, &mut handler, arg_off) - { - print_err!(e, "Command error"); - return; - } - - // try to load a schematic from the file argument or as base64 - let reg = build_registry(); - let mut ss = SchematicSerializer(®); - let mut state = State{reg: ®, schematic: None, unsaved: false, subregion: None, quit: false}; - if let Some(path) = handler.get_value(opt_file).get_value() - { - match fs::read(path) - { - Ok(data) => - { - match ss.deserialize(&mut DataRead::new(&data)) - { - Ok(s) => - { - println!("Loaded schematic from {path}"); - state.schematic = Some(s); - }, - Err(e) => print_err!(e, "Could not read schematic from {path}"), - } - }, - Err(e) => print_err!(e, "Could not read file {path:?}"), - } - } - else if let Some(b64) = handler.get_literals().first() - { - match ss.deserialize_base64(b64) - { - Ok(s) => - { - println!("Loaded schematic from CLI"); - state.schematic = Some(s); - }, - Err(e) => print_err!(e, "Could not read schematic"), - } - } - if state.schematic.is_none() - { - println!(r#"No active schematic, use "new" or "load" to begin editing."#); - } - println!(r#"Type "help" for a list of available commands."#); - - // the main command interpreter loop - let mut line_buff = String::new(); - let stdin = io::stdin(); - while !state.quit - { - line_buff.clear(); - print!("> "); - if let Err(e) = io::stdout().flush() - { - // what the print & println macros would do - panic!("failed printing to stdout: {e}"); - } - match stdin.read_line(&mut line_buff) - { - Ok(..) => interpret(&mut state, line_buff.trim_start()), - Err(e) => - { - print_err!(e, "Failed to read next command"); - if state.unsaved - { - // special case because we wouldn't be able to read a path from stdin - match ss.serialize_base64(state.schematic.as_ref().unwrap()) - { - Ok(curr) => println!("Current schematic: {curr}"), - Err(e) => print_err!(e, "Could not serialize schematic"), - } - state.unsaved = false; - } - break; - }, - } - } - - // give the user a chance to save their work - if state.unsaved - { - let mut data = DataWrite::new(); - match ss.serialize(&mut data, state.schematic.as_ref().unwrap()) - { - Ok(()) => - { - let data = data.get_written(); - // SAFETY: base64 output is always valid ASCII - let buff = unsafe{line_buff.as_mut_vec()}; - buff.resize(4 * ((data.len() + 2) / 3), 0); - match base64::encode(data, buff) - { - Ok(len) => println!("Current schematic: {}", &line_buff[..len]), - Err(e) => print_err!(e, "Could not convert schematic to base-64"), - } - - println!("You have unsaved work. Please type a path to save to or press enter to quit."); - loop - { - line_buff.clear(); - match stdin.read_line(&mut line_buff) - { - Ok(..) => - { - let path = line_buff.trim(); - if path.is_empty() {break;} - match fs::write(path, data) - { - Ok(()) => println!("Saved schematic to {path}"), - Err(e) => print_err!(e, "Could not write file {path:?}"), - } - }, - Err(e) => - { - print_err!(e, "Failed to read save path"); - return; - }, - } - } - }, - Err(e) => print_err!(e, "Could not serialize schematic"), - } - } +pub fn main(mut args: Args, arg_off: usize) { + let mut handler = OptionHandler::new(); + let opt_file = handler + .add(ArgOption::new( + Some('f'), + Some(Cow::Borrowed("file")), + ArgCount::Required(1), + )) + .unwrap(); + if let Err(e) = args::parse(&mut args, &mut handler, arg_off) { + print_err!(e, "Command error"); + return; + } + + // try to load a schematic from the file argument or as base64 + let reg = build_registry(); + let mut ss = SchematicSerializer(®); + let mut state = State { + reg: ®, + schematic: None, + unsaved: false, + subregion: None, + quit: false, + }; + if let Some(path) = handler.get_value(opt_file).get_value() { + match fs::read(path) { + Ok(data) => match ss.deserialize(&mut DataRead::new(&data)) { + Ok(s) => { + println!("Loaded schematic from {path}"); + state.schematic = Some(s); + } + Err(e) => print_err!(e, "Could not read schematic from {path}"), + }, + Err(e) => print_err!(e, "Could not read file {path:?}"), + } + } else if let Some(b64) = handler.get_literals().first() { + match ss.deserialize_base64(b64) { + Ok(s) => { + println!("Loaded schematic from CLI"); + state.schematic = Some(s); + } + Err(e) => print_err!(e, "Could not read schematic"), + } + } + if state.schematic.is_none() { + println!(r#"No active schematic, use "new" or "load" to begin editing."#); + } + println!(r#"Type "help" for a list of available commands."#); + + // the main command interpreter loop + let mut line_buff = String::new(); + let stdin = io::stdin(); + while !state.quit { + line_buff.clear(); + print!("> "); + if let Err(e) = io::stdout().flush() { + // what the print & println macros would do + panic!("failed printing to stdout: {e}"); + } + match stdin.read_line(&mut line_buff) { + Ok(..) => interpret(&mut state, line_buff.trim_start()), + Err(e) => { + print_err!(e, "Failed to read next command"); + if state.unsaved { + // special case because we wouldn't be able to read a path from stdin + match ss.serialize_base64(state.schematic.as_ref().unwrap()) { + Ok(curr) => println!("Current schematic: {curr}"), + Err(e) => print_err!(e, "Could not serialize schematic"), + } + state.unsaved = false; + } + break; + } + } + } + + // give the user a chance to save their work + if state.unsaved { + let mut data = DataWrite::new(); + match ss.serialize(&mut data, state.schematic.as_ref().unwrap()) { + Ok(()) => { + let data = data.get_written(); + // SAFETY: base64 output is always valid ASCII + let buff = unsafe { line_buff.as_mut_vec() }; + buff.resize(4 * ((data.len() + 2) / 3), 0); + match base64::encode(data, buff) { + Ok(len) => println!("Current schematic: {}", &line_buff[..len]), + Err(e) => print_err!(e, "Could not convert schematic to base-64"), + } + + println!( + "You have unsaved work. Please type a path to save to or press enter to quit." + ); + loop { + line_buff.clear(); + match stdin.read_line(&mut line_buff) { + Ok(..) => { + let path = line_buff.trim(); + if path.is_empty() { + break; + } + match fs::write(path, data) { + Ok(()) => println!("Saved schematic to {path}"), + Err(e) => print_err!(e, "Could not write file {path:?}"), + } + } + Err(e) => { + print_err!(e, "Failed to read save path"); + return; + } + } + } + } + Err(e) => print_err!(e, "Could not serialize schematic"), + } + } } struct Tokenizer<'l>(Option<&'l str>); -impl<'l> Tokenizer<'l> -{ - fn skip_ws(&mut self) - { - if let Some(curr) = self.0 - { - let curr = curr.trim_start(); - self.0 = if curr.is_empty() {None} else {Some(curr)}; - } - } - - fn next(&mut self) -> Option<&'l str> - { - self.skip_ws(); - if let Some(curr) = self.0 - { - if curr.len() >= 2 && (curr.as_bytes()[0] == b'"' || curr.as_bytes()[0] == b'\'') - { - match (&curr[1..]).find(curr.as_bytes()[0] as char) - { - None => - { - self.0 = None; - Some(&curr[1..]) - }, - Some(end) => - { - let rest = &curr[(end + 2)..]; - self.0 = if rest.is_empty() {None} else {Some(rest)}; - Some(&curr[1..end]) - }, - } - } - else - { - match curr.find(char::is_whitespace) - { - None => - { - self.0 = None; - Some(curr) - }, - Some(end) => - { - let rest = &curr[end..]; - self.0 = if rest.is_empty() {None} else {Some(rest)}; - Some(&curr[..end]) - } - } - } - } - else {None} - } - - fn remainder(&mut self) -> Option<&'l str> - { - self.skip_ws(); - if let Some(curr) = self.0 - { - let bytes = curr.as_bytes(); - if bytes.len() >= 2 && (bytes[0] == b'"' || bytes[0] == b'\'') && bytes[bytes.len() - 1] == bytes[0] - { - self.0 = None; - return Some(&curr[1..(curr.len() - 1)]); - } - } - let curr = self.0; - self.0 = None; - curr - } +impl<'l> Tokenizer<'l> { + fn skip_ws(&mut self) { + if let Some(curr) = self.0 { + let curr = curr.trim_start(); + self.0 = if curr.is_empty() { None } else { Some(curr) }; + } + } + + fn next(&mut self) -> Option<&'l str> { + self.skip_ws(); + if let Some(curr) = self.0 { + if curr.len() >= 2 && (curr.as_bytes()[0] == b'"' || curr.as_bytes()[0] == b'\'') { + match (&curr[1..]).find(curr.as_bytes()[0] as char) { + None => { + self.0 = None; + Some(&curr[1..]) + } + Some(end) => { + let rest = &curr[(end + 2)..]; + self.0 = if rest.is_empty() { None } else { Some(rest) }; + Some(&curr[1..end]) + } + } + } else { + match curr.find(char::is_whitespace) { + None => { + self.0 = None; + Some(curr) + } + Some(end) => { + let rest = &curr[end..]; + self.0 = if rest.is_empty() { None } else { Some(rest) }; + Some(&curr[..end]) + } + } + } + } else { + None + } + } + + fn remainder(&mut self) -> Option<&'l str> { + self.skip_ws(); + if let Some(curr) = self.0 { + let bytes = curr.as_bytes(); + if bytes.len() >= 2 + && (bytes[0] == b'"' || bytes[0] == b'\'') + && bytes[bytes.len() - 1] == bytes[0] + { + self.0 = None; + return Some(&curr[1..(curr.len() - 1)]); + } + } + let curr = self.0; + self.0 = None; + curr + } } -enum Command -{ - Help, New, Input, Load, Place, Rotate, Mirror, Move, Resize, Remove, Sub, Print, Dump, Save, Quit +enum Command { + Help, + New, + Input, + Load, + Place, + Rotate, + Mirror, + Move, + Resize, + Remove, + Sub, + Print, + Dump, + Save, + Quit, } -impl Command -{ - fn print_help(&self, indent: usize) - { - match self - { - Self::Help => println!("{:<indent$}Prints a list of available commands", "\"help\":"), - Self::New => println!("{:<indent$}Creates a new schematic, erasing the currently loaded one", "\"new\":"), - Self::Input => println!("{:<indent$}Loads a new schematic from a base-64 encoded string", "\"input\":"), - Self::Load => println!("{:<indent$}Loads a new schematic from a file", "\"load\":"), - Self::Place => println!("{:<indent$}Places a block if enough space is available", "\"place\":"), - Self::Rotate => println!("{:<indent$}Rotates the schematic (CCW) in increments of 90 degrees", "\"rotate\":"), - Self::Mirror => println!("{:<indent$}Mirrors the schematic horizontally or vertically", "\"mirror\":"), - Self::Move => println!("{:<indent$}Moves all blocks by a certain offset", "\"move\":"), - Self::Resize => println!("{:<indent$}Resizes the schematic and offsets it", "\"resize\":"), - Self::Remove => println!("{:<indent$}Removes blocks at a position or within a region", "\"remove\":"), - Self::Sub => println!("{:<indent$}Various commands for editing subregions", "\"sub\":"), - Self::Print => println!("{:<indent$}Prints the schematic in a visual representation", "\"print\":"), - Self::Dump => println!("{:<indent$}Prints the schematic as a base-64 encoded string", "\"dump\":"), - Self::Save => println!("{:<indent$}Saves the schematic to a file", "\"save\":"), - Self::Quit => println!("{:<indent$}Offers to save unsaved work and exits the program", "\"quit\":"), - } - self.print_usage(indent); - } - - fn print_usage(&self, indent: usize) - { - match self - { - Self::Help => (), - Self::New => println!(r#"{:indent$} Usage: "new" <width> [<height>]"#, ""), - Self::Input => println!(r#"{:indent$} Usage: "input" <base64>"#, ""), - Self::Load => println!(r#"{:indent$} Usage: "load" <load path>"#, ""), - Self::Place => - { - println!(r#"{:indent$} Usage: "place" <x> <y> <block name> [<rotation> [<replace>]]"#, ""); - println!(r#"{:indent$} Rotation is one of right, up, left, down or compass angles"#, "") - }, - Self::Rotate => println!(r#"{:indent$} Usage: "rotate" <angle>"#, ""), - Self::Mirror => println!(r#"{:indent$} Usage: "mirror" <axis>"#, ""), - Self::Move => println!(r#"{:indent$} Usage: "move" <dx> <dy>"#, ""), - Self::Resize => println!(r#"{:indent$} Usage: "resize" <width> <height> [<dx> <dy>]"#, ""), - Self::Remove => println!(r#"{:indent$} Usage: "remove" <x0> <y0> [<x1> <y1>]"#, ""), - Self::Sub => println!(r#"{:indent$} Usage: "sub" ... (see "sub help")"#, ""), - Self::Print | Self::Dump => (), - Self::Save => println!(r#"{:indent$} Usage: "save" <save path>"#, ""), - Self::Quit => (), - } - } +impl Command { + fn print_help(&self, indent: usize) { + match self { + Self::Help => println!( + "{:<indent$}Prints a list of available commands", + "\"help\":" + ), + Self::New => println!( + "{:<indent$}Creates a new schematic, erasing the currently loaded one", + "\"new\":" + ), + Self::Input => println!( + "{:<indent$}Loads a new schematic from a base-64 encoded string", + "\"input\":" + ), + Self::Load => println!("{:<indent$}Loads a new schematic from a file", "\"load\":"), + Self::Place => println!( + "{:<indent$}Places a block if enough space is available", + "\"place\":" + ), + Self::Rotate => println!( + "{:<indent$}Rotates the schematic (CCW) in increments of 90 degrees", + "\"rotate\":" + ), + Self::Mirror => println!( + "{:<indent$}Mirrors the schematic horizontally or vertically", + "\"mirror\":" + ), + Self::Move => println!( + "{:<indent$}Moves all blocks by a certain offset", + "\"move\":" + ), + Self::Resize => println!( + "{:<indent$}Resizes the schematic and offsets it", + "\"resize\":" + ), + Self::Remove => println!( + "{:<indent$}Removes blocks at a position or within a region", + "\"remove\":" + ), + Self::Sub => println!( + "{:<indent$}Various commands for editing subregions", + "\"sub\":" + ), + Self::Print => println!( + "{:<indent$}Prints the schematic in a visual representation", + "\"print\":" + ), + Self::Dump => println!( + "{:<indent$}Prints the schematic as a base-64 encoded string", + "\"dump\":" + ), + Self::Save => println!("{:<indent$}Saves the schematic to a file", "\"save\":"), + Self::Quit => println!( + "{:<indent$}Offers to save unsaved work and exits the program", + "\"quit\":" + ), + } + self.print_usage(indent); + } + + fn print_usage(&self, indent: usize) { + match self { + Self::Help => (), + Self::New => println!(r#"{:indent$} Usage: "new" <width> [<height>]"#, ""), + Self::Input => println!(r#"{:indent$} Usage: "input" <base64>"#, ""), + Self::Load => println!(r#"{:indent$} Usage: "load" <load path>"#, ""), + Self::Place => { + println!( + r#"{:indent$} Usage: "place" <x> <y> <block name> [<rotation> [<replace>]]"#, + "" + ); + println!( + r#"{:indent$} Rotation is one of right, up, left, down or compass angles"#, + "" + ) + } + Self::Rotate => println!(r#"{:indent$} Usage: "rotate" <angle>"#, ""), + Self::Mirror => println!(r#"{:indent$} Usage: "mirror" <axis>"#, ""), + Self::Move => println!(r#"{:indent$} Usage: "move" <dx> <dy>"#, ""), + Self::Resize => println!( + r#"{:indent$} Usage: "resize" <width> <height> [<dx> <dy>]"#, + "" + ), + Self::Remove => println!(r#"{:indent$} Usage: "remove" <x0> <y0> [<x1> <y1>]"#, ""), + Self::Sub => println!(r#"{:indent$} Usage: "sub" ... (see "sub help")"#, ""), + Self::Print | Self::Dump => (), + Self::Save => println!(r#"{:indent$} Usage: "save" <save path>"#, ""), + Self::Quit => (), + } + } } -macro_rules!parse_num -{ - ($cmd:path, $name:expr, <$type:ty>::from($val:expr)) => - { - match $val - { - None => - { - eprintln!("Missing argument: {}", $name); - $cmd.print_usage(0); - return; - }, - Some(s) => - { - match <$type>::from_str_radix(s, 10) - { - Ok(v) => v, - Err(e) => - { - print_err!(e, "Could not parse {}", $name); - $cmd.print_usage(0); - return; - }, - } - }, - } - }; - ($cmd:path, $tokens:expr, $name:expr, $type:ty) => - { - parse_num!($cmd, $name, <$type>::from($tokens.next())) - } +macro_rules! parse_num { + ($cmd:path, $name:expr, <$type:ty>::from($val:expr)) => { + match $val { + None => { + eprintln!("Missing argument: {}", $name); + $cmd.print_usage(0); + return; + } + Some(s) => match <$type>::from_str_radix(s, 10) { + Ok(v) => v, + Err(e) => { + print_err!(e, "Could not parse {}", $name); + $cmd.print_usage(0); + return; + } + }, + } + }; + ($cmd:path, $tokens:expr, $name:expr, $type:ty) => { + parse_num!($cmd, $name, <$type>::from($tokens.next())) + }; } -fn interpret(state: &mut State, cmd: &str) -{ - let mut tokens = Tokenizer(Some(cmd)); - match tokens.next() - { - None => println!(r#"Empty command, type "quit" to exit"#), - Some("help") => - { - println!(r#"List of available commands:"#); - const INDENT: usize = 12; - Command::Help.print_help(INDENT); - Command::New.print_help(INDENT); - Command::Input.print_help(INDENT); - Command::Load.print_help(INDENT); - Command::Place.print_help(INDENT); - Command::Rotate.print_help(INDENT); - Command::Mirror.print_help(INDENT); - Command::Remove.print_help(INDENT); - Command::Sub.print_help(INDENT); - Command::Print.print_help(INDENT); - Command::Dump.print_help(INDENT); - Command::Save.print_help(INDENT); - Command::Quit.print_help(INDENT); - println!(); - println!("Legend: \"literal (excluding quotes)\" <required> [<optional>]"); - println!("Arguments are delimited by whitespace, unless surrounded with ' or \""); - if tokens.remainder().is_some() - { - eprintln!("Extra arguments are considered an error"); - } - else - { - println!("Extra arguments are considered an error"); - } - }, - Some("new") => - { - let width = parse_num!(Command::New, tokens, "width", u16); - if width == 0 - { - eprintln!("Schematic width must be positive"); - return; - } - let height = parse_num!(Command::New, tokens, "height", u16); - if height == 0 - { - eprintln!("Schematic height must be positive"); - return; - } - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "new""#); - Command::New.print_usage(0); - return; - } - state.schematic = Some(Schematic::new(width, height)); - // it's empty, no need to save this - state.unsaved = false; - }, - Some("input") => - { - let schematic = match tokens.next() - { - None => - { - eprintln!("Missing argument: base64"); - Command::Input.print_usage(0); - return; - }, - Some(b64) => - { - match SchematicSerializer(state.reg).deserialize_base64(b64) - { - Ok(s) => s, - Err(e) => - { - print_err!(e, "Could not deserialize schematic"); - return; - }, - } - }, - }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "input""#); - Command::Input.print_usage(0); - return; - } - state.schematic = Some(schematic); - state.unsaved = false; - }, - Some("load") => - { - let schematic = match tokens.next() - { - None => - { - eprintln!("Missing argument: load path"); - Command::Load.print_usage(0); - return; - }, - Some(path) => - { - let data = match fs::read(path) - { - Ok(d) => d, - Err(e) => - { - print_err!(e, "Could not load from file"); - return; - }, - }; - match SchematicSerializer(state.reg).deserialize(&mut DataRead::new(&data)) - { - Ok(s) => s, - Err(e) => - { - print_err!(e, "Could not deserialize schematic"); - return; - }, - } - }, - }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "load""#); - Command::Load.print_usage(0); - return; - } - state.schematic = Some(schematic); - state.unsaved = false; - }, - Some("place") => - { - let Some(ref mut schematic) = state.schematic +fn interpret(state: &mut State, cmd: &str) { + let mut tokens = Tokenizer(Some(cmd)); + match tokens.next() { + None => println!(r#"Empty command, type "quit" to exit"#), + Some("help") => { + println!(r#"List of available commands:"#); + const INDENT: usize = 12; + Command::Help.print_help(INDENT); + Command::New.print_help(INDENT); + Command::Input.print_help(INDENT); + Command::Load.print_help(INDENT); + Command::Place.print_help(INDENT); + Command::Rotate.print_help(INDENT); + Command::Mirror.print_help(INDENT); + Command::Remove.print_help(INDENT); + Command::Sub.print_help(INDENT); + Command::Print.print_help(INDENT); + Command::Dump.print_help(INDENT); + Command::Save.print_help(INDENT); + Command::Quit.print_help(INDENT); + println!(); + println!("Legend: \"literal (excluding quotes)\" <required> [<optional>]"); + println!("Arguments are delimited by whitespace, unless surrounded with ' or \""); + if tokens.remainder().is_some() { + eprintln!("Extra arguments are considered an error"); + } else { + println!("Extra arguments are considered an error"); + } + } + Some("new") => { + let width = parse_num!(Command::New, tokens, "width", u16); + if width == 0 { + eprintln!("Schematic width must be positive"); + return; + } + let height = parse_num!(Command::New, tokens, "height", u16); + if height == 0 { + eprintln!("Schematic height must be positive"); + return; + } + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "new""#); + Command::New.print_usage(0); + return; + } + state.schematic = Some(Schematic::new(width, height)); + // it's empty, no need to save this + state.unsaved = false; + } + Some("input") => { + let schematic = match tokens.next() { + None => { + eprintln!("Missing argument: base64"); + Command::Input.print_usage(0); + return; + } + Some(b64) => match SchematicSerializer(state.reg).deserialize_base64(b64) { + Ok(s) => s, + Err(e) => { + print_err!(e, "Could not deserialize schematic"); + return; + } + }, + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "input""#); + Command::Input.print_usage(0); + return; + } + state.schematic = Some(schematic); + state.unsaved = false; + } + Some("load") => { + let schematic = match tokens.next() { + None => { + eprintln!("Missing argument: load path"); + Command::Load.print_usage(0); + return; + } + Some(path) => { + let data = match fs::read(path) { + Ok(d) => d, + Err(e) => { + print_err!(e, "Could not load from file"); + return; + } + }; + match SchematicSerializer(state.reg).deserialize(&mut DataRead::new(&data)) { + Ok(s) => s, + Err(e) => { + print_err!(e, "Could not deserialize schematic"); + return; + } + } + } + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "load""#); + Command::Load.print_usage(0); + return; + } + state.schematic = Some(schematic); + state.unsaved = false; + } + Some("place") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "place" requires an active schematic (see "help")"#); return; }; - let x = parse_num!(Command::Place, tokens, "x", u16); - let y = parse_num!(Command::Place, tokens, "y", u16); - if x >= schematic.get_width() || y >= schematic.get_height() - { - eprintln!("Invalid coordinate ({x} / {y}) out of bounds ({} / {})", schematic.get_width(), schematic.get_height()); - return; - } - let block = match tokens.next() - { - None => - { - eprintln!("Missing argument: block name"); - Command::Place.print_usage(0); - return; - }, - Some(name) => - { - match state.reg.get(name) - { - None => - { - eprintln!("No such block {name:?}"); - return; - }, - Some(b) => b, - } - }, - }; - let rot = match tokens.next() - { - None => None, - Some("right") | Some("east") => Some(Rotation::Right), - Some("up") | Some("north") => Some(Rotation::Up), - Some("left") | Some("west") => Some(Rotation::Left), - Some("down") | Some("south") => Some(Rotation::Down), - Some(rot) => - { - eprintln!("Invalid rotation {rot:?}"); - return; - }, - }; - let replace = if rot.is_some() - { - match tokens.next() - { - None => None, - Some("true") | Some("yes") => Some(true), - Some("false") | Some("no") => Some(false), - Some(replace) => - { - eprintln!("Invalid replacement {replace:?}"); - return; - }, - } - } - else {None}; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "place""#); - Command::Place.print_usage(0); - return; - } - let rot = rot.unwrap_or(Rotation::Right); - let result = if replace.unwrap_or(false) - { - schematic.replace(x, y, block, DynData::Empty, rot, false).err() - } - else - { - schematic.set(x, y, block, DynData::Empty, rot).err() - }; - if let Some(e) = result - { - print_err!(e, "Failed to place block at {x} / {y}"); - return; - } - }, - Some("rotate") => - { - let Some(ref mut schematic) = state.schematic + let x = parse_num!(Command::Place, tokens, "x", u16); + let y = parse_num!(Command::Place, tokens, "y", u16); + if x >= schematic.get_width() || y >= schematic.get_height() { + eprintln!( + "Invalid coordinate ({x} / {y}) out of bounds ({} / {})", + schematic.get_width(), + schematic.get_height() + ); + return; + } + let block = match tokens.next() { + None => { + eprintln!("Missing argument: block name"); + Command::Place.print_usage(0); + return; + } + Some(name) => match state.reg.get(name) { + None => { + eprintln!("No such block {name:?}"); + return; + } + Some(b) => b, + }, + }; + let rot = match tokens.next() { + None => None, + Some("right") | Some("east") => Some(Rotation::Right), + Some("up") | Some("north") => Some(Rotation::Up), + Some("left") | Some("west") => Some(Rotation::Left), + Some("down") | Some("south") => Some(Rotation::Down), + Some(rot) => { + eprintln!("Invalid rotation {rot:?}"); + return; + } + }; + let replace = if rot.is_some() { + match tokens.next() { + None => None, + Some("true") | Some("yes") => Some(true), + Some("false") | Some("no") => Some(false), + Some(replace) => { + eprintln!("Invalid replacement {replace:?}"); + return; + } + } + } else { + None + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "place""#); + Command::Place.print_usage(0); + return; + } + let rot = rot.unwrap_or(Rotation::Right); + let result = if replace.unwrap_or(false) { + schematic + .replace(x, y, block, DynData::Empty, rot, false) + .err() + } else { + schematic.set(x, y, block, DynData::Empty, rot).err() + }; + if let Some(e) = result { + print_err!(e, "Failed to place block at {x} / {y}"); + return; + } + } + Some("rotate") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "rotate" requires an active schematic (see "help")"#); return; }; - let angle = parse_num!(Command::Rotate, tokens, "angle", i32); - if angle % 90 != 0 - { - eprintln!("Rotation angle must be a multiple of 90 degrees"); - return; - } - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "rotate""#); - Command::Rotate.print_usage(0); - return; - } - match (angle / 90) % 4 - { - 0 => (), - 1 | -3 => - { - schematic.rotate(false); - state.unsaved = true; - }, - 2 | -2 => - { - schematic.rotate_180(); - state.unsaved = true; - }, - 3 | -1 => - { - schematic.rotate(true); - state.unsaved = true; - }, - a => unreachable!("angle {angle} -> {a}"), - } - }, - Some("mirror") => - { - let Some(ref mut schematic) = state.schematic + let angle = parse_num!(Command::Rotate, tokens, "angle", i32); + if angle % 90 != 0 { + eprintln!("Rotation angle must be a multiple of 90 degrees"); + return; + } + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "rotate""#); + Command::Rotate.print_usage(0); + return; + } + match (angle / 90) % 4 { + 0 => (), + 1 | -3 => { + schematic.rotate(false); + state.unsaved = true; + } + 2 | -2 => { + schematic.rotate_180(); + state.unsaved = true; + } + 3 | -1 => { + schematic.rotate(true); + state.unsaved = true; + } + a => unreachable!("angle {angle} -> {a}"), + } + } + Some("mirror") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "mirror" requires an active schematic (see "help")"#); return; }; - let (x, y) = match tokens.next() - { - None => - { - eprintln!("Missing argument: axis"); - Command::Mirror.print_usage(0); - return; - }, - Some("x") | Some("h") | Some("horizontal") | Some("horizontally") => (true, false), - Some("y") | Some("v") | Some("vertical") | Some("vertically") => (false, true), - Some("both") | Some("all") => (true, true), - Some(axis) => - { - eprintln!("Invalid mirroring axis: {axis:?}"); - return; - }, - }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "mirror""#); - Command::Mirror.print_usage(0); - return; - } - schematic.mirror(x, y); - state.unsaved = true; - }, - Some("move") => - { - let Some(ref mut schematic) = state.schematic + let (x, y) = match tokens.next() { + None => { + eprintln!("Missing argument: axis"); + Command::Mirror.print_usage(0); + return; + } + Some("x") | Some("h") | Some("horizontal") | Some("horizontally") => (true, false), + Some("y") | Some("v") | Some("vertical") | Some("vertically") => (false, true), + Some("both") | Some("all") => (true, true), + Some(axis) => { + eprintln!("Invalid mirroring axis: {axis:?}"); + return; + } + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "mirror""#); + Command::Mirror.print_usage(0); + return; + } + schematic.mirror(x, y); + state.unsaved = true; + } + Some("move") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "move" requires an active schematic (see "help")"#); return; }; - let dx = parse_num!(Command::Move, tokens, "dx", i16); - let dy = parse_num!(Command::Move, tokens, "dy", i16); - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "move""#); - Command::Move.print_usage(0); - return; - } - if dx != 0 && dy != 0 - { - if let Err(e) = schematic.resize(dx, dy, schematic.get_width(), schematic.get_height()) - { - match e - { - ResizeError::XOffset{dx, old_w, new_w} => - { - debug_assert_eq!(old_w, new_w); - eprintln!("Invalid horizontal move {dx} not in ]-{old_w}, {new_w}["); - }, - ResizeError::YOffset{dy, old_h, new_h} => - { - debug_assert_eq!(old_h, new_h); - eprintln!("Invalid vertical move {dy} not in ]-{old_h}, {new_h}["); - }, - ResizeError::Truncated{right, top, left, bottom} => - { - eprint!("Move would truncate schematic: "); - let mut first = true; - if right > 0 - { - eprint!("{}right: {right}", if first {""} else {", "}); - first = false; - } - if top > 0 - { - eprint!("{}top: {top}", if first {""} else {", "}); - first = false; - } - if left > 0 - { - eprint!("{}left: {left}", if first {""} else {", "}); - first = false; - } - if bottom > 0 - { - eprint!("{}bottom: {bottom}", if first {""} else {", "}); - first = false; - } - if first {eprintln!("<unknown>");} else {eprintln!();} - }, - _ => print_err!(e, "Unexpected resize error (for {dx} / {dy})") - } - } - else {state.unsaved = true;} - } - }, - Some("resize") => - { - let Some(ref mut schematic) = state.schematic + let dx = parse_num!(Command::Move, tokens, "dx", i16); + let dy = parse_num!(Command::Move, tokens, "dy", i16); + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "move""#); + Command::Move.print_usage(0); + return; + } + if dx != 0 && dy != 0 { + if let Err(e) = + schematic.resize(dx, dy, schematic.get_width(), schematic.get_height()) + { + match e { + ResizeError::XOffset { dx, old_w, new_w } => { + debug_assert_eq!(old_w, new_w); + eprintln!("Invalid horizontal move {dx} not in ]-{old_w}, {new_w}["); + } + ResizeError::YOffset { dy, old_h, new_h } => { + debug_assert_eq!(old_h, new_h); + eprintln!("Invalid vertical move {dy} not in ]-{old_h}, {new_h}["); + } + ResizeError::Truncated { + right, + top, + left, + bottom, + } => { + eprint!("Move would truncate schematic: "); + let mut first = true; + if right > 0 { + eprint!("{}right: {right}", if first { "" } else { ", " }); + first = false; + } + if top > 0 { + eprint!("{}top: {top}", if first { "" } else { ", " }); + first = false; + } + if left > 0 { + eprint!("{}left: {left}", if first { "" } else { ", " }); + first = false; + } + if bottom > 0 { + eprint!("{}bottom: {bottom}", if first { "" } else { ", " }); + first = false; + } + if first { + eprintln!("<unknown>"); + } else { + eprintln!(); + } + } + _ => print_err!(e, "Unexpected resize error (for {dx} / {dy})"), + } + } else { + state.unsaved = true; + } + } + } + Some("resize") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "resize" requires an active schematic (see "help")"#); return; }; - let w = parse_num!(Command::Resize, tokens, "width", u16); - if w == 0 - { - eprintln!("Schematic width must be positive"); - return; - } - let h = parse_num!(Command::Resize, tokens, "height", u16); - if h == 0 - { - eprintln!("Schematic height must be positive"); - return; - } - let (dx, dy) = if let arg @ Some(..) = tokens.next() - { - let dx = parse_num!(Command::Resize, "dx", <i16>::from(arg)); - let dy = parse_num!(Command::Resize, tokens, "dy", i16); - (dx, dy) - } - else {(0, 0)}; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "resize""#); - Command::Resize.print_usage(0); - return; - } - if w != schematic.get_width() || h != schematic.get_height() || dx != 0 || dy != 0 - { - if let Err(e) = schematic.resize(dx, dy, w, h) - { - print_err!(e, "Could not resize schematic"); - } - } - }, - Some("remove") => - { - let Some(ref mut schematic) = state.schematic + let w = parse_num!(Command::Resize, tokens, "width", u16); + if w == 0 { + eprintln!("Schematic width must be positive"); + return; + } + let h = parse_num!(Command::Resize, tokens, "height", u16); + if h == 0 { + eprintln!("Schematic height must be positive"); + return; + } + let (dx, dy) = if let arg @ Some(..) = tokens.next() { + let dx = parse_num!(Command::Resize, "dx", <i16>::from(arg)); + let dy = parse_num!(Command::Resize, tokens, "dy", i16); + (dx, dy) + } else { + (0, 0) + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "resize""#); + Command::Resize.print_usage(0); + return; + } + if w != schematic.get_width() || h != schematic.get_height() || dx != 0 || dy != 0 { + if let Err(e) = schematic.resize(dx, dy, w, h) { + print_err!(e, "Could not resize schematic"); + } + } + } + Some("remove") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "remove" requires an active schematic (see "help")"#); return; }; - let x0 = parse_num!(Command::Remove, tokens, "x0", u16); - let y0 = parse_num!(Command::Remove, tokens, "y0", u16); - if x0 >= schematic.get_width() || y0 >= schematic.get_height() - { - eprintln!("Invalid coordinate ({x0} / {y0}) out of bounds ({} / {})", schematic.get_width(), schematic.get_height()); - return; - } - let (x0, y0, x1, y1) = if let arg @ Some(..) = tokens.next() - { - let x1 = parse_num!(Command::Remove, "x1", <u16>::from(arg)); - let y1 = parse_num!(Command::Remove, tokens, "y1", u16); - if x1 >= schematic.get_width() || y1 >= schematic.get_height() - { - eprintln!("Invalid coordinate ({x1} / {y1}) out of bounds ({} / {})", schematic.get_width(), schematic.get_height()); - return; - } - (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)) - } - else {(x0, y0, x0, y0)}; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "remove""#); - Command::Remove.print_usage(0); - return; - } - if x1 > x0 || y1 > y0 - { - let mut cnt = 0u32; - for y in y0..=y1 - { - for x in x0..=x1 - { - // position was already checked while parsing - if schematic.take(x, y).unwrap().is_some() {cnt += 1;} - } - } - println!("Removed {cnt} blocks in {x0} / {y0} to {x1} / {y1}"); - if cnt > 0 {state.unsaved = true;} - } - else - { - // position was already checked while parsing - match schematic.take(x0, y0).unwrap() - { - None => (), - Some(p) => - { - println!("Removed block {} from {x0} / {y0}", p.get_block().get_name()); - state.unsaved = true; - }, - } - } - }, - Some("sub") => interpret_sub(state, &mut tokens), - Some("print") => - { - let Some(ref schematic) = state.schematic + let x0 = parse_num!(Command::Remove, tokens, "x0", u16); + let y0 = parse_num!(Command::Remove, tokens, "y0", u16); + if x0 >= schematic.get_width() || y0 >= schematic.get_height() { + eprintln!( + "Invalid coordinate ({x0} / {y0}) out of bounds ({} / {})", + schematic.get_width(), + schematic.get_height() + ); + return; + } + let (x0, y0, x1, y1) = if let arg @ Some(..) = tokens.next() { + let x1 = parse_num!(Command::Remove, "x1", <u16>::from(arg)); + let y1 = parse_num!(Command::Remove, tokens, "y1", u16); + if x1 >= schematic.get_width() || y1 >= schematic.get_height() { + eprintln!( + "Invalid coordinate ({x1} / {y1}) out of bounds ({} / {})", + schematic.get_width(), + schematic.get_height() + ); + return; + } + (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)) + } else { + (x0, y0, x0, y0) + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "remove""#); + Command::Remove.print_usage(0); + return; + } + if x1 > x0 || y1 > y0 { + let mut cnt = 0u32; + for y in y0..=y1 { + for x in x0..=x1 { + // position was already checked while parsing + if schematic.take(x, y).unwrap().is_some() { + cnt += 1; + } + } + } + println!("Removed {cnt} blocks in {x0} / {y0} to {x1} / {y1}"); + if cnt > 0 { + state.unsaved = true; + } + } else { + // position was already checked while parsing + match schematic.take(x0, y0).unwrap() { + None => (), + Some(p) => { + println!( + "Removed block {} from {x0} / {y0}", + p.get_block().get_name() + ); + state.unsaved = true; + } + } + } + } + Some("sub") => interpret_sub(state, &mut tokens), + Some("print") => { + let Some(ref schematic) = state.schematic else { eprintln!(r#"Command "print" requires an active schematic (see "help")"#); return; }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "print""#); - Command::Print.print_usage(0); - return; - } - print_schematic(schematic); - }, - Some("dump") => - { - let Some(ref schematic) = state.schematic + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "print""#); + Command::Print.print_usage(0); + return; + } + print_schematic(schematic); + } + Some("dump") => { + let Some(ref schematic) = state.schematic else { eprintln!(r#"Command "dump" requires an active schematic (see "help")"#); return; }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "dump""#); - Command::Dump.print_usage(0); - return; - } - let b64 = match SchematicSerializer(state.reg).serialize_base64(schematic) - { - Ok(b64) => b64, - Err(e) => - { - print_err!(e, "Could not serialize schematic"); - return; - }, - }; - println!("Schematic: {}", b64); - }, - Some("save") => - { - let Some(ref schematic) = state.schematic + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "dump""#); + Command::Dump.print_usage(0); + return; + } + let b64 = match SchematicSerializer(state.reg).serialize_base64(schematic) { + Ok(b64) => b64, + Err(e) => { + print_err!(e, "Could not serialize schematic"); + return; + } + }; + println!("Schematic: {}", b64); + } + Some("save") => { + let Some(ref schematic) = state.schematic else { eprintln!(r#"Command "save" requires an active schematic (see "help")"#); return; }; - let Some(path) = tokens.next() + let Some(path) = tokens.next() else { eprintln!("Missing argument: save path"); Command::Save.print_usage(0); return; }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "save""#); - Command::Save.print_usage(0); - return; - } - let mut serial_buff = DataWrite::new(); - if let Err(e) = SchematicSerializer(state.reg).serialize(&mut serial_buff, schematic) - { - print_err!(e, "Could not serialize schematic"); - return; - } - if let Err(e) = fs::write(path, serial_buff.get_written()) - { - print_err!(e, "Could not write to file"); - return; - } - state.unsaved = false; - println!("Saved schematic to {path}."); - }, - Some("quit") => state.quit = true, - Some(unknown) => eprintln!("Unknown command {unknown:?}"), - } + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "save""#); + Command::Save.print_usage(0); + return; + } + let mut serial_buff = DataWrite::new(); + if let Err(e) = SchematicSerializer(state.reg).serialize(&mut serial_buff, schematic) { + print_err!(e, "Could not serialize schematic"); + return; + } + if let Err(e) = fs::write(path, serial_buff.get_written()) { + print_err!(e, "Could not write to file"); + return; + } + state.unsaved = false; + println!("Saved schematic to {path}."); + } + Some("quit") => state.quit = true, + Some(unknown) => eprintln!("Unknown command {unknown:?}"), + } } -enum SubCommand -{ - Help, Input, Copy, Cut, Paste, Place, Rotate, Mirror, Move, Resize, Remove, Print, Dump +enum SubCommand { + Help, + Input, + Copy, + Cut, + Paste, + Place, + Rotate, + Mirror, + Move, + Resize, + Remove, + Print, + Dump, } -impl SubCommand -{ - fn print_help(&self, indent: usize) - { - match self - { - Self::Help => println!("{:<indent$}Prints a list of available commands", "\"sub\" \"help\":"), - Self::Input => println!("{:<indent$}Loads a new subregion from a base-64 encoded string", "\"sub\" \"input\":"), - Self::Copy => println!("{:<indent$}Replaces the current subregion by copying from the schematic", "\"sub\" \"copy\":"), - Self::Cut => println!("{:<indent$}Replaces the current subregion by removing from the schematic", "\"sub\" \"cut\":"), - Self::Paste => println!("{:<indent$}Places a copy of the current subregion into the schematic", "\"sub\" \"paste\":"), - Self::Place => println!("{:<indent$}Places a block if enough space is available", "\"sub\" \"place\":"), - Self::Rotate => println!("{:<indent$}Rotates the current subregion (CCW) in increments of 90 degrees", "\"sub\" \"rotate\":"), - Self::Mirror => println!("{:<indent$}Mirrors the current subregion horizontally or vertically", "\"sub\" \"mirror\":"), - Self::Move => println!("{:<indent$}Moves all blocks by a certain offset", "\"sub\" \"move\":"), - Self::Resize => println!("{:<indent$}Resizes the current subregion and offsets it", "\"sub\" \"resize\":"), - Self::Remove => println!("{:<indent$}Removes blocks at a position or within a region", "\"sub\" \"remove\":"), - Self::Print => println!("{:<indent$}Prints the current subregion in a visual representation", "\"sub\" \"print\":"), - Self::Dump => println!("{:<indent$}Prints the current subregion as a base-64 encoded string", "\"sub\" \"dump\":"), - } - self.print_usage(indent); - } - - fn print_usage(&self, indent: usize) - { - match self - { - Self::Help => (), - Self::Input => println!(r#"{:indent$} Usage: "sub" "input" <base64>"#, ""), - Self::Copy => - { - println!(r#"{:indent$} Usage: "sub" "copy" <x0> <y0> <x1> <y1> [<strict>]"#, ""); - println!(r#"{:indent$} Strictness ignores blocks which are not entirely within the bounds"#, ""); - }, - Self::Cut => - { - println!(r#"{:indent$} Usage: "sub" "cut" <x0> <y0> <x1> <y1> [<strict>]"#, ""); - println!(r#"{:indent$} Strictness ignores blocks which are not entirely within the bounds"#, ""); - }, - Self::Paste => println!(r#"{:indent$} Usage: "sub" "paste" <x> <y>"#, ""), - Self::Place => - { - println!(r#"{:indent$} Usage: "sub" "place" <x> <y> <block name> [<rotation> [<replace>]]"#, ""); - println!(r#"{:indent$} Rotation is one of right, up, left, down or compass angles"#, "") - }, - Self::Rotate => println!(r#"{:indent$} Usage: "sub" "rotate" <angle>"#, ""), - Self::Mirror => println!(r#"{:indent$} Usage: "sub" "mirror" <axis>"#, ""), - Self::Move => println!(r#"{:indent$} Usage: "sub" "move" <dx> <dy>"#, ""), - Self::Resize => println!(r#"{:indent$} Usage: "sub" "resize" <width> <height> [<dx> <dy>]"#, ""), - Self::Remove => println!(r#"{:indent$} Usage: "sub" "remove" <x0> <y0> [<x1> <y1>]"#, ""), - Self::Print | Self::Dump => (), - } - } +impl SubCommand { + fn print_help(&self, indent: usize) { + match self { + Self::Help => println!( + "{:<indent$}Prints a list of available commands", + "\"sub\" \"help\":" + ), + Self::Input => println!( + "{:<indent$}Loads a new subregion from a base-64 encoded string", + "\"sub\" \"input\":" + ), + Self::Copy => println!( + "{:<indent$}Replaces the current subregion by copying from the schematic", + "\"sub\" \"copy\":" + ), + Self::Cut => println!( + "{:<indent$}Replaces the current subregion by removing from the schematic", + "\"sub\" \"cut\":" + ), + Self::Paste => println!( + "{:<indent$}Places a copy of the current subregion into the schematic", + "\"sub\" \"paste\":" + ), + Self::Place => println!( + "{:<indent$}Places a block if enough space is available", + "\"sub\" \"place\":" + ), + Self::Rotate => println!( + "{:<indent$}Rotates the current subregion (CCW) in increments of 90 degrees", + "\"sub\" \"rotate\":" + ), + Self::Mirror => println!( + "{:<indent$}Mirrors the current subregion horizontally or vertically", + "\"sub\" \"mirror\":" + ), + Self::Move => println!( + "{:<indent$}Moves all blocks by a certain offset", + "\"sub\" \"move\":" + ), + Self::Resize => println!( + "{:<indent$}Resizes the current subregion and offsets it", + "\"sub\" \"resize\":" + ), + Self::Remove => println!( + "{:<indent$}Removes blocks at a position or within a region", + "\"sub\" \"remove\":" + ), + Self::Print => println!( + "{:<indent$}Prints the current subregion in a visual representation", + "\"sub\" \"print\":" + ), + Self::Dump => println!( + "{:<indent$}Prints the current subregion as a base-64 encoded string", + "\"sub\" \"dump\":" + ), + } + self.print_usage(indent); + } + + fn print_usage(&self, indent: usize) { + match self { + Self::Help => (), + Self::Input => println!(r#"{:indent$} Usage: "sub" "input" <base64>"#, ""), + Self::Copy => { + println!( + r#"{:indent$} Usage: "sub" "copy" <x0> <y0> <x1> <y1> [<strict>]"#, + "" + ); + println!( + r#"{:indent$} Strictness ignores blocks which are not entirely within the bounds"#, + "" + ); + } + Self::Cut => { + println!( + r#"{:indent$} Usage: "sub" "cut" <x0> <y0> <x1> <y1> [<strict>]"#, + "" + ); + println!( + r#"{:indent$} Strictness ignores blocks which are not entirely within the bounds"#, + "" + ); + } + Self::Paste => println!(r#"{:indent$} Usage: "sub" "paste" <x> <y>"#, ""), + Self::Place => { + println!( + r#"{:indent$} Usage: "sub" "place" <x> <y> <block name> [<rotation> [<replace>]]"#, + "" + ); + println!( + r#"{:indent$} Rotation is one of right, up, left, down or compass angles"#, + "" + ) + } + Self::Rotate => println!(r#"{:indent$} Usage: "sub" "rotate" <angle>"#, ""), + Self::Mirror => println!(r#"{:indent$} Usage: "sub" "mirror" <axis>"#, ""), + Self::Move => println!(r#"{:indent$} Usage: "sub" "move" <dx> <dy>"#, ""), + Self::Resize => println!( + r#"{:indent$} Usage: "sub" "resize" <width> <height> [<dx> <dy>]"#, + "" + ), + Self::Remove => println!( + r#"{:indent$} Usage: "sub" "remove" <x0> <y0> [<x1> <y1>]"#, + "" + ), + Self::Print | Self::Dump => (), + } + } } -fn interpret_sub(state: &mut State, tokens: &mut Tokenizer) -{ - match tokens.next() - { - None => println!(r#"Empty "sub" command, type "sub help" for more info"#), - Some("help") => - { - const INDENT: usize = 20; - println!(r#"List of available commands for \"sub\":"#); - SubCommand::Help.print_help(INDENT); - SubCommand::Input.print_help(INDENT); - SubCommand::Copy.print_help(INDENT); - SubCommand::Cut.print_help(INDENT); - SubCommand::Paste.print_help(INDENT); - SubCommand::Place.print_help(INDENT); - SubCommand::Rotate.print_help(INDENT); - SubCommand::Mirror.print_help(INDENT); - SubCommand::Move.print_help(INDENT); - SubCommand::Resize.print_help(INDENT); - SubCommand::Remove.print_help(INDENT); - SubCommand::Print.print_help(INDENT); - SubCommand::Dump.print_help(INDENT); - if tokens.remainder().is_some() - { - eprintln!("Extra arguments are considered an error"); - } - }, - Some("input") => - { - let subregion = match tokens.next() - { - None => - { - eprintln!("Missing argument: base64"); - SubCommand::Input.print_usage(0); - return; - }, - Some(b64) => - { - match SchematicSerializer(state.reg).deserialize_base64(b64) - { - Ok(s) => s, - Err(e) => - { - print_err!(e, "Could not deserialize schematic"); - return; - }, - } - }, - }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub input""#); - SubCommand::Input.print_usage(0); - return; - } - state.subregion = Some(subregion); - }, - Some(op @ ("copy" | "cut")) => - { - let modify_original = op == "cut"; - let Some(ref mut schematic) = state.schematic +fn interpret_sub(state: &mut State, tokens: &mut Tokenizer) { + match tokens.next() { + None => println!(r#"Empty "sub" command, type "sub help" for more info"#), + Some("help") => { + const INDENT: usize = 20; + println!(r#"List of available commands for \"sub\":"#); + SubCommand::Help.print_help(INDENT); + SubCommand::Input.print_help(INDENT); + SubCommand::Copy.print_help(INDENT); + SubCommand::Cut.print_help(INDENT); + SubCommand::Paste.print_help(INDENT); + SubCommand::Place.print_help(INDENT); + SubCommand::Rotate.print_help(INDENT); + SubCommand::Mirror.print_help(INDENT); + SubCommand::Move.print_help(INDENT); + SubCommand::Resize.print_help(INDENT); + SubCommand::Remove.print_help(INDENT); + SubCommand::Print.print_help(INDENT); + SubCommand::Dump.print_help(INDENT); + if tokens.remainder().is_some() { + eprintln!("Extra arguments are considered an error"); + } + } + Some("input") => { + let subregion = match tokens.next() { + None => { + eprintln!("Missing argument: base64"); + SubCommand::Input.print_usage(0); + return; + } + Some(b64) => match SchematicSerializer(state.reg).deserialize_base64(b64) { + Ok(s) => s, + Err(e) => { + print_err!(e, "Could not deserialize schematic"); + return; + } + }, + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub input""#); + SubCommand::Input.print_usage(0); + return; + } + state.subregion = Some(subregion); + } + Some(op @ ("copy" | "cut")) => { + let modify_original = op == "cut"; + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "sub {op}" requires an active schematic (see "help")"#); return; }; - let x0 = if modify_original {parse_num!(SubCommand::Cut, tokens, "x0", u16)} else {parse_num!(SubCommand::Copy, tokens, "x0", u16)}; - let y0 = if modify_original {parse_num!(SubCommand::Cut, tokens, "y0", u16)} else {parse_num!(SubCommand::Copy, tokens, "y0", u16)}; - if x0 >= schematic.get_width() || y0 >= schematic.get_height() - { - eprintln!("Invalid lower coordinate ({x0} / {y0}) out of bounds ({} / {})", schematic.get_width(), schematic.get_height()); - return; - } - let x1 = if modify_original {parse_num!(SubCommand::Cut, tokens, "x1", u16)} else {parse_num!(SubCommand::Copy, tokens, "x1", u16)}; - let y1 = if modify_original {parse_num!(SubCommand::Cut, tokens, "y1", u16)} else {parse_num!(SubCommand::Copy, tokens, "y1", u16)}; - if x1 < x0 || y1 < y0 - { - eprintln!("Invalid upper coordinate ({x1} / {y1}) too low (lower bound {x0} / {y0})"); - return; - } - if x1 >= schematic.get_width() || y1 >= schematic.get_height() - { - eprintln!("Invalid upper coordinate ({x1} / {y1}) out of bounds ({} / {})", schematic.get_width(), schematic.get_height()); - return; - } - let strict = match tokens.next() - { - None => false, - Some("true") | Some("yes") => true, - Some("false") | Some("no") => false, - Some(strict) => - { - eprintln!("Invalid strictness {strict:?}"); - return; - }, - }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub {op}""#); - SubCommand::Copy.print_usage(0); - return; - } - let mut true_x0 = x0; - let mut true_y0 = y0; - let mut true_x1 = x1; - let mut true_y1 = y1; - let mut targets = Vec::new(); - for p in schematic.block_iter() - { - let sz = p.get_block().get_size() as u16; - let pos0 = (p.get_pos().0 - (sz - 1) / 2, p.get_pos().1 - (sz - 1) / 2); - let pos1 = (p.get_pos().0 + sz / 2, p.get_pos().1 + sz / 2); - let include = if strict {pos0.0 >= x0 && pos0.1 >= y0 && pos1.0 <= x1 && pos1.1 <= y1} - else {pos0.0 <= x1 && pos0.1 <= y1 && pos1.0 >= x0 && pos1.1 >= y0}; - if include - { - targets.push(p.get_pos()); - if !strict - { - if pos0.0 < true_x0 {true_x0 = pos0.0;} - if pos0.1 < true_y0 {true_y0 = pos0.1;} - if pos1.0 > true_x1 {true_x1 = pos1.0;} - if pos1.1 > true_y1 {true_y1 = pos1.1;} - } - } - } - let mut subregion = Schematic::new(true_x1 - true_x0 + 1, true_y1 - true_y0 + 1); - if !targets.is_empty() - { - if modify_original {state.unsaved = true;} - for GridPos(x, y) in targets - { - let mut original: Option<Placement> = None; - let place = if modify_original - { - original = schematic.take(x, y).unwrap(); - original.as_ref().unwrap() - } - else {schematic.get(x, y).unwrap().unwrap()}; - let data = match place.get_state() - { - None => DynData::Empty, - Some(d) => place.get_block().serialize_state(d).unwrap(), - }; - subregion.set(place.get_pos().0 - true_x0, place.get_pos().1 - true_y0, place.get_block(), data, place.get_rotation()).unwrap(); - drop(original); - } - } - state.subregion = Some(subregion); - }, - Some("paste") => - { - let Some(ref mut schematic) = state.schematic + let x0 = if modify_original { + parse_num!(SubCommand::Cut, tokens, "x0", u16) + } else { + parse_num!(SubCommand::Copy, tokens, "x0", u16) + }; + let y0 = if modify_original { + parse_num!(SubCommand::Cut, tokens, "y0", u16) + } else { + parse_num!(SubCommand::Copy, tokens, "y0", u16) + }; + if x0 >= schematic.get_width() || y0 >= schematic.get_height() { + eprintln!( + "Invalid lower coordinate ({x0} / {y0}) out of bounds ({} / {})", + schematic.get_width(), + schematic.get_height() + ); + return; + } + let x1 = if modify_original { + parse_num!(SubCommand::Cut, tokens, "x1", u16) + } else { + parse_num!(SubCommand::Copy, tokens, "x1", u16) + }; + let y1 = if modify_original { + parse_num!(SubCommand::Cut, tokens, "y1", u16) + } else { + parse_num!(SubCommand::Copy, tokens, "y1", u16) + }; + if x1 < x0 || y1 < y0 { + eprintln!( + "Invalid upper coordinate ({x1} / {y1}) too low (lower bound {x0} / {y0})" + ); + return; + } + if x1 >= schematic.get_width() || y1 >= schematic.get_height() { + eprintln!( + "Invalid upper coordinate ({x1} / {y1}) out of bounds ({} / {})", + schematic.get_width(), + schematic.get_height() + ); + return; + } + let strict = match tokens.next() { + None => false, + Some("true") | Some("yes") => true, + Some("false") | Some("no") => false, + Some(strict) => { + eprintln!("Invalid strictness {strict:?}"); + return; + } + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub {op}""#); + SubCommand::Copy.print_usage(0); + return; + } + let mut true_x0 = x0; + let mut true_y0 = y0; + let mut true_x1 = x1; + let mut true_y1 = y1; + let mut targets = Vec::new(); + for p in schematic.block_iter() { + let sz = p.get_block().get_size() as u16; + let pos0 = (p.get_pos().0 - (sz - 1) / 2, p.get_pos().1 - (sz - 1) / 2); + let pos1 = (p.get_pos().0 + sz / 2, p.get_pos().1 + sz / 2); + let include = if strict { + pos0.0 >= x0 && pos0.1 >= y0 && pos1.0 <= x1 && pos1.1 <= y1 + } else { + pos0.0 <= x1 && pos0.1 <= y1 && pos1.0 >= x0 && pos1.1 >= y0 + }; + if include { + targets.push(p.get_pos()); + if !strict { + if pos0.0 < true_x0 { + true_x0 = pos0.0; + } + if pos0.1 < true_y0 { + true_y0 = pos0.1; + } + if pos1.0 > true_x1 { + true_x1 = pos1.0; + } + if pos1.1 > true_y1 { + true_y1 = pos1.1; + } + } + } + } + let mut subregion = Schematic::new(true_x1 - true_x0 + 1, true_y1 - true_y0 + 1); + if !targets.is_empty() { + if modify_original { + state.unsaved = true; + } + for GridPos(x, y) in targets { + let mut original: Option<Placement> = None; + let place = if modify_original { + original = schematic.take(x, y).unwrap(); + original.as_ref().unwrap() + } else { + schematic.get(x, y).unwrap().unwrap() + }; + let data = match place.get_state() { + None => DynData::Empty, + Some(d) => place.get_block().serialize_state(d).unwrap(), + }; + subregion + .set( + place.get_pos().0 - true_x0, + place.get_pos().1 - true_y0, + place.get_block(), + data, + place.get_rotation(), + ) + .unwrap(); + drop(original); + } + } + state.subregion = Some(subregion); + } + Some("paste") => { + let Some(ref mut schematic) = state.schematic else { eprintln!(r#"Command "sub paste" requires an active schematic (see "help")"#); return; }; - let Some(ref subregion) = state.subregion + let Some(ref subregion) = state.subregion else { eprintln!(r#"Command "sub paste" requires an active subregion (see "sub help")"#); return; }; - let x = parse_num!(SubCommand::Paste, tokens, "x", u16); - let y = parse_num!(SubCommand::Paste, tokens, "y", u16); - if subregion.get_width() > schematic.get_width() || subregion.get_height() > schematic.get_height() - { - eprintln!("Subregion ({} / {}) is larger than schematic ({} / {})", subregion.get_width(), subregion.get_height(), - schematic.get_width(), schematic.get_height()); - return; - } - if x >= schematic.get_width() - subregion.get_width() - { - let x1 = x + subregion.get_width() - 1; - let y1 = y + subregion.get_height() - 1; - eprintln!("Invalid coordinate ({x} / {y} to {x1} / {y1}) out of bounds ({} / {})", schematic.get_width(), schematic.get_height()); - return; - } - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub paste""#); - SubCommand::Paste.print_usage(0); - return; - } - state.unsaved = true; - for p in subregion.block_iter() - { - let data = match p.get_state() - { - None => DynData::Empty, - Some(d) => p.get_block().serialize_state(d).unwrap(), - }; - schematic.replace(x + p.get_pos().0, y + p.get_pos().1, p.get_block(), data, p.get_rotation(), false).unwrap(); - } - }, - Some("place") => - { - let Some(ref mut subregion) = state.subregion + let x = parse_num!(SubCommand::Paste, tokens, "x", u16); + let y = parse_num!(SubCommand::Paste, tokens, "y", u16); + if subregion.get_width() > schematic.get_width() + || subregion.get_height() > schematic.get_height() + { + eprintln!( + "Subregion ({} / {}) is larger than schematic ({} / {})", + subregion.get_width(), + subregion.get_height(), + schematic.get_width(), + schematic.get_height() + ); + return; + } + if x >= schematic.get_width() - subregion.get_width() { + let x1 = x + subregion.get_width() - 1; + let y1 = y + subregion.get_height() - 1; + eprintln!( + "Invalid coordinate ({x} / {y} to {x1} / {y1}) out of bounds ({} / {})", + schematic.get_width(), + schematic.get_height() + ); + return; + } + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub paste""#); + SubCommand::Paste.print_usage(0); + return; + } + state.unsaved = true; + for p in subregion.block_iter() { + let data = match p.get_state() { + None => DynData::Empty, + Some(d) => p.get_block().serialize_state(d).unwrap(), + }; + schematic + .replace( + x + p.get_pos().0, + y + p.get_pos().1, + p.get_block(), + data, + p.get_rotation(), + false, + ) + .unwrap(); + } + } + Some("place") => { + let Some(ref mut subregion) = state.subregion else { eprintln!(r#"Command "sub place" requires an active subregion (see "sub help")"#); return; }; - let x = parse_num!(SubCommand::Place, tokens, "x", u16); - let y = parse_num!(SubCommand::Place, tokens, "y", u16); - if x >= subregion.get_width() || y >= subregion.get_height() - { - eprintln!("Invalid coordinate ({x} / {y}) out of bounds ({} / {})", subregion.get_width(), subregion.get_height()); - return; - } - let block = match tokens.next() - { - None => - { - eprintln!("Missing argument: block name"); - SubCommand::Place.print_usage(0); - return; - }, - Some(name) => - { - match state.reg.get(name) - { - None => - { - eprintln!("No such block {name:?}"); - return; - }, - Some(b) => b, - } - }, - }; - let rot = match tokens.next() - { - None => None, - Some("right") | Some("east") => Some(Rotation::Right), - Some("up") | Some("north") => Some(Rotation::Up), - Some("left") | Some("west") => Some(Rotation::Left), - Some("down") | Some("south") => Some(Rotation::Down), - Some(rot) => - { - eprintln!("Invalid rotation {rot:?}"); - return; - }, - }; - let replace = if rot.is_some() - { - match tokens.next() - { - None => None, - Some("true") | Some("yes") => Some(true), - Some("false") | Some("no") => Some(false), - Some(replace) => - { - eprintln!("Invalid replacement {replace:?}"); - return; - }, - } - } - else {None}; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub place""#); - SubCommand::Place.print_usage(0); - return; - } - let rot = rot.unwrap_or(Rotation::Right); - let result = if replace.unwrap_or(false) - { - subregion.replace(x, y, block, DynData::Empty, rot, false).err() - } - else - { - subregion.set(x, y, block, DynData::Empty, rot).err() - }; - if let Some(e) = result - { - print_err!(e, "Failed to place block at {x} / {y}"); - return; - } - }, - Some("rotate") => - { - let Some(ref mut subregion) = state.subregion + let x = parse_num!(SubCommand::Place, tokens, "x", u16); + let y = parse_num!(SubCommand::Place, tokens, "y", u16); + if x >= subregion.get_width() || y >= subregion.get_height() { + eprintln!( + "Invalid coordinate ({x} / {y}) out of bounds ({} / {})", + subregion.get_width(), + subregion.get_height() + ); + return; + } + let block = match tokens.next() { + None => { + eprintln!("Missing argument: block name"); + SubCommand::Place.print_usage(0); + return; + } + Some(name) => match state.reg.get(name) { + None => { + eprintln!("No such block {name:?}"); + return; + } + Some(b) => b, + }, + }; + let rot = match tokens.next() { + None => None, + Some("right") | Some("east") => Some(Rotation::Right), + Some("up") | Some("north") => Some(Rotation::Up), + Some("left") | Some("west") => Some(Rotation::Left), + Some("down") | Some("south") => Some(Rotation::Down), + Some(rot) => { + eprintln!("Invalid rotation {rot:?}"); + return; + } + }; + let replace = if rot.is_some() { + match tokens.next() { + None => None, + Some("true") | Some("yes") => Some(true), + Some("false") | Some("no") => Some(false), + Some(replace) => { + eprintln!("Invalid replacement {replace:?}"); + return; + } + } + } else { + None + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub place""#); + SubCommand::Place.print_usage(0); + return; + } + let rot = rot.unwrap_or(Rotation::Right); + let result = if replace.unwrap_or(false) { + subregion + .replace(x, y, block, DynData::Empty, rot, false) + .err() + } else { + subregion.set(x, y, block, DynData::Empty, rot).err() + }; + if let Some(e) = result { + print_err!(e, "Failed to place block at {x} / {y}"); + return; + } + } + Some("rotate") => { + let Some(ref mut subregion) = state.subregion else { eprintln!(r#"Command "sub rotate" requires an active subgregion (see "sub help")"#); return; }; - let angle = parse_num!(SubCommand::Rotate, tokens, "angle", i32); - if angle % 90 != 0 - { - eprintln!("Rotation angle must be a multiple of 90 degrees"); - return; - } - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub rotate""#); - SubCommand::Rotate.print_usage(0); - return; - } - match (angle / 90) % 4 - { - 0 => (), - 1 | -3 => subregion.rotate(false), - 2 | -2 => subregion.rotate_180(), - 3 | -1 => subregion.rotate(true), - a => unreachable!("angle {angle} -> {a}"), - } - }, - Some("mirror") => - { - let Some(ref mut subregion) = state.subregion + let angle = parse_num!(SubCommand::Rotate, tokens, "angle", i32); + if angle % 90 != 0 { + eprintln!("Rotation angle must be a multiple of 90 degrees"); + return; + } + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub rotate""#); + SubCommand::Rotate.print_usage(0); + return; + } + match (angle / 90) % 4 { + 0 => (), + 1 | -3 => subregion.rotate(false), + 2 | -2 => subregion.rotate_180(), + 3 | -1 => subregion.rotate(true), + a => unreachable!("angle {angle} -> {a}"), + } + } + Some("mirror") => { + let Some(ref mut subregion) = state.subregion else { eprintln!(r#"Command "sub mirror" requires an active subregion (see "sub help")"#); return; }; - let (x, y) = match tokens.next() - { - None => - { - eprintln!("Missing argument: axis"); - SubCommand::Mirror.print_usage(0); - return; - }, - Some("x") | Some("h") | Some("horizontal") | Some("horizontally") => (true, false), - Some("y") | Some("v") | Some("vertical") | Some("vertically") => (false, true), - Some("both") | Some("all") => (true, true), - Some(axis) => - { - eprintln!("Invalid mirroring axis: {axis:?}"); - return; - }, - }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub mirror""#); - SubCommand::Mirror.print_usage(0); - return; - } - subregion.mirror(x, y); - }, - Some("move") => - { - let Some(ref mut subregion) = state.subregion + let (x, y) = match tokens.next() { + None => { + eprintln!("Missing argument: axis"); + SubCommand::Mirror.print_usage(0); + return; + } + Some("x") | Some("h") | Some("horizontal") | Some("horizontally") => (true, false), + Some("y") | Some("v") | Some("vertical") | Some("vertically") => (false, true), + Some("both") | Some("all") => (true, true), + Some(axis) => { + eprintln!("Invalid mirroring axis: {axis:?}"); + return; + } + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub mirror""#); + SubCommand::Mirror.print_usage(0); + return; + } + subregion.mirror(x, y); + } + Some("move") => { + let Some(ref mut subregion) = state.subregion else { eprintln!(r#"Command "sub move" requires an active subregion (see "sub help")"#); return; }; - let dx = parse_num!(SubCommand::Move, tokens, "dx", i16); - let dy = parse_num!(SubCommand::Move, tokens, "dy", i16); - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub move""#); - SubCommand::Move.print_usage(0); - return; - } - if dx != 0 && dy != 0 - { - if let Err(e) = subregion.resize(dx, dy, subregion.get_width(), subregion.get_height()) - { - match e - { - ResizeError::XOffset{dx, old_w, new_w} => - { - debug_assert_eq!(old_w, new_w); - eprintln!("Invalid horizontal move {dx} not in ]-{old_w}, {new_w}["); - }, - ResizeError::YOffset{dy, old_h, new_h} => - { - debug_assert_eq!(old_h, new_h); - eprintln!("Invalid vertical move {dy} not in ]-{old_h}, {new_h}["); - }, - ResizeError::Truncated{right, top, left, bottom} => - { - eprint!("Move would truncate subregion: "); - let mut first = true; - if right > 0 - { - eprint!("{}right: {right}", if first {""} else {", "}); - first = false; - } - if top > 0 - { - eprint!("{}top: {top}", if first {""} else {", "}); - first = false; - } - if left > 0 - { - eprint!("{}left: {left}", if first {""} else {", "}); - first = false; - } - if bottom > 0 - { - eprint!("{}bottom: {bottom}", if first {""} else {", "}); - first = false; - } - if first {eprintln!("<unknown>");} else {eprintln!();} - }, - _ => print_err!(e, "Unexpected resize error (for {dx} / {dy})") - } - } - } - }, - Some("resize") => - { - let Some(ref mut subregion) = state.subregion + let dx = parse_num!(SubCommand::Move, tokens, "dx", i16); + let dy = parse_num!(SubCommand::Move, tokens, "dy", i16); + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub move""#); + SubCommand::Move.print_usage(0); + return; + } + if dx != 0 && dy != 0 { + if let Err(e) = + subregion.resize(dx, dy, subregion.get_width(), subregion.get_height()) + { + match e { + ResizeError::XOffset { dx, old_w, new_w } => { + debug_assert_eq!(old_w, new_w); + eprintln!("Invalid horizontal move {dx} not in ]-{old_w}, {new_w}["); + } + ResizeError::YOffset { dy, old_h, new_h } => { + debug_assert_eq!(old_h, new_h); + eprintln!("Invalid vertical move {dy} not in ]-{old_h}, {new_h}["); + } + ResizeError::Truncated { + right, + top, + left, + bottom, + } => { + eprint!("Move would truncate subregion: "); + let mut first = true; + if right > 0 { + eprint!("{}right: {right}", if first { "" } else { ", " }); + first = false; + } + if top > 0 { + eprint!("{}top: {top}", if first { "" } else { ", " }); + first = false; + } + if left > 0 { + eprint!("{}left: {left}", if first { "" } else { ", " }); + first = false; + } + if bottom > 0 { + eprint!("{}bottom: {bottom}", if first { "" } else { ", " }); + first = false; + } + if first { + eprintln!("<unknown>"); + } else { + eprintln!(); + } + } + _ => print_err!(e, "Unexpected resize error (for {dx} / {dy})"), + } + } + } + } + Some("resize") => { + let Some(ref mut subregion) = state.subregion else { eprintln!(r#"Command "sub resize" requires an active subregion (see "sub help")"#); return; }; - let w = parse_num!(SubCommand::Resize, tokens, "width", u16); - if w == 0 - { - eprintln!("Subregion width must be positive"); - return; - } - let h = parse_num!(SubCommand::Resize, tokens, "height", u16); - if h == 0 - { - eprintln!("Subregion height must be positive"); - return; - } - let (dx, dy) = if let arg @ Some(..) = tokens.next() - { - let dx = parse_num!(SubCommand::Resize, "dx", <i16>::from(arg)); - let dy = parse_num!(SubCommand::Resize, tokens, "dy", i16); - (dx, dy) - } - else {(0, 0)}; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub resize""#); - SubCommand::Resize.print_usage(0); - return; - } - if w != subregion.get_width() || h != subregion.get_height() || dx != 0 || dy != 0 - { - if let Err(e) = subregion.resize(dx, dy, w, h) - { - print_err!(e, "Could not resize subregion"); - } - } - }, - Some("remove") => - { - let Some(ref mut subregion) = state.subregion + let w = parse_num!(SubCommand::Resize, tokens, "width", u16); + if w == 0 { + eprintln!("Subregion width must be positive"); + return; + } + let h = parse_num!(SubCommand::Resize, tokens, "height", u16); + if h == 0 { + eprintln!("Subregion height must be positive"); + return; + } + let (dx, dy) = if let arg @ Some(..) = tokens.next() { + let dx = parse_num!(SubCommand::Resize, "dx", <i16>::from(arg)); + let dy = parse_num!(SubCommand::Resize, tokens, "dy", i16); + (dx, dy) + } else { + (0, 0) + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub resize""#); + SubCommand::Resize.print_usage(0); + return; + } + if w != subregion.get_width() || h != subregion.get_height() || dx != 0 || dy != 0 { + if let Err(e) = subregion.resize(dx, dy, w, h) { + print_err!(e, "Could not resize subregion"); + } + } + } + Some("remove") => { + let Some(ref mut subregion) = state.subregion else { eprintln!(r#"Command "sub remove" requires an active subregion (see "sub help")"#); return; }; - let x0 = parse_num!(SubCommand::Remove, tokens, "x0", u16); - let y0 = parse_num!(SubCommand::Remove, tokens, "y0", u16); - if x0 >= subregion.get_width() || y0 >= subregion.get_height() - { - eprintln!("Invalid coordinate ({x0} / {y0}) out of bounds ({} / {})", subregion.get_width(), subregion.get_height()); - return; - } - let (x0, y0, x1, y1) = if let arg @ Some(..) = tokens.next() - { - let x1 = parse_num!(SubCommand::Remove, "x1", <u16>::from(arg)); - let y1 = parse_num!(SubCommand::Remove, tokens, "y1", u16); - if x1 >= subregion.get_width() || y1 >= subregion.get_height() - { - eprintln!("Invalid coordinate ({x1} / {y1}) out of bounds ({} / {})", subregion.get_width(), subregion.get_height()); - return; - } - (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)) - } - else {(x0, y0, x0, y0)}; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub remove""#); - SubCommand::Remove.print_usage(0); - return; - } - if x1 > x0 || y1 > y0 - { - let mut cnt = 0u32; - for y in y0..=y1 - { - for x in x0..=x1 - { - // position was already checked while parsing - if subregion.take(x, y).unwrap().is_some() {cnt += 1;} - } - } - println!("Removed {cnt} blocks in {x0} / {y0} to {x1} / {y1}"); - } - else - { - // position was already checked while parsing - match subregion.take(x0, y0).unwrap() - { - None => (), - Some(p) => println!("Removed block {} from {x0} / {y0}", p.get_block().get_name()), - } - } - }, - Some("print") => - { - let Some(ref subregion) = state.subregion + let x0 = parse_num!(SubCommand::Remove, tokens, "x0", u16); + let y0 = parse_num!(SubCommand::Remove, tokens, "y0", u16); + if x0 >= subregion.get_width() || y0 >= subregion.get_height() { + eprintln!( + "Invalid coordinate ({x0} / {y0}) out of bounds ({} / {})", + subregion.get_width(), + subregion.get_height() + ); + return; + } + let (x0, y0, x1, y1) = if let arg @ Some(..) = tokens.next() { + let x1 = parse_num!(SubCommand::Remove, "x1", <u16>::from(arg)); + let y1 = parse_num!(SubCommand::Remove, tokens, "y1", u16); + if x1 >= subregion.get_width() || y1 >= subregion.get_height() { + eprintln!( + "Invalid coordinate ({x1} / {y1}) out of bounds ({} / {})", + subregion.get_width(), + subregion.get_height() + ); + return; + } + (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)) + } else { + (x0, y0, x0, y0) + }; + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub remove""#); + SubCommand::Remove.print_usage(0); + return; + } + if x1 > x0 || y1 > y0 { + let mut cnt = 0u32; + for y in y0..=y1 { + for x in x0..=x1 { + // position was already checked while parsing + if subregion.take(x, y).unwrap().is_some() { + cnt += 1; + } + } + } + println!("Removed {cnt} blocks in {x0} / {y0} to {x1} / {y1}"); + } else { + // position was already checked while parsing + match subregion.take(x0, y0).unwrap() { + None => (), + Some(p) => println!( + "Removed block {} from {x0} / {y0}", + p.get_block().get_name() + ), + } + } + } + Some("print") => { + let Some(ref subregion) = state.subregion else { eprintln!(r#"Command "sub print" requires an active subregion (see "sub help")"#); return; }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub print""#); - SubCommand::Print.print_usage(0); - return; - } - print_schematic(subregion); - }, - Some("dump") => - { - let Some(ref subregion) = state.subregion + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub print""#); + SubCommand::Print.print_usage(0); + return; + } + print_schematic(subregion); + } + Some("dump") => { + let Some(ref subregion) = state.subregion else { eprintln!(r#"Command "sub dump" requires an active subregion (see "sub help")"#); return; }; - if tokens.remainder().is_some() - { - eprintln!(r#"Too many parameters for "sub dump""#); - SubCommand::Dump.print_usage(0); - return; - } - let b64 = match SchematicSerializer(state.reg).serialize_base64(subregion) - { - Ok(b64) => b64, - Err(e) => - { - print_err!(e, "Could not serialize subregion"); - return; - }, - }; - println!("Subregion: {}", b64); - }, - Some(unknown) => eprintln!("Unknown command \"sub\" {unknown:?}"), - } + if tokens.remainder().is_some() { + eprintln!(r#"Too many parameters for "sub dump""#); + SubCommand::Dump.print_usage(0); + return; + } + let b64 = match SchematicSerializer(state.reg).serialize_base64(subregion) { + Ok(b64) => b64, + Err(e) => { + print_err!(e, "Could not serialize subregion"); + return; + } + }; + println!("Subregion: {}", b64); + } + Some(unknown) => eprintln!("Unknown command \"sub\" {unknown:?}"), + } } |