mindustry logic execution, map- and schematic- parsing and rendering
Implement interactive schematic editor
KosmosPrime 2023-01-27
parent 00c2618 · commit 3c5ce41
-rw-r--r--src/exe/edit.rs741
-rw-r--r--src/exe/mod.rs2
-rw-r--r--src/exe/print.rs2
3 files changed, 744 insertions, 1 deletions
diff --git a/src/exe/edit.rs b/src/exe/edit.rs
new file mode 100644
index 0000000..051394b
--- /dev/null
+++ b/src/exe/edit.rs
@@ -0,0 +1,741 @@
+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::{DataRead, Serializer, DataWrite, base64};
+use crate::data::schematic::{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,
+ 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(&reg);
+ let mut state = State{reg: &reg, schematic: None, unsaved: false, 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, Remove, 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::Remove => println!("{:<indent$}Removes blocks at a position or within a region", "\"remove\":"),
+ 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(12);
+ }
+
+ 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::Remove => println!(r#"{:indent$} Usage: "remove" <x0> <y0> [<x1> <y1>]"#, ""),
+ Self::Print | Self::Dump => (),
+ Self::Save => println!(r#"{:indent$} Usage: "save" <save path>"#, ""),
+ Self::Quit => (),
+ }
+ }
+}
+
+macro_rules!parse_num
+{
+ ($cmd:ident, $name:expr, <$type:ty>::from($val:expr)) =>
+ {
+ match $val
+ {
+ None =>
+ {
+ eprintln!("Missing argument: {}", $name);
+ Command::$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);
+ Command::$cmd.print_usage(0);
+ return;
+ },
+ }
+ },
+ }
+ };
+ ($cmd:ident, $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:"#);
+ Command::Help.print_help(12);
+ Command::New.print_help(12);
+ Command::Input.print_help(12);
+ Command::Load.print_help(12);
+ Command::Place.print_help(12);
+ Command::Rotate.print_help(12);
+ Command::Mirror.print_help(12);
+ Command::Remove.print_help(12);
+ Command::Print.print_help(12);
+ Command::Dump.print_help(12);
+ Command::Save.print_help(12);
+ Command::Quit.print_help(12);
+ 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!(New, tokens, "width", u16);
+ if width == 0
+ {
+ eprintln!("Schematic width must be positive");
+ return;
+ }
+ let height = parse_num!(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!(Place, tokens, "x", u16);
+ let y = parse_num!(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!(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("remove") =>
+ {
+ let Some(ref mut schematic) = state.schematic
+ else
+ {
+ eprintln!(r#"Command "remove" requires an active schematic (see "help")"#);
+ return;
+ };
+ let x0 = parse_num!(Remove, tokens, "x0", u16);
+ let y0 = parse_num!(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!(Remove, "x1", <u16>::from(arg));
+ let y1 = parse_num!(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("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:?}"),
+ }
+}
diff --git a/src/exe/mod.rs b/src/exe/mod.rs
index dfa0365..23ed745 100644
--- a/src/exe/mod.rs
+++ b/src/exe/mod.rs
@@ -1,6 +1,7 @@
use std::env::Args;
pub mod args;
+pub mod edit;
pub mod print;
macro_rules!print_err
@@ -33,6 +34,7 @@ pub fn main(mut args: Args)
match args.next()
{
None => panic!("not enough arguments"),
+ Some(s) if s == "edit" => edit::main(args, 1),
Some(s) if s == "print" => print::main(args, 1),
Some(s) => panic!("unknown argument {s}"),
}
diff --git a/src/exe/print.rs b/src/exe/print.rs
index 78876ce..591701b 100644
--- a/src/exe/print.rs
+++ b/src/exe/print.rs
@@ -145,7 +145,7 @@ pub fn main(mut args: Args, arg_off: usize)
}
}
-fn print_schematic(s: &Schematic)
+pub fn print_schematic(s: &Schematic)
{
if let Some(name) = s.get_tags().get("name")
{