use std::borrow::Cow;
use std::env::Args;
use std::io::{self, Write};
use std::fs;
use crate::block::{BlockRegistry, build_registry, Rotation};
use crate::data::dynamic::DynData;
use crate::data::{base64, DataRead, Serializer, DataWrite, GridPos};
use crate::data::schematic::{Placement, ResizeError, Schematic, SchematicSerializer};
use crate::exe::print::print_schematic;
use crate::exe::print_err;
use crate::exe::args::{self, ArgCount, ArgOption, OptionHandler};
use crate::registry::RegistryEntry;
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"),
}
}
}
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
}
}
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 => (),
}
}
}
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
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
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
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
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
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
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
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
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
else
{
eprintln!(r#"Command "save" requires an active schematic (see "help")"#);
return;
};
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:?}"),
}
}
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 => (),
}
}
}
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
else
{
eprintln!(r#"Command "sub paste" requires an active schematic (see "help")"#);
return;
};
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
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
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
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
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
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
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
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
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:?}"),
}
}