minetest_pnr/src/canvas.rs

516 lines
18 KiB
Rust

// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::channel_router::{ChannelOp, ChannelSubState, WireConnection};
use crate::CircuitType;
use byteorder::{BigEndian, WriteBytesExt};
use deflate::write::ZlibEncoder;
use deflate::Compression;
use std::fs::File;
use std::io::{BufWriter, Write};
const BLOCK_IDS: &[&str; 16] = &[
"air",
"stone",
"mesecons_lamp:lamp_off",
"mesecons_walllever:wall_lever_off",
// Gates
"mesecons_gates:and_off",
"mesecons_gates:nand_off",
"mesecons_gates:nor_off",
"mesecons_gates:not_off",
"mesecons_gates:or_off",
"mesecons_gates:xor_off",
// Regular wire
"mesecons:mesecon_off",
// Insulated wires
"mesecons_insulated:insulated_off",
"mesecons_extrawires:corner_off",
"mesecons_extrawires:tjunction_off",
"mesecons_extrawires:crossover_off",
// For constant inputs
"mesecons_torch:mesecon_torch_off",
];
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum CornerOrientation {
LeftUp,
LeftDown,
DownRight,
UpRight,
}
impl CornerOrientation {
pub fn get_param2(self) -> u8 {
use self::CornerOrientation::*;
match self {
LeftUp => 0,
LeftDown => 3,
DownRight => 2,
UpRight => 1,
}
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum TRotation {
LeftRightDown,
LeftRightUp,
RightUpDown,
LeftUpDown,
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum BlockType {
Air,
WireH,
WireV,
WireCrossing,
WireT(TRotation),
WireCorner(CornerOrientation),
WireStar,
Gate(CircuitType),
Input,
Output,
Constant,
}
impl Default for BlockType {
fn default() -> Self {
BlockType::Air
}
}
impl BlockType {
pub fn c(self) -> char {
use self::BlockType::*;
match self {
Air => ' ',
WireH => '─',
WireV => '│',
WireCrossing => '╂',
WireCorner(CornerOrientation::DownRight) => '┌',
WireCorner(CornerOrientation::LeftUp) => '┘',
WireCorner(CornerOrientation::LeftDown) => '┐',
WireCorner(CornerOrientation::UpRight) => '└',
WireT(TRotation::LeftRightDown) => '┬',
WireT(TRotation::LeftRightUp) => '┴',
WireT(TRotation::RightUpDown) => '├',
WireT(TRotation::LeftUpDown) => '┤',
WireStar => '┼',
Gate(CircuitType::INPUT) => '░',
Gate(CircuitType::FORWARD) => '»',
Gate(CircuitType::NOT) => '¬',
Gate(CircuitType::OR) => 'v',
Gate(CircuitType::AND) => '^',
Gate(_) => '▓',
Input => '─',
Output => '─',
Constant => 'o',
}
}
pub fn minetest_type(self) -> &'static str {
use self::BlockType::*;
match self {
Air => "air",
WireH => "mesecons_insulated:insulated_off",
WireV => "mesecons_insulated:insulated_off",
WireCrossing => "mesecons_extrawires:crossover_off",
WireCorner(_) => "mesecons_extrawires:corner_off",
WireT(_) => "mesecons_extrawires:tjunction_off",
WireStar => "mesecons:mesecon_off",
Gate(gate) => gate.mesecon_id(),
Input => "mesecons_insulated:insulated_off",
Output => "mesecons_insulated:insulated_off",
Constant => "mesecons_torch:mesecon_torch_off",
}
}
pub fn get_param2(self) -> u8 {
use self::BlockType::*;
match self {
WireV => 0,
WireH => 3,
WireCorner(co) => co.get_param2(),
WireT(TRotation::LeftUpDown) => 0,
WireT(TRotation::LeftRightUp) => 1,
WireT(TRotation::RightUpDown) => 2,
WireT(TRotation::LeftRightDown) => 3,
// gate 'input / output' pseudo types (are actually wires)
Input => 3,
Output => 3,
Gate(CircuitType::OUTPUT) => 0,
Gate(CircuitType::INPUT) => 0,
Gate(_) => 3,
// Can be rotated in any way.
Air => 0,
WireCrossing => 0,
WireStar => 0,
Constant => 0,
}
}
}
// Note that they do not need to be bigger than 64 * 1024 as that's a limitation
// of minetest.
const CANVAS_MAX_W: usize = 8 * 1024;
const CANVAS_MAX_H: usize = 8 * 1024;
pub struct Canvas {
data: Box<[[BlockType; CANVAS_MAX_H]]>,
width: usize,
height: usize,
}
impl Canvas {
pub fn new() -> Self {
Self {
data: vec![[BlockType::Air; CANVAS_MAX_H]; CANVAS_MAX_W].into_boxed_slice(),
height: 0,
width: 0,
}
}
pub fn set(&mut self, x: usize, y: usize, c: BlockType) {
if x >= std::u16::MAX as _ || y >= std::u16::MAX as _ {
panic!("Sorry, circuit too large for minetest (MTS format limitation)");
}
if x >= CANVAS_MAX_W || y >= CANVAS_MAX_H {
panic!("Sorry, circuit too large for the internal canvas, consider increasing CANVAS_MAX_{{W/H}}");
}
if x > self.width {
self.width = x;
}
if y > self.height {
self.height = y;
}
self.data[x][y] = c;
}
pub fn get(&self, x: usize, y: usize) -> BlockType {
if x >= std::u16::MAX as _ || y >= std::u16::MAX as _ {
panic!("Sorry, circuit too large for minetest (MTS format limitation)");
}
if x >= CANVAS_MAX_W || y >= CANVAS_MAX_H {
panic!("Sorry, circuit too large for the internal canvas, consider increasing CANVAS_MAX_{{W/H}}");
}
self.data[x][y]
}
pub fn dimensions(&self) -> (usize, usize) {
(self.width + 1, self.height + 1)
}
pub fn draw(&self) {
let d = self.dimensions();
let stdout = std::io::stdout();
let mut lock = stdout.lock();
for line in 0..d.1 {
for c in 0..d.0 {
let c = &self.get(c, line).c();
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf);
let buf = &buf[0..c.len_utf8()];
lock.write_all(&buf).unwrap();
}
lock.write_all(b"\n").unwrap();
}
}
pub fn set_channel_wires(&mut self, ops: &[ChannelSubState], x: &mut u32) {
// Draw actual channel.
// Defines how many additional blocks of space should be next to the wires.
const CHANNEL_WIRE_PADDING: usize = 0;
for state in ops.iter() {
for xi in 0..(1 + CHANNEL_WIRE_PADDING) {
// Go through the current channel state.
for channel_idx in 0..state.occupancy_map.len() {
if state.occupancy_map.get(channel_idx).unwrap() != 0 {
// Channel is occupied, draw a wire (or crossing).
let x = *x as usize;
if self.get(x + xi, channel_idx) == BlockType::WireV {
self.set(x + xi, channel_idx, BlockType::WireCrossing);
} else {
self.set(x + xi, channel_idx, BlockType::WireH);
}
}
}
}
for op in state.wires.iter() {
let WireConnection {
from: source,
to: mut destination,
mode: op,
} = op.clone();
if op == ChannelOp::Copy {
destination.push(source);
}
// Connect all destination pins of the same net if there is more than one.
let mi = *destination.iter().min().unwrap();
let ma = *destination.iter().max().unwrap();
if mi != ma {
for y in mi..=ma {
if y == mi {
self.set(
*x as _,
y as _,
BlockType::WireCorner(CornerOrientation::DownRight),
);
} else if y == ma {
self.set(
*x as _,
y as _,
BlockType::WireCorner(CornerOrientation::UpRight),
);
} else if destination.contains(&y) {
self.set(*x as _, y as _, BlockType::WireT(TRotation::RightUpDown));
} else if self.get(*x as _, y as _) == BlockType::WireH {
self.set(*x as _, y as _, BlockType::WireCrossing);
} else if self.get(*x as _, y as _) == BlockType::Air {
self.set(*x as _, y as _, BlockType::WireV);
}
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
enum SourcePosition {
Above,
Below,
}
let (start, end, spos) = if source < mi {
(source, mi, SourcePosition::Above)
} else if source > ma {
(ma, source, SourcePosition::Below)
} else {
if destination.contains(&source) {
if source == mi {
self.set(*x as _, source, BlockType::WireT(TRotation::LeftRightDown));
} else if source == ma {
self.set(*x as _, source, BlockType::WireT(TRotation::LeftRightUp));
} else {
self.set(*x as _, source, BlockType::WireStar);
}
} else {
self.set(*x as _, source, BlockType::WireT(TRotation::LeftUpDown));
}
continue;
};
// Connect input to ranges.
for y in start..=end {
if y == start || y == end {
let x = *x as usize;
let y = y as usize;
if y == source && op == ChannelOp::Copy {
continue;
}
let block_type = match (y == source, spos, self.get(x, y)) {
// @ Source - should be empty.
(true, SourcePosition::Above, BlockType::Air) => {
BlockType::WireCorner(CornerOrientation::LeftDown)
}
(true, SourcePosition::Below, BlockType::Air) => {
BlockType::WireCorner(CornerOrientation::LeftUp)
}
// Destination, should not be empty.
// If we're coming from above, we either have already the correct connection (single pin)
(
false,
SourcePosition::Above,
BlockType::WireCorner(CornerOrientation::DownRight),
) => BlockType::WireT(TRotation::RightUpDown),
(
false,
SourcePosition::Below,
BlockType::WireCorner(CornerOrientation::UpRight),
) => BlockType::WireT(TRotation::RightUpDown),
(false, SourcePosition::Below, BlockType::WireH) => {
BlockType::WireCorner(CornerOrientation::DownRight)
}
(false, SourcePosition::Above, BlockType::WireH) => {
BlockType::WireCorner(CornerOrientation::UpRight)
}
(is_start, pos, prev) => {
println!(
"Unexpected block type {:?} is_start={} pos={:?} - {}:{}",
prev, is_start, pos, x, y
);
BlockType::Constant
}
};
self.set(x as _, y as _, block_type);
} else if self.get(*x as _, y as _) == BlockType::Air {
self.set(*x as _, y as _, BlockType::WireV);
} else if self.get(*x as _, y as _) == BlockType::WireH {
self.set(*x as _, y as _, BlockType::WireCrossing);
}
}
if op == ChannelOp::Copy {
self.set(
*x as _,
source as usize,
BlockType::WireT(match spos {
SourcePosition::Below => TRotation::LeftRightUp,
SourcePosition::Above => TRotation::LeftRightDown,
}),
);
}
}
*x += 1 + CHANNEL_WIRE_PADDING as u32;
}
}
pub fn generate_lua_schematic(&self, fname: &str) -> std::io::Result<()> {
let d = self.dimensions();
let mut file = File::create(fname)?;
file.write_all("schematic = {\n".as_bytes())?;
file.write_all(format!("\tsize = {{x={}, y=2, z={}}},\n", d.1, d.0).as_bytes())?;
file.write_all("\tdata = {\n".as_bytes())?;
for x in 0..d.0 {
for z in 0..2 {
for y in 0..d.1 {
file.write_all(
if z == 0 {
"\t\t{name=\"stone\"},\n".to_string()
} else {
format!(
"\t\t{{name=\"{}\", param2=param2}},\n",
self.get(x, y).minetest_type()
)
}
.as_bytes(),
)?;
}
}
}
file.write_all("\t}\n".as_bytes())?;
file.write_all("}\n".as_bytes())?;
Ok(())
}
pub fn serialize_to_mts(&self, fname: &str) -> std::io::Result<()> {
// Map size.
let d = self.dimensions();
// The minetest source is not consistent when it comes to the type of
// this field (i16 vs u16), so picking the conservative option here.
if d.1 > std::i16::MAX as _ || d.0 >= std::i16::MAX as _ {
panic!("Schematic too big to export to a mts :/");
}
let mut file = File::create(fname)?;
file.write_all(b"MTSM")?; // MTSCHEM_FILE_SIGNATURE
file.write_u16::<BigEndian>(1)?; // Version 1
file.write_i16::<BigEndian>(d.1 as i16)?;
file.write_i16::<BigEndian>(2)?;
file.write_i16::<BigEndian>(d.0 as i16)?;
// No need to do the prob table as we're totally old.
// Write # node names.
file.write_u16::<BigEndian>(BLOCK_IDS.len() as u16)?;
let serialize_string = |f: &mut File, s: &str| -> std::io::Result<()> {
if s.len() > std::u16::MAX as usize {
panic!("String too large to serialize.");
}
let s = s.as_bytes();
f.write_u16::<BigEndian>(s.len() as u16)?;
f.write_all(s)?;
Ok(())
};
for b in BLOCK_IDS.iter() {
serialize_string(&mut file, b)?;
}
let mut encoder = BufWriter::new(ZlibEncoder::new(file, Compression::Best));
// Generate reverse lookup table for block ids.
let mut block_lookup_table: std::collections::HashMap<&'static str, usize> =
std::collections::HashMap::new();
for (idx, val) in BLOCK_IDS.iter().enumerate() {
block_lookup_table.insert(val, idx);
}
println!(" [+] Writing node types");
// Write node types.
for x in 0..d.0 {
for z in 0..2 {
for y in 0..d.1 {
let t = if z == 0 {
"stone"
} else {
self.get(x, y).minetest_type()
};
encoder.write_u16::<BigEndian>(
*block_lookup_table
.get(t)
.unwrap_or_else(|| panic!("Block type {:?} not found?", t))
as u16,
)?;
}
}
}
println!(" [+] Writing param1");
// Write param1
for _ in 0..2 {
for _ in 0..d.0 {
for _ in 0..d.1 {
encoder.write_u8(0)?;
}
}
}
println!(" [+] Writing param2");
// Write param2
for x in 0..d.0 {
for z in 0..2 {
for y in 0..d.1 {
encoder.write_u8(if z == 0 {
0
} else {
self.get(x, y).get_param2()
})?;
}
}
}
Ok(())
}
}