use crate::routing::engine::blocks as rblocks;
use crate::UniversalSchematic;
pub type P3 = (i32, i32, i32);
const REFRESH_AT: usize = 6;
const SUPPORT: &str = rblocks::STONE;
fn add(a: P3, b: P3) -> P3 {
(a.0 + b.0, a.1 + b.1, a.2 + b.2)
}
fn mul(a: P3, k: i32) -> P3 {
(a.0 * k, a.1 * k, a.2 * k)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PortPatch {
pub writes: std::collections::BTreeMap<P3, Option<String>>,
pub saved: std::collections::BTreeMap<P3, Option<String>>,
pub wires: Vec<P3>,
pub hardware: Vec<P3>,
pub step: P3,
pub pivoted: bool,
pub note: String,
}
impl PortPatch {
pub fn to_json(&self) -> String {
let pos = |ps: &[P3]| {
let v: Vec<String> = ps
.iter()
.map(|p| format!("[{},{},{}]", p.0, p.1, p.2))
.collect();
format!("[{}]", v.join(","))
};
let removed = self.writes.values().filter(|v| v.is_none()).count()
+ self
.saved
.iter()
.filter(|(p, v)| v.is_some() && self.writes.get(*p).is_some_and(|w| w.is_some()))
.count();
format!(
"{{\"wires\":{},\"hardware\":{},\"step\":[{},{},{}],\"removed\":{},\"added\":{},\
\"pivoted\":{},\"note\":{:?}}}",
pos(&self.wires),
pos(&self.hardware),
self.step.0,
self.step.1,
self.step.2,
removed,
self.writes.values().filter(|v| v.is_some()).count(),
self.pivoted,
self.note,
)
}
}
struct Body<'a> {
sch: &'a UniversalSchematic,
}
impl Body<'_> {
fn at(&self, p: P3) -> Option<String> {
self.sch
.get_block(p.0, p.1, p.2)
.map(|b| b.to_string())
.filter(|s| !s.contains("minecraft:air"))
}
fn free(&self, p: P3) -> bool {
self.at(p).is_none()
}
}
struct Patcher<'a> {
body: Body<'a>,
patch: PortPatch,
}
impl<'a> Patcher<'a> {
fn new(sch: &'a UniversalSchematic) -> Self {
Patcher {
body: Body { sch },
patch: PortPatch::default(),
}
}
fn free(&self, p: P3) -> bool {
match self.patch.writes.get(&p) {
Some(Some(_)) => false,
Some(None) => true,
None => self.body.free(p),
}
}
fn write(&mut self, p: P3, block: Option<&str>) {
self.patch.saved.entry(p).or_insert_with(|| self.body.at(p));
self.patch.writes.insert(p, block.map(|s| s.to_string()));
}
fn place(&mut self, p: P3, block: &str, what: &str) -> Result<(), String> {
if !self.free(p) {
return Err(format!(
"promotion needs {:?} for its {what}, but the cell has `{}` there",
p,
self.body.at(p).unwrap_or_else(|| "?".into())
));
}
self.write(p, Some(block));
Ok(())
}
fn support(&mut self, p: P3) -> Result<(), String> {
let below = add(p, (0, -1, 0));
match self.patch.writes.get(&below) {
Some(Some(b)) if rblocks::is_sturdy_support(b) => Ok(()),
Some(Some(b)) => Err(format!(
"promotion wants a support under {below:?} but its own stub put `{b}` there"
)),
_ => match self.body.at(below) {
Some(b) if rblocks::is_sturdy_support(&b) => Ok(()),
Some(b) => Err(format!(
"promotion wants a support under {below:?}; the cell has non-sturdy `{b}` there"
)),
None => {
self.write(below, Some(SUPPORT));
Ok(())
}
},
}
}
fn dust(&mut self, p: P3, what: &str) -> Result<(), String> {
self.place(p, rblocks::DUST, what)?;
self.support(p)
}
}
fn attachment(block: &str, at: P3) -> Result<(P3, P3), String> {
let face = block
.find("face=")
.map(|i| {
let rest = &block[i + 5..];
let end = rest.find([',', ']']).unwrap_or(rest.len());
&rest[..end]
})
.ok_or_else(|| format!("`{block}` at {at:?} has no `face=` property"))?;
match face {
"floor" => Ok((add(at, (0, -1, 0)), (0, 1, 0))),
"ceiling" => Ok((add(at, (0, 1, 0)), (0, -1, 0))),
"wall" => {
let f = rblocks::facing_of(block)
.and_then(rblocks::facing_vec)
.ok_or_else(|| format!("wall lever `{block}` at {at:?} has no usable facing"))?;
Ok((add(at, mul(f, -1)), f))
}
other => Err(format!("`{block}` at {at:?}: unsupported face `{other}`")),
}
}
pub fn plan_input(sch: &UniversalSchematic, hardware: &[P3]) -> Result<PortPatch, String> {
if hardware.is_empty() {
return Err("port declares no positions".to_string());
}
let mut p = Patcher::new(sch);
let mut wires = Vec::new();
let mut how = "";
for (k, hp) in hardware.iter().enumerate() {
let block = p
.body
.at(*hp)
.ok_or_else(|| format!("bit {k}: nothing at {hp:?} to promote"))?;
if rblocks::is_dust(&block) {
wires.push(*hp);
how = "already dust";
continue;
}
if !(rblocks::is_lever(&block) || block.contains("button")) {
return Err(format!(
"bit {k}: {hp:?} holds `{block}`, which is not a lever or button — promotion \
replaces executor hardware, so declare the port over the real hardware first"
));
}
let (att, out) = attachment(&block, *hp)?;
if out.1 != 0 {
if out.1 < 0 {
return Err(format!(
"bit {k}: {hp:?} is a CEILING lever (attachment block above at {att:?}); \
nothing may sit above a block to power it, so this port cannot be promoted \
— rebuild the cell with a floor or wall lever"
));
}
p.write(*hp, Some(rblocks::DUST));
p.support(*hp)?;
wires.push(*hp);
how = "dust on the lever's own support (weak power into the attachment block)";
} else {
let facing = rblocks::facing_of(&block)
.ok_or_else(|| format!("bit {k}: wall lever `{block}` has no facing"))?
.to_string();
p.write(*hp, Some(&rblocks::repeater(&facing, 1)));
p.support(*hp)?;
let wire = add(*hp, out);
p.dust(wire, "connection cell")?;
wires.push(wire);
how = "repeater into the attachment block, connection dust one cell out";
}
}
finish(p, hardware, wires, how, sch, false)
}
pub fn plan_output(sch: &UniversalSchematic, hardware: &[P3]) -> Result<PortPatch, String> {
if hardware.is_empty() {
return Err("port declares no positions".to_string());
}
let mut p = Patcher::new(sch);
let mut wires = Vec::new();
for (k, hp) in hardware.iter().enumerate() {
let block = p
.body
.at(*hp)
.ok_or_else(|| format!("bit {k}: nothing at {hp:?} to tap"))?;
if rblocks::is_dust(&block) {
wires.push(*hp);
continue;
}
if !rblocks::is_sturdy_support(&block) {
return Err(format!(
"bit {k}: {hp:?} holds `{block}`, which cannot support a dust tap — an output \
port is promoted by putting dust on the lamp that already carries the signal"
));
}
let tap = add(*hp, (0, 1, 0));
p.place(tap, rblocks::DUST, "output tap")?;
wires.push(tap);
}
finish(
p,
hardware,
wires,
"dust tap on top of the output lamp (the lamp keeps working)",
sch,
true,
)
}
fn uniform_step(wires: &[P3]) -> Result<P3, String> {
if wires.len() == 1 {
return Ok((0, 2, 0));
}
let s = (
wires[1].0 - wires[0].0,
wires[1].1 - wires[0].1,
wires[1].2 - wires[0].2,
);
if wires
.windows(2)
.all(|w| (w[1].0 - w[0].0, w[1].1 - w[0].1, w[1].2 - w[0].2) == s)
{
Ok(s)
} else {
Err(format!(
"connection cells {wires:?} do not lie on a uniform step"
))
}
}
fn finish(
mut p: Patcher<'_>,
hardware: &[P3],
wires: Vec<P3>,
how: &str,
_sch: &UniversalSchematic,
_flow_out: bool,
) -> Result<PortPatch, String> {
let step = uniform_step(&wires)?;
let n = wires.len();
if step.1 != 0 && step != (0, 2, 0) {
return Err(format!(
"promoted connection cells step {step:?}: a bus can adapt a horizontal ROW or the \
canonical vertical 2y-pitch stack onto its form. This port's hardware is neither."
));
}
let body_at = |q: P3| -> Option<String> { p.body.at(q) };
let mut writes: std::collections::BTreeMap<P3, String> = p
.patch
.writes
.iter()
.filter_map(|(q, v)| v.clone().map(|b| (*q, b)))
.collect();
crate::routing::engine::wire::rewire(&mut writes, &body_at);
for (q, b) in writes {
p.patch.writes.insert(q, Some(b));
}
p.patch.step = if n == 1 { (0, 2, 0) } else { step };
p.patch.wires = wires;
p.patch.hardware = hardware.to_vec();
p.patch.note = if p.patch.step == (0, 2, 0) {
format!("{n} bit(s): {how}")
} else {
format!(
"{n} bit(s): {how}; the port keeps its native {:?}-pitch form — the bus grows the \
form adapter it needs, and rips it with itself",
p.patch.step
)
};
Ok(p.patch)
}
#[derive(Clone, Debug, Default)]
pub struct PivotPlan {
pub cells: std::collections::BTreeMap<P3, String>,
pub column: Vec<P3>,
pub note: String,
}
struct PivotSink<'f> {
at: &'f dyn Fn(P3) -> Option<String>,
cells: std::collections::BTreeMap<P3, String>,
}
impl PivotSink<'_> {
fn look(&self, p: P3) -> Option<String> {
self.cells.get(&p).cloned().or_else(|| (self.at)(p))
}
fn free(&self, p: P3) -> bool {
self.look(p).is_none()
}
fn write(&mut self, p: P3, block: Option<&str>) {
match block {
Some(b) => {
self.cells.insert(p, b.to_string());
}
None => {
self.cells.remove(&p);
}
}
}
fn place(&mut self, p: P3, block: &str, what: &str) -> Result<(), String> {
if let Some(b) = self.look(p) {
return Err(format!(
"the form adapter needs {p:?} for its {what}, but `{b}` is there"
));
}
self.write(p, Some(block));
Ok(())
}
fn support(&mut self, p: P3) -> Result<(), String> {
let below = add(p, (0, -1, 0));
match self.look(below) {
Some(b) if rblocks::is_sturdy_support(&b) => Ok(()),
Some(b) => Err(format!(
"the form adapter wants a support under {below:?}; `{b}` is not sturdy"
)),
None => {
self.write(below, Some(SUPPORT));
Ok(())
}
}
}
fn dust(&mut self, p: P3, what: &str) -> Result<(), String> {
self.place(p, rblocks::DUST, what)?;
self.support(p)
}
}
pub fn plan_pivot(
wires: &[P3],
step: P3,
prefer_away_from: P3,
flow_out: bool,
at: &dyn Fn(P3) -> Option<String>,
toward: Option<P3>,
gather_end: Option<bool>,
) -> Result<PivotPlan, String> {
if wires.is_empty() {
return Err("form adapter needs at least one connection cell".to_string());
}
if step.1 != 0 {
return Err(format!(
"form adapter turns a HORIZONTAL row into the vertical stack; this port's step is \
{step:?}"
));
}
let along = if step.0 != 0 { (1, 0, 0) } else { (0, 0, 1) };
let mut cands: Vec<P3> = if step.0 != 0 {
vec![(0, 0, -1), (0, 0, 1)]
} else {
vec![(-1, 0, 0), (1, 0, 0)]
};
let outward = |d: &P3| {
-((d.0 * (prefer_away_from.0 - wires[0].0)) + (d.2 * (prefer_away_from.2 - wires[0].2)))
};
cands.sort_by_key(|d| -outward(d));
let mut errs = Vec::new();
let mut best: Option<(i64, PivotPlan)> = None;
let ends: &[bool] = match gather_end {
Some(false) => &[false],
Some(true) => &[true],
None => &[false, true],
};
let reach_weight = wires.len() as i64;
for &gather_to_last in ends {
for out in &cands {
let out = *out;
let mut sink = PivotSink {
at,
cells: std::collections::BTreeMap::new(),
};
match lay_pivot(&mut sink, wires, step, along, out, flow_out, gather_to_last) {
Ok(column) => {
let cells = sink.cells.len() as i64;
let reach = toward.map_or(0, |t| {
((column[0].0 - t.0).abs() + (column[0].2 - t.2).abs()) as i64
}) * reach_weight;
let cost =
cells + reach - i64::from(outward(&out) > 0) + i64::from(gather_to_last);
let plan = PivotPlan {
cells: sink.cells,
column,
note: format!(
"form adapter: pivoted the {:?}-pitch row onto a vertical 2y stack via \
a staircase growing {} block(s) toward {:?}, gathering to bit {}'s \
lane ({cells} cells; the selected side/end combination)",
step,
2 * wires.len(),
out,
if gather_to_last { wires.len() - 1 } else { 0 }
),
};
if best.as_ref().is_none_or(|(c, _)| cost < *c) {
best = Some((cost, plan));
}
}
Err(e) => errs.push(format!("toward {out:?} gather_last={gather_to_last}: {e}")),
}
}
}
if let Some((_, plan)) = best {
return Ok(plan);
}
Err(format!(
"the row needs a form adapter to reach the vertical 2y-pitch bus stack, but neither side \
of the port face has room for one ({})",
errs.join("; ")
))
}
#[allow(clippy::too_many_arguments)]
fn lay_pivot(
p: &mut PivotSink<'_>,
wires: &[P3],
step: P3,
along: P3,
out: P3,
flow_out: bool,
gather_to_last: bool,
) -> Result<Vec<P3>, String> {
let n = wires.len() as i32;
let pitch = step.0.abs().max(step.2.abs()); if pitch < 2 {
return Err(format!("row pitch {pitch} leaves no lane between bits"));
}
let to_first = mul(along, -(step.0 + step.2).signum());
let back = if gather_to_last {
mul(to_first, -1)
} else {
to_first
};
let depth = 2 + 2 * (n - 1) + 2 * refresh_pauses(2 * (n - 1) as usize) as i32;
let mut column = Vec::new();
for i in 0..n {
let w = wires[i as usize];
let mut y = w.1;
let mut t = 1i32;
let mut since = 0usize;
let sgn = if flow_out { -1 } else { 1 };
let rep_in = rblocks::facing_name(sgn * out.0, sgn * out.2)
.ok_or("pivot direction is not axis-aligned")?;
p.place(add(w, out), &rblocks::repeater(rep_in, 1), "stub repeater")?;
p.support(add(w, out))?;
t += 1;
p.dust(add(w, mul(out, t)), "stub run")?;
since = 1;
t += 1;
let mut climb = 2 * i;
while climb > 0 {
if since >= REFRESH_AT {
let c = add(add(w, mul(out, t)), (0, y - w.1, 0));
p.place(c, &rblocks::repeater(rep_in, 1), "refresh repeater")?;
p.support(c)?;
t += 1;
let d = add(add(w, mul(out, t)), (0, y - w.1, 0));
p.dust(d, "landing")?;
since = 1;
t += 1;
continue;
}
let here = add(add(w, mul(out, t - 1)), (0, y - w.1, 0));
if !p.free(add(here, (0, 1, 0))) {
return Err(format!(
"the staircase for bit {i} needs {:?} clear to climb",
add(here, (0, 1, 0))
));
}
y += 1;
p.dust(add(add(w, mul(out, t)), (0, y - w.1, 0)), "staircase")?;
since += 1;
climb -= 1;
t += 1;
}
while t <= depth {
let c = add(add(w, mul(out, t)), (0, y - w.1, 0));
if since >= REFRESH_AT && t < depth {
p.place(c, &rblocks::repeater(rep_in, 1), "refresh repeater")?;
p.support(c)?;
since = 0;
} else {
p.dust(c, "stub run")?;
since += 1;
}
t += 1;
}
debug_assert_eq!(
t - 1,
depth,
"bit {i}'s lane ended at depth {} but the gather plane is at {depth}",
t - 1
);
if t - 1 != depth {
return Err(format!(
"internal: bit {i}'s staircase ended at depth {}, not the shared {depth}",
t - 1
));
}
let corner = add(add(w, mul(out, depth)), (0, y - w.1, 0));
let gsgn = if flow_out { -1 } else { 1 };
let gather_in = rblocks::facing_name(gsgn * back.0, gsgn * back.2).ok_or("gather axis")?;
let hops = pitch * if gather_to_last { n - 1 - i } else { i };
let mut since_g = 0usize;
for k in 1..=hops {
let c = add(corner, mul(back, k));
if k == 1 || since_g >= REFRESH_AT {
p.place(c, &rblocks::repeater(gather_in, 1), "gather repeater")?;
p.support(c)?;
since_g = 0;
} else {
p.dust(c, "gather run")?;
since_g += 1;
}
}
let last = add(corner, mul(back, hops));
if hops > 0 {
p.write(last, Some(rblocks::DUST));
p.support(last)?;
}
column.push(last);
}
Ok(column)
}
fn refresh_pauses(h: usize) -> usize {
h.saturating_sub(1) / (REFRESH_AT - 1)
}
#[cfg(test)]
mod tests {
use super::*;
fn slab(sx: i32, sy: i32, sz: i32) -> UniversalSchematic {
let mut s = UniversalSchematic::new("t".to_string());
for x in 0..sx {
for y in 0..sy {
for z in 0..sz {
s.set_block_from_string(x, y, z, rblocks::STONE).unwrap();
}
}
}
s
}
#[test]
fn a_wall_lever_is_promoted_through_a_repeater() {
let mut s = slab(4, 8, 1);
s.set_block_from_string(
-1,
3,
0,
"minecraft:lever[face=wall,facing=west,powered=false]",
)
.unwrap();
let patch = plan_input(&s, &[(-1, 3, 0)]).unwrap();
assert_eq!(patch.wires, vec![(-2, 3, 0)]);
assert!(patch.writes[&(-1, 3, 0)]
.as_deref()
.unwrap()
.contains("repeater"));
assert!(rblocks::is_dust(
patch.writes[&(-2, 3, 0)].as_deref().unwrap()
));
assert_eq!(
patch.saved[&(-1, 3, 0)].as_deref(),
Some("minecraft:lever[face=wall,facing=west,powered=false]")
);
}
#[test]
fn a_floor_lever_is_promoted_in_place() {
let mut s = slab(4, 4, 1);
s.set_block_from_string(
1,
4,
0,
"minecraft:lever[face=floor,facing=north,powered=false]",
)
.unwrap();
let patch = plan_input(&s, &[(1, 4, 0)]).unwrap();
assert_eq!(patch.wires, vec![(1, 4, 0)]);
assert!(rblocks::is_dust(
patch.writes[&(1, 4, 0)].as_deref().unwrap()
));
}
#[test]
fn a_horizontal_row_keeps_its_form_and_the_bus_adapts_it() {
let mut s = slab(8, 4, 4);
for i in 0..4 {
s.set_block_from_string(
2 * i,
4,
0,
"minecraft:lever[face=floor,facing=north,powered=false]",
)
.unwrap();
}
let hw: Vec<P3> = (0..4).map(|i| (2 * i, 4, 0)).collect();
let patch = plan_input(&s, &hw).unwrap();
assert!(!patch.pivoted, "{}", patch.note);
assert_eq!(patch.step, (2, 0, 0), "{}", patch.note);
assert_eq!(patch.wires, hw, "the connection cells left the lever row");
assert_eq!(
patch.writes.len(),
4,
"promotion wrote {} cells for 4 bits: {:?}",
patch.writes.len(),
patch.writes
);
for w in &hw {
assert!(
patch
.writes
.get(w)
.and_then(|o| o.as_deref())
.is_some_and(rblocks::is_dust),
"bit at {w:?} is not dust in place"
);
}
let at = |q: P3| -> Option<String> {
s.get_block(q.0, q.1, q.2)
.map(|b| b.to_string())
.filter(|b| !b.contains("minecraft:air"))
.filter(|_| !hw.contains(&q))
};
let plan = plan_pivot(&patch.wires, patch.step, (4, 4, 0), false, &at, None, None)
.expect("the row must be adaptable onto the stack");
for (k, w) in plan.column.iter().enumerate() {
assert_eq!(w.0, plan.column[0].0, "bit {k} left the column");
assert_eq!(w.2, plan.column[0].2, "bit {k} left the column");
assert_eq!(w.1, plan.column[0].1 + 2 * k as i32, "bit {k} off pitch");
}
for (q, b) in &plan.cells {
if rblocks::is_dust(b) {
let below = add(*q, (0, -1, 0));
let has = plan
.cells
.get(&below)
.map(|b| rblocks::is_sturdy_support(b))
.unwrap_or(false)
|| s.get_block(below.0, below.1, below.2)
.map(|b| rblocks::is_sturdy_support(&b.to_string()))
.unwrap_or(false);
assert!(has, "adapter dust at {q:?} floats");
}
}
}
#[test]
fn a_pivot_can_gather_to_either_end_without_changing_its_size() {
let wires: Vec<P3> = (0..8).map(|i| (2 + 2 * i, 2, 2)).collect();
let at = |_q: P3| None;
let first = plan_pivot(&wires, (2, 0, 0), (8, 2, 2), true, &at, None, Some(false)).unwrap();
let last = plan_pivot(&wires, (2, 0, 0), (8, 2, 2), true, &at, None, Some(true)).unwrap();
assert_eq!(first.column[0].0, wires[0].0);
assert_eq!(last.column[0].0, wires[7].0);
assert_eq!(first.cells.len(), last.cells.len());
}
#[test]
fn a_ceiling_lever_is_refused_with_a_reason() {
let mut s = slab(4, 8, 1);
s.set_block_from_string(
1,
2,
0,
"minecraft:lever[face=ceiling,facing=north,powered=false]",
)
.unwrap();
let e = plan_input(&s, &[(1, 2, 0)]).unwrap_err();
assert!(e.contains("CEILING"), "{e}");
}
#[test]
fn a_lamp_output_gets_a_dust_tap() {
let mut s = slab(4, 4, 1);
s.set_block_from_string(1, 4, 0, rblocks::LAMP).unwrap();
let patch = plan_output(&s, &[(1, 4, 0)]).unwrap();
assert_eq!(patch.wires, vec![(1, 5, 0)]);
assert!(!patch.writes.contains_key(&(1, 4, 0)));
}
}