mindustry logic execution, map- and schematic- parsing and rendering
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | items.py | 23 | ||||
| -rw-r--r-- | src/block/base.rs | 13 | ||||
| -rw-r--r-- | src/block/defense.rs | 27 | ||||
| -rw-r--r-- | src/block/extraction.rs | 6 | ||||
| -rw-r--r-- | src/block/factory.rs | 16 | ||||
| -rw-r--r-- | src/block/fluid.rs | 12 | ||||
| -rw-r--r-- | src/block/mod.rs | 6 | ||||
| -rw-r--r-- | src/block/payload.rs | 27 | ||||
| -rw-r--r-- | src/block/power.rs | 15 | ||||
| -rw-r--r-- | src/block/simple.rs | 2 | ||||
| -rw-r--r-- | src/block/transport.rs | 11 | ||||
| -rw-r--r-- | src/block/turret.rs | 14 | ||||
| -rw-r--r-- | src/data/base64.rs | 336 | ||||
| -rw-r--r-- | src/data/schematic.rs | 106 | ||||
| -rw-r--r-- | src/exe/edit.rs | 1488 | ||||
| -rw-r--r-- | src/exe/mod.rs | 2 | ||||
| -rw-r--r-- | src/exe/print.rs | 6 | ||||
| -rw-r--r-- | src/item/mod.rs | 3 |
19 files changed, 216 insertions, 1898 deletions
@@ -9,6 +9,7 @@ license = "GPL-3.0" [dependencies] flate2 = { version = "1.0", features = ["zlib"], default-features = false } +base64 = "0.21.2" paste = "1.0.12" [[bin]] diff --git a/items.py b/items.py new file mode 100644 index 0000000..922cc30 --- /dev/null +++ b/items.py @@ -0,0 +1,23 @@ +import re +from sys import argv +import pyperclip + +string = pyperclip.paste() +reg = re.compile(r"with\((.+)\)\);") +match = reg.search(string)[1].replace("Items", "") # .replace(",", ":") +out: str = "" +i: int = -1 +j: int = 0 +while i < len(match) - 1: + i += 1 + if match[i] == ".": + out += match[i + 1].upper() + i += 1 + continue + if match[i] == ",": + j += 1 + if j % 2 != 0: + out += ":" + continue + out += match[i] +pyperclip.copy(f"cost!({out})") diff --git a/src/block/base.rs b/src/block/base.rs index ded5ec4..394415a 100644 --- a/src/block/base.rs +++ b/src/block/base.rs @@ -19,11 +19,24 @@ make_register! { "core-shard" => SimpleBlock::new(3, true, cost!(Copper: 1000, Lead: 800)); "core-foundation" => SimpleBlock::new(4, true, cost!(Copper: 3000, Lead: 3000, Silicon: 2000)); "core-nucleus" => SimpleBlock::new(5, true, cost!(Copper: 8000, Lead: 8000, Thorium: 4000, Silicon: 5000)); + "core-bastion" => SimpleBlock::new(4, true, cost!(Graphite: 1000, Silicon: 1000, Beryllium: 800)); + "core-citadel" => SimpleBlock::new(5, true, cost!(Silicon: 4000, Beryllium: 4000, Tungsten: 3000, Oxide: 1000)); + "core-acropolis" => SimpleBlock::new(6, true, cost!(Beryllium: 6000, Silicon: 5000, Tungsten: 5000, Carbide: 3000, Oxide: 3000)); "container" => SimpleBlock::new(2, true, cost!(Titanium: 100)); "vault" => SimpleBlock::new(3, true, cost!(Titanium: 250, Thorium: 125)); "unloader" => ItemBlock::new(1, true, cost!(Titanium: 25, Silicon: 30)); + "reinforced-container" => SimpleBlock::new(2, true, cost!(Tungsten: 30, Graphite: 40)); + "reinforced-vault" => SimpleBlock::new(3, true, cost!(Tungsten: 125, Thorium: 70, Beryllium: 100)); "illuminator" => LampBlock::new(1, true, cost!(Lead: 8, Graphite: 12, Silicon: 8)); "launch-pad" => SimpleBlock::new(3, true, cost!(Copper: 350, Lead: 200, Titanium: 150, Silicon: 140)); + "radar" => SimpleBlock::new(1, true, cost!(Silicon: 60, Graphite: 50, Beryllium: 10)); + "build-tower" => SimpleBlock::new(3, true, cost!(Silicon: 150, Oxide: 40, Thorium: 60)); + "regen-projector" => SimpleBlock::new(3, true, cost!(Silicon: 80, Tungsten: 60, Oxide: 40, Beryllium: 80)); + // barrier projector + // editor only + "shockwave-tower" => SimpleBlock::new(3, true, cost!(SurgeAlloy: 50, Silicon: 150, Oxide: 30, Tungsten: 100)); + "shield-projector" => SimpleBlock::new(3, true, &[]); + "large-shield-projector" => SimpleBlock::new(4, true, &[]); } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/src/block/defense.rs b/src/block/defense.rs index 05f8e98..d136dc6 100644 --- a/src/block/defense.rs +++ b/src/block/defense.rs @@ -8,29 +8,36 @@ use crate::data::dynamic::{DynData, DynType}; use crate::data::GridPos; use crate::item::storage::Storage; -make_register! -( +make_register! { "copper-wall" => SimpleBlock::new(1, true, cost!(Copper: 6)); - "copper-wall-large" => SimpleBlock::new(2, true, cost!(Copper: 24)); + "copper-wall-large" => SimpleBlock::new(2, true, cost!(Copper: 6 * 4)); "titanium-wall" => SimpleBlock::new(1, true, cost!(Titanium: 6)); - "titanium-wall-large" => SimpleBlock::new(2, true, cost!(Titanium: 24)); + "titanium-wall-large" => SimpleBlock::new(2, true, cost!(Titanium: 6 * 4)); "plastanium-wall" => SimpleBlock::new(1, true, cost!(Metaglass: 2, Plastanium: 5)); - "plastanium-wall-large" => SimpleBlock::new(2, true, cost!(Metaglass: 8, Plastanium: 20)); + "plastanium-wall-large" => SimpleBlock::new(2, true, cost!(Metaglass: 2 * 4, Plastanium: 5 * 4)); "thorium-wall" => SimpleBlock::new(1, true, cost!(Thorium: 6)); - "thorium-wall-large" => SimpleBlock::new(2, true, cost!(Thorium: 24)); + "thorium-wall-large" => SimpleBlock::new(2, true, cost!(Thorium: 6 * 4)); "phase-wall" => SimpleBlock::new(1, true, cost!(PhaseFabric: 6)); - "phase-wall-large" => SimpleBlock::new(2, true, cost!(PhaseFabric: 24)); + "phase-wall-large" => SimpleBlock::new(2, true, cost!(PhaseFabric: 6 * 4)); "surge-wall" => SimpleBlock::new(1, true, cost!(SurgeAlloy: 6)); - "surge-wall-large" => SimpleBlock::new(2, true, cost!(SurgeAlloy: 24)); + "surge-wall-large" => SimpleBlock::new(2, true, cost!(SurgeAlloy: 6 * 4)); "door" => DoorBlock::new(1, true, cost!(Titanium: 6, Silicon: 4)); - "door-large" => DoorBlock::new(2, true, cost!(Titanium: 24, Silicon: 16)); + "door-large" => DoorBlock::new(2, true, cost!(Titanium: 6 * 4, Silicon: 4 * 4)); + "tungsten-wall" => SimpleBlock::new(1, true, cost!(Tungsten: 6)); + "large-tungsten-wall" => SimpleBlock::new(2, true, cost!(Tungsten: 6 * 4)); + "blast-door" => DoorBlock::new(2, true, cost!(Tungsten: 24, Silicon: 24)); + "reinforced-surge-wall" => SimpleBlock::new(1, true, cost!(SurgeAlloy: 6, Tungsten: 2)); + "reinforced-surge-wall-large" => SimpleBlock::new(2, true, cost!(SurgeAlloy: 6 * 4, Tungsten: 2 * 4)); + "carbide-wall" => SimpleBlock::new(1, true, cost!(Thorium: 6, Carbide: 6)); + "carbide-wall-large" => SimpleBlock::new(2, true, cost!(Thorium: 6 * 4, Carbide: 6 * 4)); + "shielded-wall" => SimpleBlock::new(2, true, cost!(PhaseFabric: 20, SurgeAlloy: 12, Beryllium: 12)); // sandbox only "scrap-wall" => SimpleBlock::new(1, true, cost!(Scrap: 6)); "scrap-wall-large" => SimpleBlock::new(2, true, cost!(Scrap: 24)); "scrap-wall-huge" => SimpleBlock::new(3, true, cost!(Scrap: 54)); "scrap-wall-gigantic" => SimpleBlock::new(4, true, cost!(Scrap: 96)); "thruster" => SimpleBlock::new(4, false, cost!(Scrap: 96)); -); +} pub struct DoorBlock { size: u8, diff --git a/src/block/extraction.rs b/src/block/extraction.rs index 237af77..ebea8cc 100644 --- a/src/block/extraction.rs +++ b/src/block/extraction.rs @@ -10,4 +10,10 @@ make_register! "water-extractor" => SimpleBlock::new(2, true, cost!(Copper: 30, Lead: 30, Metaglass: 30, Graphite: 30)); "cultivator" => SimpleBlock::new(2, true, cost!(Copper: 25, Lead: 25, Silicon: 10)); "oil-extractor" => SimpleBlock::new(3, true, cost!(Copper: 150, Lead: 115, Graphite: 175, Thorium: 115, Silicon: 75)); + "vent-condenser" => SimpleBlock::new(3, true, cost!(Graphite: 20, Beryllium: 60)); + "cliff-crusher" => SimpleBlock::new(2, false, cost!(Beryllium: 100, Graphite: 40)); + "plasma-bore" => SimpleBlock::new(2, false, cost!(Beryllium: 40)); + "large-plasma-bore" => SimpleBlock::new(3, false, cost!(Silicon: 100, Oxide: 25, Beryllium: 100, Tungsten: 70)); + "impact-drill" => SimpleBlock::new(4, true, cost!(Silicon: 70, Beryllium: 90, Graphite: 60)); + "eruption-drill" => SimpleBlock::new(5, true, cost!(Silicon: 200, Oxide: 20, Tungsten: 200, Thorium: 120)); ); diff --git a/src/block/factory.rs b/src/block/factory.rs index add4458..f7cfd13 100644 --- a/src/block/factory.rs +++ b/src/block/factory.rs @@ -21,6 +21,22 @@ make_register! "pulverizer" => SimpleBlock::new(1, true, cost!(Copper: 30, Lead: 25)); "coal-centrifuge" => SimpleBlock::new(2, true, cost!(Lead: 30, Graphite: 40, Titanium: 20)); "incinerator" => SimpleBlock::new(1, true, cost!(Lead: 15, Graphite: 5)); + "silicon-arc-furnace" => SimpleBlock::new(3, true, cost!(Beryllium: 70, Graphite: 80)); + "electrolyzer" => SimpleBlock::new(3, true, cost!(Silicon: 50, Graphite: 40, Beryllium: 130, Tungsten: 80)); + "atmospheric-condenser" => SimpleBlock::new(3, true, cost!(Oxide: 60, Beryllium: 180, Silicon: 150)); + "oxidation-chamber" => SimpleBlock::new(3, true, cost!(Tungsten: 120, Graphite: 80, Silicon: 100, Beryllium: 120)); + "electric-heater" => SimpleBlock::new(2, false, cost!(Tungsten: 30, Oxide: 30)); + "slag-heater" => SimpleBlock::new(3, false, cost!(Tungsten: 50, Oxide: 20, Beryllium: 20)); + "phase-heater" => SimpleBlock::new(2, false, cost!(Oxide: 30, Carbide: 30, Beryllium: 30)); + "heat-redirector" => SimpleBlock::new(3, false, cost!(Tungsten: 10, Graphite: 10)); + "heat-router" => SimpleBlock::new(3, false, cost!(Tungsten: 15, Graphite: 10)); + "slag-incinerator" => SimpleBlock::new(1, true, cost!(Tungsten: 15)); + "carbide-crucible" => SimpleBlock::new(3, true, cost!(Tungsten: 110, Thorium: 150, Oxide: 60)); + // slag centrifuge + "surge-crucible" => SimpleBlock::new(3, true, cost!(Silicon: 100, Graphite: 80, Tungsten: 80, Oxide: 80)); + "cyanogen-synthesizer" => SimpleBlock::new(3, true, cost!(Carbide: 50, Silicon: 80, Beryllium: 90)); + "phase-synthesizer" => SimpleBlock::new(3, true, cost!(Carbide: 90, Silicon: 100, Thorium: 100, Tungsten: 200)); + // heat reactor // sandbox only "heat-source" => SimpleBlock::new(1, false, &[]); ); diff --git a/src/block/fluid.rs b/src/block/fluid.rs index 90289bc..0454461 100644 --- a/src/block/fluid.rs +++ b/src/block/fluid.rs @@ -13,8 +13,8 @@ use crate::data::GridPos; use crate::fluid; use crate::item::storage::Storage; -make_register! -( +make_register! { + "reinforced-pump" => SimpleBlock::new(2, true, cost!(Beryllium: 40, Tungsten: 30, Silicon: 20)); "mechanical-pump" => SimpleBlock::new(1, true, cost!(Copper: 15, Metaglass: 10)); "rotary-pump" => SimpleBlock::new(2, true, cost!(Copper: 70, Metaglass: 50, Titanium: 35, Silicon: 20)); "impulse-pump" => SimpleBlock::new(3, true, cost!(Copper: 80, Metaglass: 90, Titanium: 40, Thorium: 35, Silicon: 30)); @@ -27,10 +27,16 @@ make_register! "liquid-junction" => SimpleBlock::new(1, true, cost!(Metaglass: 8, Graphite: 4)); "bridge-conduit" => BridgeBlock::new(1, true, cost!(Metaglass: 8, Graphite: 4), 4, true); "phase-conduit" => BridgeBlock::new(1, true, cost!(Metaglass: 20, Titanium: 10, Silicon: 7, PhaseFabric: 5), 12, true); + "reinforced-conduit" => SimpleBlock::new(1, false, cost!(Beryllium: 2)); + "reinforced-liquid-junction" => SimpleBlock::new(1, true, cost!(Graphite: 4, Beryllium: 8)); + "reinforced-bridge-conduit" => BridgeBlock::new(1, true, cost!(Graphite: 8, Beryllium: 20), 4, true); + "reinforced-liquid-router" => SimpleBlock::new(1, true, cost!(Graphite: 8, Beryllium: 4)); + "reinforced-liquid-container" => SimpleBlock::new(2, true, cost!(Tungsten: 10, Beryllium: 16)); + "reinforced-liquid-tank" => SimpleBlock::new(3, true, cost!(Tungsten: 40, Beryllium: 50)); // sandbox only "liquid-source" => FluidBlock::new(1, true, &[]); "liquid-void" => SimpleBlock::new(1, true, &[]); -); +} pub struct FluidBlock { size: u8, diff --git a/src/block/mod.rs b/src/block/mod.rs index 74e8c2c..2c2e597 100644 --- a/src/block/mod.rs +++ b/src/block/mod.rs @@ -168,6 +168,12 @@ pub struct Block { logic: BoxAccess<'static, dyn BlockLogic + Sync>, } +impl PartialEq for Block { + fn eq(&self, rhs: &Block) -> bool { + self.name == rhs.name + } +} + impl Block { #[must_use] pub const fn new( diff --git a/src/block/payload.rs b/src/block/payload.rs index 8339df3..39a6534 100644 --- a/src/block/payload.rs +++ b/src/block/payload.rs @@ -4,7 +4,8 @@ use std::fmt; use crate::block::simple::{cost, state_impl, BuildCost, SimpleBlock}; use crate::block::{ - self, impl_block, make_register, BlockLogic, DataConvertError, DeserializeError, SerializeError, + self, impl_block, make_register, transport::BridgeBlock, BlockLogic, DataConvertError, + DeserializeError, SerializeError, }; use crate::content; use crate::data::dynamic::{DynData, DynType}; @@ -28,8 +29,30 @@ make_register! { cost!(Lead: 4000, Thorium: 1000, Silicon: 3000, Plastanium: 600, PhaseFabric: 600, SurgeAlloy: 800)); "repair-point" => SimpleBlock::new(1, true, cost!(Copper: 30, Lead: 30, Silicon: 20)); "repair-turret" => SimpleBlock::new(2, true, cost!(Thorium: 80, Silicon: 90, Plastanium: 60)); + "tank-fabricator" => SimpleBlock::new(3, true, cost!(Silicon: 200, Beryllium: 150)); + "ship-fabricator" => SimpleBlock::new(3, true, cost!(Silicon: 250, Beryllium: 200)); + "mech-fabricator" => SimpleBlock::new(3, true, cost!(Silicon: 200, Graphite: 300, Tungsten: 60)); + "tank-refabricator" => SimpleBlock::new(3, true, cost!(Beryllium: 200, Tungsten: 80, Silicon: 100)); + "mech-refabricator" => SimpleBlock::new(3, true, cost!(Beryllium: 250, Tungsten: 120, Silicon: 150)); + "ship-refabricator" => SimpleBlock::new(3, true, cost!(Beryllium: 200, Tungsten: 100, Silicon: 150, Oxide: 40)); + "prime-refabricator" => SimpleBlock::new(5, true, cost!(Thorium: 250, Oxide: 200, Tungsten: 200, Silicon: 400)); + "tank-assembler" => SimpleBlock::new(5, true, cost!(Thorium: 500, Oxide: 150, Carbide: 80, Silicon: 500)); + "ship-assembler" => SimpleBlock::new(5, true, cost!(Carbide: 100, Oxide: 200, Tungsten: 500, Silicon: 800, Thorium: 400)); + "mech-assembler" => SimpleBlock::new(5, true, cost!(Carbide: 200, Thorium: 600, Oxide: 200, Tungsten: 500, Silicon: 900)); // smh collaris + "basic-assembler-module" => SimpleBlock::new(5, true, cost!(Carbide: 300, Thorium: 500, Oxide: 200, PhaseFabric: 400)); // the dummy block + // payload "payload-conveyor" => SimpleBlock::new(3, false, cost!(Copper: 10, Graphite: 10)); - "payload-router" => SimpleBlock::new(3, false, cost!(Copper: 10, Graphite: 15)); + "payload-router" => PayloadBlock::new(3, false, cost!(Copper: 10, Graphite: 15)); + "reinforced-payload-conveyor" => SimpleBlock::new(3, false, cost!(Tungsten: 10)); + "reinforced-payload-router" => SimpleBlock::new(3, false, cost!(Tungsten: 15)); + "payload-mass-driver" => BridgeBlock::new(3, true, cost!(Tungsten: 120, Silicon: 120, Graphite: 50), 700, false); + "large-payload-mass-driver" => BridgeBlock::new(5, true, cost!(Thorium: 200, Tungsten: 200, Silicon: 200, Graphite: 100, Oxide: 30), 1100, false); + "small-deconstructor" => SimpleBlock::new(3, true, cost!(Beryllium: 100, Silicon: 100, Oxide: 40, Graphite: 80)); + "deconstructor" => SimpleBlock::new(5, true, cost!(Beryllium: 250, Oxide: 100, Silicon: 250, Carbide: 250)); + "constructor" => PayloadBlock::new(3, true, cost!(Silicon: 100, Beryllium: 150, Tungsten: 80)); + "large-constructor" => PayloadBlock::new(3, true, cost!(Silicon: 150, Oxide: 150, Tungsten: 200, PhaseFabric: 40)); + "payload-loader" => SimpleBlock::new(3, false, cost!(Graphite: 50, Silicon: 50, Tungsten: 80)); + "payload-unloader" => SimpleBlock::new(3, false, cost!(Graphite: 50, Silicon: 50, Tungsten: 30)); // sandbox only "payload-source" => PayloadBlock::new(5, false, &[]); "payload-void" => SimpleBlock::new(5, true, &[]); diff --git a/src/block/power.rs b/src/block/power.rs index e21d342..6cc71d5 100644 --- a/src/block/power.rs +++ b/src/block/power.rs @@ -10,8 +10,7 @@ use crate::data::dynamic::{DynData, DynType}; use crate::data::GridPos; use crate::item::storage::Storage; -make_register! -( +make_register! { "power-node" => ConnectorBlock::new(1, true, cost!(Copper: 1, Lead: 3), 10); "power-node-large" => ConnectorBlock::new(2, true, cost!(Lead: 10, Titanium: 5, Silicon: 3), 15); "surge-tower" => ConnectorBlock::new(2, true, cost!(Lead: 10, Titanium: 7, Silicon: 15, SurgeAlloy: 15), 2); @@ -28,11 +27,19 @@ make_register! "thorium-reactor" => SimpleBlock::new(3, true, cost!(Lead: 300, Metaglass: 50, Graphite: 150, Thorium: 150, Silicon: 200)); "impact-reactor" => SimpleBlock::new(4, true, cost!(Lead: 500, Metaglass: 250, Graphite: 400, Thorium: 100, Silicon: 300, SurgeAlloy: 250)); + "beam-node" => ConnectorBlock::new(1, true, cost!(Beryllium: 8), 4); + "beam-tower" => ConnectorBlock::new(3, true, cost!(Beryllium: 30, Oxide: 10, Silicon: 10), 12); + "turbine-condenser" => SimpleBlock::new(3, true, cost!(Beryllium: 60)); + "chemical-combustion-chamber" => SimpleBlock::new(3, true, cost!(Graphite: 40, Tungsten: 40, Oxide: 40, Silicon: 30)); + "pyrolosis-generator" => SimpleBlock::new(3, true, cost!(Graphite: 50, Carbide: 50, Oxide: 60, Silicon: 50)); + "flux-reactor" => SimpleBlock::new(5, true, cost!(Graphite: 300, Carbide: 200, Oxide: 100, Silicon: 600, SurgeAlloy: 300)); + "neoplasia-reactor" => SimpleBlock::new(5, true, cost!(Tungsten: 1000, Carbide: 300, Oxide: 150, Silicon: 500, PhaseFabric: 300, SurgeAlloy: 200)); + // editor only + "beam-link" => ConnectorBlock::new(3, true, &[], 12); // sandbox only "power-source" => ConnectorBlock::new(1, true, &[], 100); "power-void" => SimpleBlock::new(1, true, &[]); -); - +} pub struct ConnectorBlock { size: u8, symmetric: bool, diff --git a/src/block/simple.rs b/src/block/simple.rs index e3d8db7..d57f091 100644 --- a/src/block/simple.rs +++ b/src/block/simple.rs @@ -80,7 +80,7 @@ impl BlockLogic for SimpleBlock { } macro_rules! cost { - ($($item:ident: $cnt:literal),+) => { + ($($item:ident: $cnt:expr),+) => { &[$((crate::item::Type::$item, $cnt)),*] }; } diff --git a/src/block/transport.rs b/src/block/transport.rs index 1125ec5..4494d86 100644 --- a/src/block/transport.rs +++ b/src/block/transport.rs @@ -27,6 +27,17 @@ make_register! { "overflow-gate" => SimpleBlock::new(1, true, cost!(Copper: 4, Lead: 2)); "underflow-gate" => SimpleBlock::new(1, true, cost!(Copper: 4, Lead: 2)); "mass-driver" => BridgeBlock::new(3, true, cost!(Lead: 125, Titanium: 125, Thorium: 50, Silicon: 75), 55, false); + "duct" => SimpleBlock::new(1, false, cost!(Beryllium: 1)); + "armored-duct" => SimpleBlock::new(1, false, cost!(Beryllium: 2, Tungsten: 1)); + "duct-router" => ItemBlock::new(1, true, cost!(Beryllium: 10)); + "overflow-duct" => SimpleBlock::new(1, true, cost!(Graphite: 8, Beryllium: 8)); + "underflow-duct" => SimpleBlock::new(1, true, cost!(Graphite: 8, Beryllium: 8)); + "duct-bridge" => BridgeBlock::new(1, true, cost!(Beryllium: 20), 3, true); + "duct-unloader" => ItemBlock::new(1, true, cost!(Graphite: 20, Silicon: 20, Tungsten: 10)); + "surge-conveyor" => SimpleBlock::new(1, false, cost!(SurgeAlloy: 1, Tungsten: 1)); + "surge-router" => SimpleBlock::new(1, false, cost!(SurgeAlloy: 5, Tungsten: 1)); // not symmetric + "unit-cargo-loader" => SimpleBlock::new(3, true, cost!(Silicon: 80, SurgeAlloy: 50, Oxide: 20)); + "unit-cargo-unload-point" => ItemBlock::new(2, true, cost!(Silicon: 60, Tungsten: 60)); // sandbox only "item-source" => ItemBlock::new(1, true, &[]); "item-void" => SimpleBlock::new(1, true, &[]); diff --git a/src/block/turret.rs b/src/block/turret.rs index 6c91ce2..2280ac9 100644 --- a/src/block/turret.rs +++ b/src/block/turret.rs @@ -1,8 +1,7 @@ use crate::block::make_register; use crate::block::simple::{cost, SimpleBlock}; -make_register! -( +make_register! { "duo" => SimpleBlock::new(1, true, cost!(Copper: 35)); "scatter" => SimpleBlock::new(2, true, cost!(Copper: 85, Lead: 45)); "scorch" => SimpleBlock::new(1, true, cost!(Copper: 25, Graphite: 22)); @@ -21,4 +20,13 @@ make_register! "foreshadow" => SimpleBlock::new(4, true, cost!(Copper: 1000, Metaglass: 600, Silicon: 600, Plastanium: 200, SurgeAlloy: 300)); "spectre" => SimpleBlock::new(4, true, cost!(Copper: 900, Graphite: 300, Thorium: 250, Plastanium: 175, SurgeAlloy: 250)); "meltdown" => SimpleBlock::new(4, true, cost!(Copper: 1200, Lead: 350, Graphite: 300, Silicon: 325, SurgeAlloy: 325)); -); + "breach" => SimpleBlock::new(3, true, cost!(Beryllium: 150, Silicon: 150, Graphite: 250)); + "diffuse" => SimpleBlock::new(3, true, cost!(Beryllium: 150, Silicon: 200, Graphite: 200, Tungsten: 50)); + "sublimate" => SimpleBlock::new(3, true, cost!(Tungsten: 150, Silicon: 200, Oxide: 40, Beryllium: 400)); + "titan" => SimpleBlock::new(4, true, cost!(Tungsten: 250, Silicon: 300, Thorium: 400)); + "disperse" => SimpleBlock::new(4, true, cost!(Thorium: 50, Oxide: 150, Silicon: 200, Beryllium: 350)); + "afflict" => SimpleBlock::new(4, true, cost!(SurgeAlloy: 100, Silicon: 200, Graphite: 250, Oxide: 40)); + "lustre" => SimpleBlock::new(4, true, cost!(Silicon: 250, Graphite: 200, Oxide: 50, Carbide: 90)); + "scathe" => SimpleBlock::new(5, true, cost!(Oxide: 200, SurgeAlloy: 400, Silicon: 800, Carbide: 500, PhaseFabric: 300)); + "malign" => SimpleBlock::new(5, true, cost!(Carbide: 400, Beryllium: 2000, Silicon: 800, Graphite: 800, PhaseFabric: 300)); +} diff --git a/src/data/base64.rs b/src/data/base64.rs index 1a39c0b..303e9e0 100644 --- a/src/data/base64.rs +++ b/src/data/base64.rs @@ -1,338 +1,12 @@ -use std::error::Error; -use std::fmt; +use base64::{engine::general_purpose, engine::Engine as _}; +pub use base64::{DecodeSliceError as DecodeError, EncodeSliceError as EncodeError}; -const CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -const PADDING: u8 = b'='; - -fn decode_char(val: u8) -> Option<usize> { - match val { - b'A'..=b'Z' => Some((val - b'A') as usize), - b'a'..=b'z' => Some(26 + (val - b'a') as usize), - b'0'..=b'9' => Some(52 + (val - b'0') as usize), - b'+' => Some(0x3E), - b'/' => Some(0x3F), - _ => None, - } -} +const BASE64: general_purpose::GeneralPurpose = general_purpose::STANDARD; pub fn encode(input: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> { - let use_pad = input.len() % 3 != 0; - let expect_len = if use_pad { - 4 * (input.len() / 3 + 1) - } else { - 4 * (input.len() / 3) - }; - if output.len() < expect_len { - return Err(EncodeError::Overflow { - need: expect_len, - have: output.len(), - }); - } - let mut in_pos = 0usize; - let mut out_pos = 0usize; - while input.len() - in_pos >= 3 { - let buff = ((input[in_pos] as usize) << 16) - | ((input[in_pos + 1] as usize) << 8) - | (input[in_pos + 2] as usize); - output[out_pos] = CHARS[buff >> 18]; - output[out_pos + 1] = CHARS[(buff >> 12) & 0x3F]; - output[out_pos + 2] = CHARS[(buff >> 6) & 0x3F]; - output[out_pos + 3] = CHARS[buff & 0x3F]; - out_pos += 4; - in_pos += 3; - } - let remain = input.len() - in_pos; - if remain > 0 { - let mut buff = (input[in_pos] as usize) << 16; - if remain > 1 { - buff |= (input[in_pos + 1] as usize) << 8; - } - in_pos += if remain > 1 { 2 } else { 1 }; - output[out_pos] = CHARS[buff >> 18]; - output[out_pos + 1] = CHARS[(buff >> 12) & 0x3F]; - if remain > 1 { - output[out_pos + 2] = CHARS[(buff >> 6) & 0x3F]; - } else { - output[out_pos + 2] = PADDING; - } - output[out_pos + 3] = PADDING; - out_pos += 4; - } - assert_eq!( - in_pos, - input.len(), - "missed input ({in_pos}, expected {})", - input.len() - ); - assert_eq!( - out_pos, expect_len, - "missed output ({out_pos}, expected {expect_len})" - ); - Ok(out_pos) -} - -macro_rules! do_decode { - ($input:ident, $in_pos:expr) => { - match decode_char($input[$in_pos]) { - None => { - return Err(DecodeError::Malformed { - at: $in_pos, - value: $input[$in_pos], - }) - } - Some(v) => v, - } - }; + BASE64.encode_slice(input, output) } pub fn decode(input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> { - if input.len() % 4 != 0 { - // can't decode, but check for malformed data first - let mut in_pad = false; - for (i, &c) in input.iter().enumerate() { - if c == PADDING { - if i % 4 < 2 { - return Err(DecodeError::Malformed { at: i, value: c }); - } - in_pad = true; - } else if in_pad && i % 4 == 0 { - return Err(DecodeError::TrailingData { at: i }); - } else if (in_pad && i % 4 == 3) || decode_char(c).is_none() { - return Err(DecodeError::Malformed { at: i, value: c }); - } - } - return Err(DecodeError::Truncated); - } - let pad_len = if input.is_empty() || input[input.len() - 1] != PADDING { - 0 - } else if input[input.len() - 2] != PADDING { - 1 - } else { - 2 - }; - let expect_len = input.len() / 4 * 3 - pad_len; - if output.len() < expect_len { - return Err(DecodeError::Overflow { - need: expect_len, - have: output.len(), - }); - } - if input.is_empty() { - Ok(0) - } else { - let mut in_pos = 0usize; - let mut out_pos = 0usize; - while in_pos < input.len() - 4 { - let c0 = do_decode!(input, in_pos); - let c1 = do_decode!(input, in_pos + 1); - let c2 = do_decode!(input, in_pos + 2); - let c3 = do_decode!(input, in_pos + 3); - let buff = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3; - output[out_pos] = (buff >> 16) as u8; - output[out_pos + 1] = (buff >> 8) as u8; - output[out_pos + 2] = buff as u8; - in_pos += 4; - out_pos += 3; - } - - let c0 = do_decode!(input, in_pos); - let c1 = do_decode!(input, in_pos + 1); - let mut buff = (c0 << 18) | (c1 << 12); - output[out_pos] = (buff >> 16) as u8; - out_pos += 1; - if input[in_pos + 2] == PADDING { - if input[in_pos + 3] != PADDING { - return Err(DecodeError::Malformed { - at: in_pos + 3, - value: input[in_pos + 3], - }); - } - } else { - buff |= do_decode!(input, in_pos + 2) << 6; - output[out_pos] = (buff >> 8) as u8; - out_pos += 1; - if input[in_pos + 3] != PADDING { - buff |= do_decode!(input, in_pos + 3); - output[out_pos] = buff as u8; - out_pos += 1; - } - } - in_pos += 4; - - assert_eq!( - in_pos, - input.len(), - "missed input ({in_pos}, expected {})", - input.len() - ); - assert_eq!( - out_pos, expect_len, - "missed output ({out_pos}, expected {expect_len})" - ); - Ok(out_pos) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum DecodeError { - Malformed { at: usize, value: u8 }, - Overflow { need: usize, have: usize }, - Truncated, - TrailingData { at: usize }, -} - -impl fmt::Display for DecodeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Malformed { at, value } => { - write!(f, "malformed base64 character {value:?} (at {at})") - } - Self::Overflow { need, have } => { - write!(f, "decoder overflow (need {need}, but only have {have})") - } - Self::Truncated => f.write_str("truncated base64 input stream"), - Self::TrailingData { at } => write!(f, "trailing data in base64 stream (at {at})"), - } - } -} - -impl Error for DecodeError {} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum EncodeError { - Overflow { need: usize, have: usize }, -} - -impl fmt::Display for EncodeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Overflow { need, have } => { - write!(f, "encoder overflow (need {need}, but only have {have})") - } - } - } -} - -impl Error for EncodeError {} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn validate_chars() { - for (i, &c0) in CHARS.iter().enumerate() { - assert_ne!(c0, PADDING, "padding character in data charset at {i}"); - if i > 0 { - for (j, &c1) in CHARS[..i].iter().enumerate() { - assert_ne!(c1, c0, "duplicate data character at {j} and {i}"); - } - } - } - } - - #[test] - fn decode_matches_chars() { - for (i, &c0) in CHARS.iter().enumerate() { - assert_eq!( - decode_char(c0), - Some(i), - "data character {c0} (at {i}) isn't decoded properly" - ); - } - } - - macro_rules! test_codec { - ($func:ident, $input:expr => $expect:expr) => {{ - const EMPTY: u8 = 0xC9u8; // arbitrary - let mut output = [EMPTY; 64]; - let input = $input; - let expect = $expect; - assert_eq!($func(input, &mut output), Ok(expect.len())); - assert_eq!(&output[..expect.len()], expect); - assert!( - output[expect.len()..].iter().all(|&x| x == EMPTY), - "output buffer overflow" - ); - }}; - } - - #[test] - fn encoder_success() { - test_codec!(encode, b"Hello Wor" => b"SGVsbG8gV29y"); - test_codec!(encode, b"Hello Worl" => b"SGVsbG8gV29ybA=="); - test_codec!(encode, b"Hello World" => b"SGVsbG8gV29ybGQ="); - test_codec!(encode, b"Hello World!" => b"SGVsbG8gV29ybGQh"); - } - - #[test] - fn decoder_success() { - test_codec!(decode, b"SGVsbG8gV29y" => b"Hello Wor"); - test_codec!(decode, b"SGVsbG8gV29ybA==" => b"Hello Worl"); - test_codec!(decode, b"SGVsbG8gV29ybGQ=" => b"Hello World"); - test_codec!(decode, b"SGVsbG8gV29ybGQh" => b"Hello World!"); - } - - #[test] - fn encoder_fail() { - let mut output = [0u8; 64]; - assert_eq!( - encode(b"Hello Worl", &mut output[..15]), - Err(EncodeError::Overflow { need: 16, have: 15 }) - ); - assert_eq!( - encode(b"Hello World!", &mut output[..0]), - Err(EncodeError::Overflow { need: 16, have: 0 }) - ); - } - - #[test] - fn decoder_fail() { - let mut output = [0u8; 64]; - assert_eq!( - decode(b"SGVsbG8gV29ybA==", &mut output[..9]), - Err(DecodeError::Overflow { need: 10, have: 9 }) - ); - assert_eq!( - decode(b"SGVsbG8gV29ybGQh", &mut output[..11]), - Err(DecodeError::Overflow { need: 12, have: 11 }) - ); - assert_eq!( - decode(b"SGVsbG8gV29ybA", &mut output), - Err(DecodeError::Truncated) - ); - assert_eq!( - decode(b"SGVsbG8gV29yb", &mut output), - Err(DecodeError::Truncated) - ); - assert_eq!( - decode(b"SGVsbG8gV29y\n", &mut output), - Err(DecodeError::Malformed { - at: 12, - value: b'\n' - }) - ); - assert_eq!( - decode(b"SGVs_bG8gV29y\n", &mut output), - Err(DecodeError::Malformed { at: 4, value: b'_' }) - ); - assert_eq!( - decode(b"SGVsbG8gV29ybA==*", &mut output), - Err(DecodeError::TrailingData { at: 16 }) - ); - assert_eq!( - decode(b"SGVsbG8gV29ybA=*", &mut output), - Err(DecodeError::Malformed { - at: 15, - value: b'*' - }) - ); - assert_eq!( - decode(b"SGVsbG8gV29ybA=A", &mut output), - Err(DecodeError::Malformed { - at: 15, - value: b'A' - }) - ); - } + BASE64.decode_slice(input, output) } diff --git a/src/data/schematic.rs b/src/data/schematic.rs index 9927ad2..92f8f72 100644 --- a/src/data/schematic.rs +++ b/src/data/schematic.rs @@ -28,17 +28,13 @@ pub struct Placement<'l> { rot: Rotation, } -impl<'l> Placement<'l> { - #[must_use] - pub fn get_pos(&self) -> GridPos { - self.pos - } - - #[must_use] - pub fn get_block(&self) -> &'l Block { - self.block +impl PartialEq for Placement<'_> { + fn eq(&self, rhs: &Placement<'_>) -> bool { + self.pos == rhs.pos && self.block == rhs.block && self.rot == rhs.rot } +} +impl<'l> Placement<'l> { #[must_use] pub fn get_state(&self) -> Option<&dyn Any> { match self.state { @@ -62,11 +58,6 @@ impl<'l> Placement<'l> { Ok(std::mem::replace(&mut self.state, state)) } - #[must_use] - pub fn get_rotation(&self) -> Rotation { - self.rot - } - pub fn set_rotation(&mut self, rot: Rotation) -> Rotation { std::mem::replace(&mut self.rot, rot) } @@ -89,13 +80,22 @@ impl<'l> Clone for Placement<'l> { #[derive(Clone)] pub struct Schematic<'l> { - width: u16, - height: u16, - tags: HashMap<String, String>, - blocks: Vec<Placement<'l>>, + pub width: u16, + pub height: u16, + pub tags: HashMap<String, String>, + pub blocks: Vec<Placement<'l>>, lookup: Vec<Option<usize>>, } +impl<'l> PartialEq for Schematic<'l> { + fn eq(&self, rhs: &Schematic<'l>) -> bool { + self.width == rhs.width + && self.height == rhs.height + && self.blocks == rhs.blocks + && self.tags == rhs.tags + } +} + impl<'l> Schematic<'l> { #[must_use] pub fn new(width: u16, height: u16) -> Self { @@ -127,25 +127,6 @@ impl<'l> Schematic<'l> { } #[must_use] - pub fn get_width(&self) -> u16 { - self.width - } - - #[must_use] - pub fn get_height(&self) -> u16 { - self.height - } - - #[must_use] - pub fn get_tags(&self) -> &HashMap<String, String> { - &self.tags - } - - pub fn get_tags_mut(&mut self) -> &mut HashMap<String, String> { - &mut self.tags - } - - #[must_use] pub fn is_empty(&self) -> bool { self.blocks.is_empty() } @@ -754,6 +735,12 @@ impl fmt::Display for ResizeError { impl Error for ResizeError {} +impl fmt::Debug for Schematic<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + impl<'l> fmt::Display for Schematic<'l> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { /* @@ -1407,30 +1394,51 @@ impl FusedIterator for PosIter {} mod test { use super::*; - macro_rules!test_iter - { - ($name:ident, $it:expr, $($val:expr),+) => - { + macro_rules! test_iter { + ($name:ident, $it:expr, $($val:expr),+) => { #[test] - fn $name() - { + fn $name() { let mut it = $it; $(test_iter!(impl it, $val);)+ } }; - (impl $it:ident, $val:literal) => - { - for _ in 0..$val - { + (impl $it:ident, $val:literal) => { + for _ in 0..$val { assert_ne!($it.next(), None, "iterator returned None too early"); } }; - (impl $it:ident, $val:expr) => - { + (impl $it:ident, $val:expr) => { assert_eq!($it.next(), $val); }; } + macro_rules! test_schem { + ($name:ident, $($val:expr),+) => { + #[test] + fn $name() { + let reg = crate::block::build_registry(); + let mut ser = SchematicSerializer(®); + $( + let parsed = ser.deserialize_base64($val).unwrap(); + println!("{}", parsed.tags.get("name").unwrap()); + let unparsed = ser.serialize_base64(&parsed).unwrap(); + let parsed2 = ser.deserialize_base64(&unparsed).unwrap(); + assert_eq!(parsed, parsed2); + )* + } + }; + } + + test_schem! { + ser_de, + "bXNjaAF4nCVNy07DMBCcvC1c4MBnoNz4G8TBSSxRycSRbVr646iHlmUc2/KOZ3dmFo9QDdrVfFkMb9Gsi5mgFxvncNzS0a8Aemcm6yLq948Bz2eTbBjtTwpmTj7gafs00Y6zX0/2Qt6dzLdLeNj8mbrVLxZ6ciamcQlH59BHH5iAYTKJeOGCF6AisFSoBxF55V+hJm1Lvwca8lpVIuzlS0eGLoMqTGUG6OLRJes3Mw40E5ijc2QedkPuU3DfLX0eHriDsgMapaScu9zkT26o5Uq8EmV/zS5vi4tr/wHvJE7M", + "bXNjaAF4nE2MzWrEMAyEJ7bjdOnPobDQvfUF8kSlhyTWFlOv3VWcQvv0lRwoawzSjL4ZHOAtXJ4uhEdi+oz8ek5bDCvuA60Lx68aSwbg0zRTWmHe3j2emWI+F14ojEvJYYsVD5RoqVzSzy8xDjNNlzGXQHi5gVO8SvnIZasCnW4uM8fwQf9tT9+Ua1OUV0GBI9ozHToY6IeDtaIACxkOnaoe1rVrg2RV1cP0CuycLA5+LxuUU+U055W0Yrb4sEcGNQ3u1NTh9iHmH6qaOTI=", + "bXNjaAF4nE2R226kQAxEzW1oYC5kopV23/IDfMw+R3ng0klaYehsD6w2+fqtamuiDILCLtvH9MgPaTLJl/5ipR3cy4MN9s2FB//PTVaayV7H4N5X5xcR2c39YOerpI9Pe/kVrFuefRjt1A3BTS+2G/0ybW6V41+7rDGyy9UGjNnGtQt+W78C7ZCcgVSD7S/d4kH8+W3q7P5sbrr1nb85N9DeznZcg58/PlFxx6V77tqNr/1lQOr0anuQ7eQCCn2QQ6Rvy+z7Cb7Ib9xSSJpICsGDV5bxoVJKxpSRLIdUKrVkBQoSkVxYJDuWq5SaNByboUEYJ5LgmFlZRhdejit6oDO5Uw/trDTqgWfgpCqFiiG91MVL7IJfLKck3NooyBDEZM4Gw+9jtJOEXgQZ7TQAJZSaM+POFd5TSWpIoVHEVsqrlUcX8xq+U2pi94wyCHZpICn625jAGdVy4DxGpdom2gXeKu2OIw+6w5E7UABnMgKO9CgxOukiHBGjmGz1dFp+VQO57cA7pUR4+wVvFd5q9x2aQT0r/Ew4k/FfPyvunjhGaPgPoVJdLw==", + "bXNjaAF4nGNgZmBmZmDJS8xNZeBOyslPzlYAkwzcKanFyUWZBSWZ+XkMDAxsOYlJqTnFDEzRsYwMfAWJlTn5iSm6RfmlJalFQGlGEGJkZWSYxQAAcBkUPA==" + // "bXNjaAF4nD1TDUxTVxS+r6+vr30triCSVjLXiulKoAjMrJRkU8b4qSgLUAlIZ1rah7yt9L31h1LMMCNbRAQhYrKwOnEslhCcdmzJuohL1DjZT4FJtiVsoG5LFtzmGgGngHm790mam7x77ne+c945934HKIAcB2K3vYUGScXmWvM+TQ3jYhysG8idtNfhYTgfAw8ASFz2RtrlBaKG12VA6X1KMjg8fgfT6KLBJi7osfsYH21oYdpoD6A4NkB7DG7WSQOJl/X4IPYM426loeU0bABSv9vF2p3I1cI4PKyB87AO2gu9gGi1+10+kMTCiCYXGzActvtoWEY+ABhcIgzaOBCJ4EZICYDx6xAV86vCdx2IAS5QJJAEIRkQ4XAjAHSIIITBUCCGRwIuESCgheEIkwgYIpEAF4I3wSw9bWccTpvNmVkZy5raWT1p3r+vajJ2odyQb+HAW9HxvV556vfvpNy4oVUfDyq36Kyqe73xsdemprMyv52uAreYwcXzJaPU+aDp8fFM24nuzUvVqYo9yr7CjFT/aDDzUUz8S8W7g+X3VCpVnargblNubl4kI1q6J+cFPH2HS6VSF5xzZWhCyYCKO2FjqAEprB9WRsJbwNFFoLKhITRCQheBbByQCMAQQwow1I8M9oPJ2870npqvvq5RvvfFyYE3hjrLmst3TixrV0XSN08Uax/UrMSeHdmKDdj8uhh3Pef2Wa+qDljrj82pK+aM300sl0eTrC/rL3zzZKZhRWFMq+mLvvTZb0bbweGZL/85ywwnl4RLzR9MBdIGy0LJowOWHxoOY2EiaJ/7s7ZP0Tg2wjWb3y6Lm3IPRNonw/0yT/+lZsdFy/LmUEp2RojHl68B41zDx43WJ/qANkwdVOvGtxjzpgo/keUURn2XK6zerz9Km10w3Vb8Ww/t/UdmHyx7fXwEcPiP0w1Xx9f+/m/X/d13Wiees8yPnk69ePlS9Yuf9sQf1dvVB27mm68U+51Fj7emzS+mzw1jzwuvTKFXHoK30l9EXctVlozIiSPdpk5djrW965BmV1XW4qsp8kNXmtWztdklXXTa0u6lO0d1+GS3TV/Q95O+17+S23Hs5sIfP4e/uqvd9oo+p7u0cYiPb4+9f/L+Qn3PmuXDdDai/ev0ts69I9nuNTOXp9HfOmoy/a5Y9D2cYYsebq+cKgB1V9vXdYFfOz7vWiVCLNnUUVkLOGO9umVN0jl2KoIjYSINEzgUORoDBKAnJwSLTLikQOBSAoC0ABBAbMgDWYIuBBeFRE7CbBCXCAwxFBAJPRgCSAFADBlykokcZCKHFAkPbSRKRaFUUsRGUyZLTJksMWWyjSlDJKhfFALZmFAJdFPo1+gkQVKXw/EW8/zToeZ5fh0t/H+V6k8+" + // "bXNjaAF4nGNgZ2BjZmDJS8xNZRByrkzOyc9LdctJLEpVT1F4v3AaA3dKanFyUWZBSWZ+HgMDA1tOYlJqTjEDU3QsFwN/eWJJapFuakVJUWJySX4RA3tSYglQpJKBPRliEgNXQX45UElefkoqA39SUWZKeqpucn5eWWolSDmQlVKaWcIgkpyfm1RaDLJDNz01L7UoEWQaf3JRZX5aTmlmim5uZkUqUCA3M7koX7egKD85tbgYqIIlIzEzB+gqQQYwYGYEEkwgxMjAAuQCKSYOZgam//8ZWP7/+/+HgZGZBSTPDlEGVMEKVssAooAMNqAWBpA0SCdQKTMDMzvEZAZGRhCXBZXLyv7///8cIC4AKgZaCOKGAHEh0DBWBpAKIAW0hQNkAR9Q16+KOXtDbhfNNhBQneu5XNV+o/0FSYFCtbrHC+dm3v/MnK3TnKUYGCv0+X3uygksNwzr3jbr755T/O3NuiOxk+7YX7lSoNb3oW3CUq7vKxq4bn1rUKqJsqldfsLX2KkoICQ679L8bW8fCLaY3K+LfGLIe6hoXlaW3iMvrsUq7Hc9Mq1W/OrydlRf+VK9+Ov1iSmsK1deCPKVPF69dG+I5RQ3qSf2PLmlH2bkLwgT4Q3V5+3qnBDPqcG1dNuqZfETim+6G0UqT9b5bGsUznqqb7GZxoxc5eUMT/JvvC79UdlruPvxJis9XhbeTZXLN+68WFB41+DkNRv7uZEGOr/2rvPPu8ZfyXRLI+zoUnmLXRu3+nz0EnP1Omq39TLX3c23cleZ8F62FSnMVCviO2H6tWXB7z2LpA02vleTOXiJK20BT+ADsencfQd0tlqrfQuoWut5dHaX1OP/KwIY5r66Zp4T9+2p241L0XvPfu5w/Zu3bNX77V89kt42zOLf9jJ9vk+msV1vy/Knlywv7Lh4NEY7fvHay0t3Sxo+2q918+je/O23P+50/qEWe45XqGzaR1vh0h1idRwBYZter2DKPJv559VDhbXSHzgin2x8PeXIKsvmtRIVB5V5b/1zcBP+f7VpfuP1OLcJKiePP7n8paroYrul0uF88dp5619+MN8Z7WT0p7DTUqftYOt3XqN51hGf+IVDH0UwcDKwAZMFMwCWiVNe" + } + test_iter!( block_iter, Schematic::new(3, 4).pos_iter(), diff --git a/src/exe/edit.rs b/src/exe/edit.rs deleted file mode 100644 index 80a1506..0000000 --- a/src/exe/edit.rs +++ /dev/null @@ -1,1488 +0,0 @@ -use std::borrow::Cow; -use std::env::Args; -use std::fs; -use std::io::{self, Write}; - -use plandustry::block::{build_registry, BlockRegistry, Rotation}; -use plandustry::data::dynamic::DynData; -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; - -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::default(); - 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::default(); - 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 s.parse::<$type>() { - 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}"); - } - } - 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::default(); - 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}"); - } - } - 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:?}"), - } -} diff --git a/src/exe/mod.rs b/src/exe/mod.rs index 801e122..fcfae12 100644 --- a/src/exe/mod.rs +++ b/src/exe/mod.rs @@ -1,5 +1,4 @@ pub mod args; -pub mod edit; pub mod print; macro_rules!print_err @@ -32,7 +31,6 @@ fn main() { args.next().unwrap(); // path to executable match args.next() { None => eprintln!("Not enough arguments, valid commands are: edit, print"), - Some(s) if s == "edit" => edit::main(args, 1), Some(s) if s == "print" => print::main(args, 1), Some(s) => eprintln!("Unknown argument {s}, valid commands are: edit, print"), } diff --git a/src/exe/print.rs b/src/exe/print.rs index d1b79c5..525bf9a 100644 --- a/src/exe/print.rs +++ b/src/exe/print.rs @@ -156,17 +156,17 @@ pub fn main(mut args: Args, arg_off: usize) { } pub fn print_schematic(s: &Schematic) { - if let Some(name) = s.get_tags().get("name") { + if let Some(name) = s.tags.get("name") { if !name.is_empty() { println!("Name: {name}"); } } - if let Some(desc) = s.get_tags().get("description") { + if let Some(desc) = s.tags.get("description") { if !desc.is_empty() { println!("Desc: {desc}"); } } - if let Some(labels) = s.get_tags().get("labels") { + if let Some(labels) = s.tags.get("labels") { if !labels.is_empty() && labels != "[]" { println!("Tags: {:?}", labels); } diff --git a/src/item/mod.rs b/src/item/mod.rs index 4aae21e..a08c71a 100644 --- a/src/item/mod.rs +++ b/src/item/mod.rs @@ -5,8 +5,7 @@ use crate::content::content_enum; pub mod storage; content_enum! { - pub enum Type / Item for u16 | TryFromU16Error - { + pub enum Type / Item for u16 | TryFromU16Error { "copper", "lead", "metaglass", |