use crate::formats::gametest::to_gametest_snbt;
pub(crate) fn simulate_placement_into(
schematic: &mut crate::UniversalSchematic,
x: i32,
y: i32,
z: i32,
descriptor: &str,
) -> Result<usize, String> {
simulate_placements_into(schematic, &[(x, y, z)], descriptor)
}
const LOCAL_COMPONENT_LINK: i32 = 2;
const LOCAL_EFFECT_MARGIN: i32 = 2;
const LOCAL_PISTON_EFFECT_MARGIN: i32 = 12;
const LOCAL_CONTEXT_MARGIN: i32 = 4;
fn block_is_air(block: Option<&crate::BlockState>) -> bool {
block.is_none_or(|block| {
matches!(
block.get_name(),
"minecraft:air" | "minecraft:cave_air" | "minecraft:void_air"
)
})
}
fn active_near_placements(
schematic: &crate::UniversalSchematic,
positions: &[(i32, i32, i32)],
) -> bool {
positions.iter().any(|&(x, y, z)| {
for dx in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
for dy in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
for dz in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
if dx.abs() + dy.abs() + dz.abs() > LOCAL_COMPONENT_LINK {
continue;
}
let Some(candidate) = x
.checked_add(dx)
.zip(y.checked_add(dy))
.zip(z.checked_add(dz))
.map(|((x, y), z)| (x, y, z))
else {
continue;
};
if schematic
.get_block(candidate.0, candidate.1, candidate.2)
.is_some_and(|block| {
mc_tick::vanilla::is_simulation_component(&block.to_string())
})
{
return true;
}
}
}
}
false
})
}
fn try_resolve_placements(
schematic: &mut crate::UniversalSchematic,
positions: &[(i32, i32, i32)],
descriptor: &str,
core: Option<&crate::BoundingBox>,
requested_cells: usize,
) -> Result<Option<usize>, String> {
let name = descriptor
.split_once('[')
.map_or(descriptor, |(name, _)| name);
if !mc_tick::vanilla::is_simulation_component(descriptor)
&& !active_near_placements(schematic, positions)
{
for &(x, y, z) in positions {
schematic.set_block_from_string(x, y, z, descriptor)?;
}
return Ok(Some(requested_cells));
}
if !matches!(name, "minecraft:redstone_wire" | "minecraft:redstone_block") {
return Ok(None);
}
let Some(core) = core else {
return Ok(None);
};
try_resolve_simple_wire_network(schematic, positions, descriptor, core, requested_cells)
}
fn try_resolve_simple_wire_network(
schematic: &mut crate::UniversalSchematic,
positions: &[(i32, i32, i32)],
descriptor: &str,
core: &crate::BoundingBox,
requested_cells: usize,
) -> Result<Option<usize>, String> {
use std::collections::{HashMap, HashSet, VecDeque};
const WIRE: &str = "minecraft:redstone_wire";
const SOURCE: &str = "minecraft:redstone_block";
const HORIZONTAL: [(i32, i32, i32); 4] = [(0, 0, -1), (0, 0, 1), (-1, 0, 0), (1, 0, 0)];
const ALL_FACES: [(i32, i32, i32); 6] = [
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
(-1, 0, 0),
(1, 0, 0),
];
let placed_name = descriptor
.split_once('[')
.map_or(descriptor, |(name, _)| name);
let mut wires = HashSet::new();
let mut sources = HashSet::new();
let mut wire_y = None;
for x in core.min.0..=core.max.0 {
for y in core.min.1..=core.max.1 {
for z in core.min.2..=core.max.2 {
let Some(block) = schematic.get_block(x, y, z) else {
continue;
};
let name = block.get_name();
if mc_tick::vanilla::is_simulation_component(&block.to_string())
&& !matches!(name, WIRE | SOURCE)
{
return Ok(None);
}
if name == WIRE {
if wire_y.is_some_and(|level| level != y) {
return Ok(None); }
wire_y = Some(y);
wires.insert((x, y, z));
} else if name == SOURCE {
sources.insert((x, y, z));
}
}
}
}
for &position in positions {
wires.remove(&position);
sources.remove(&position);
if placed_name == WIRE {
if wire_y.is_some_and(|level| level != position.1) {
return Ok(None);
}
wire_y = Some(position.1);
wires.insert(position);
} else {
sources.insert(position);
}
}
for &(x, y, z) in &wires {
if !block_is_air(schematic.get_block(x, y + 1, z)) {
return Ok(None);
}
if block_is_air(schematic.get_block(x, y - 1, z)) && !sources.contains(&(x, y - 1, z)) {
return Ok(None);
}
for &(dx, _, dz) in &HORIZONTAL {
if wires.contains(&(x + dx, y + 1, z + dz)) || wires.contains(&(x + dx, y - 1, z + dz))
{
return Ok(None);
}
}
}
let mut power: HashMap<(i32, i32, i32), u8> = wires
.iter()
.copied()
.map(|position| (position, 0))
.collect();
let mut queue = VecDeque::new();
for &wire in &wires {
if ALL_FACES
.iter()
.any(|&(dx, dy, dz)| sources.contains(&(wire.0 + dx, wire.1 + dy, wire.2 + dz)))
{
power.insert(wire, 15);
queue.push_back(wire);
}
}
while let Some(position) = queue.pop_front() {
let next_power = power[&position].saturating_sub(1);
if next_power == 0 {
continue;
}
for &(dx, _, dz) in &HORIZONTAL {
let next = (position.0 + dx, position.1, position.2 + dz);
if wires.contains(&next) && power[&next] < next_power {
power.insert(next, next_power);
queue.push_back(next);
}
}
}
let mut written = 0;
for &position in positions {
if placed_name == SOURCE {
let before = schematic
.get_block(position.0, position.1, position.2)
.map(ToString::to_string);
if before.as_deref() != Some(descriptor) {
schematic.set_block_from_string(position.0, position.1, position.2, descriptor)?;
written += 1;
}
}
}
for &(x, y, z) in &wires {
let mut sides = [false; 4]; for (index, &(dx, _, dz)) in HORIZONTAL.iter().enumerate() {
sides[index] =
wires.contains(&(x + dx, y, z + dz)) || sources.contains(&(x + dx, y, z + dz));
}
let no_north_south = !sides[0] && !sides[1];
let no_west_east = !sides[2] && !sides[3];
if !sides[2] && no_north_south {
sides[2] = true;
}
if !sides[3] && no_north_south {
sides[3] = true;
}
if !sides[0] && no_west_east {
sides[0] = true;
}
if !sides[1] && no_west_east {
sides[1] = true;
}
let side = |connected| if connected { "side" } else { "none" };
let resolved = format!(
"{WIRE}[east={},north={},power={},south={},west={}]",
side(sides[3]),
side(sides[0]),
power[&(x, y, z)],
side(sides[1]),
side(sides[2])
);
let before = schematic.get_block(x, y, z).map(ToString::to_string);
if before.as_deref() != Some(resolved.as_str()) {
schematic.set_block_from_string(x, y, z, &resolved)?;
written += 1;
}
}
Ok(Some(written.max(requested_cells)))
}
fn checked_local_bounds(
schematic: &crate::UniversalSchematic,
positions: &[(i32, i32, i32)],
descriptor: &str,
) -> Result<Option<(crate::BoundingBox, crate::BoundingBox)>, String> {
use std::collections::{HashSet, VecDeque};
if positions.is_empty() {
return Ok(None);
}
let is_active = |position: (i32, i32, i32)| {
schematic
.get_block(position.0, position.1, position.2)
.is_some_and(|block| mc_tick::vanilla::is_simulation_component(&block.to_string()))
};
let mut active = HashSet::new();
let mut queue = VecDeque::new();
for &position in positions {
if mc_tick::vanilla::is_simulation_component(descriptor) {
active.insert(position);
queue.push_back(position);
}
for dx in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
for dy in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
for dz in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
if dx.abs() + dy.abs() + dz.abs() > LOCAL_COMPONENT_LINK {
continue;
}
let Some(candidate) = position
.0
.checked_add(dx)
.zip(position.1.checked_add(dy))
.zip(position.2.checked_add(dz))
.map(|((x, y), z)| (x, y, z))
else {
continue;
};
if is_active(candidate) && active.insert(candidate) {
queue.push_back(candidate);
}
}
}
}
}
while let Some(position) = queue.pop_front() {
for dx in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
for dy in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
for dz in -LOCAL_COMPONENT_LINK..=LOCAL_COMPONENT_LINK {
if dx.abs() + dy.abs() + dz.abs() > LOCAL_COMPONENT_LINK {
continue;
}
let Some(candidate) = position
.0
.checked_add(dx)
.zip(position.1.checked_add(dy))
.zip(position.2.checked_add(dz))
.map(|((x, y), z)| (x, y, z))
else {
continue;
};
if is_active(candidate) && active.insert(candidate) {
queue.push_back(candidate);
}
}
}
}
}
let is_motion = |descriptor: &str| {
matches!(
mc_tick::machine_graph::classify(descriptor),
mc_tick::machine_graph::PartKind::Piston { .. }
| mc_tick::machine_graph::PartKind::Slime
| mc_tick::machine_graph::PartKind::Honey
) || descriptor.starts_with("minecraft:moving_piston")
|| descriptor.starts_with("minecraft:piston_head")
};
let motion_component = is_motion(descriptor)
|| active.iter().any(|&(x, y, z)| {
schematic
.get_block(x, y, z)
.is_some_and(|block| is_motion(&block.to_string()))
});
let effect_margin = if motion_component {
LOCAL_PISTON_EFFECT_MARGIN
} else {
LOCAL_EFFECT_MARGIN
};
let mut min = positions[0];
let mut max = positions[0];
for &(x, y, z) in positions.iter().chain(active.iter()) {
min.0 = min.0.min(x);
min.1 = min.1.min(y);
min.2 = min.2.min(z);
max.0 = max.0.max(x);
max.1 = max.1.max(y);
max.2 = max.2.max(z);
}
let expand = |value: i32, amount: i32, lower: bool| {
if lower {
value.saturating_sub(amount)
} else {
value.saturating_add(amount)
}
};
let core = crate::BoundingBox::new(
(
expand(min.0, effect_margin, true),
expand(min.1, effect_margin, true),
expand(min.2, effect_margin, true),
),
(
expand(max.0, effect_margin, false),
expand(max.1, effect_margin, false),
expand(max.2, effect_margin, false),
),
);
let context = crate::BoundingBox::new(
(
expand(core.min.0, LOCAL_CONTEXT_MARGIN, true),
expand(core.min.1, LOCAL_CONTEXT_MARGIN, true),
expand(core.min.2, LOCAL_CONTEXT_MARGIN, true),
),
(
expand(core.max.0, LOCAL_CONTEXT_MARGIN, false),
expand(core.max.1, LOCAL_CONTEXT_MARGIN, false),
expand(core.max.2, LOCAL_CONTEXT_MARGIN, false),
),
);
let dimensions = (
i64::from(context.max.0) - i64::from(context.min.0) + 1,
i64::from(context.max.1) - i64::from(context.min.1) + 1,
i64::from(context.max.2) - i64::from(context.min.2) + 1,
);
let volume = dimensions
.0
.checked_mul(dimensions.1)
.and_then(|xy| xy.checked_mul(dimensions.2))
.unwrap_or(i64::MAX);
if volume > MAX_VOLUME as i64 {
return Err(format!(
"local simulated component is {} x {} x {} = {volume} cells, over the \
{MAX_VOLUME}-cell limit; split the placement batch by component",
dimensions.0, dimensions.1, dimensions.2
));
}
Ok(Some((core, context)))
}
pub(crate) fn simulate_placements_into(
schematic: &mut crate::UniversalSchematic,
positions: &[(i32, i32, i32)],
descriptor: &str,
) -> Result<usize, String> {
if positions.is_empty() {
return Ok(0);
}
let requested_cells = positions
.iter()
.copied()
.collect::<std::collections::HashSet<_>>()
.len();
if schematic.total_blocks() == 0 {
for &(x, y, z) in positions {
schematic.set_block_from_string(x, y, z, descriptor)?;
}
return Ok(requested_cells);
}
if let Some(written) =
try_resolve_placements(schematic, positions, descriptor, None, requested_cells)?
{
return Ok(written);
}
let Some((core, context)) = checked_local_bounds(schematic, positions, descriptor)? else {
return Ok(0);
};
if let Some(written) = try_resolve_placements(
schematic,
positions,
descriptor,
Some(&core),
requested_cells,
)? {
return Ok(written);
}
let mut local = schematic.create_schematic_from_region(&context);
local.metadata = schematic.metadata.clone();
let local_positions: Vec<(i32, i32, i32)> = positions
.iter()
.map(|&(x, y, z)| {
(
x.saturating_sub(context.min.0),
y.saturating_sub(context.min.1),
z.saturating_sub(context.min.2),
)
})
.collect();
simulate_placements_into_world(&mut local, &local_positions, descriptor)?;
let mut written = 0;
for x in core.min.0..=core.max.0 {
for y in core.min.1..=core.max.1 {
for z in core.min.2..=core.max.2 {
let local_pos = (
x.saturating_sub(context.min.0),
y.saturating_sub(context.min.1),
z.saturating_sub(context.min.2),
);
let after = local
.get_block(local_pos.0, local_pos.1, local_pos.2)
.map(ToString::to_string)
.unwrap_or_else(|| "minecraft:air".to_string());
let before = schematic
.get_block(x, y, z)
.map(ToString::to_string)
.unwrap_or_else(|| "minecraft:air".to_string());
if before != after {
schematic.set_block_from_string(x, y, z, &after)?;
written += 1;
}
}
}
}
Ok(written.max(requested_cells))
}
pub(crate) fn simulate_placement_into_world(
schematic: &mut crate::UniversalSchematic,
x: i32,
y: i32,
z: i32,
descriptor: &str,
) -> Result<usize, String> {
use mc_tick::Pos;
let bb = schematic.get_bounding_box();
let isolated = schematic.total_blocks() == 0 || {
let (min, max) = (bb.min, bb.max);
x < min.0 - 3
|| x > max.0 + 3
|| y < min.1 - 3
|| y > max.1 + 3
|| z < min.2 - 3
|| z > max.2 + 3
};
if isolated {
schematic.set_block_from_string(x, y, z, descriptor)?;
return Ok(1);
}
check_volume((
bb.max.0 - bb.min.0 + 1,
bb.max.1 - bb.min.1 + 1,
bb.max.2 - bb.min.2 + 1,
))?;
let offset = (bb.min.0, bb.min.1, bb.min.2);
let snbt = to_gametest_snbt(schematic);
let structure = mc_tick::Structure::parse(&snbt)
.map_err(|e| format!("simulate=true could not load this schematic: {e:?}"))?;
let mut sim = wire_simulation(
&structure,
Pos::new(0, 0, 0),
ffi::TickSettleMode::InWorld,
&[descriptor],
schematic.metadata.source_data_version,
)
.map_err(|e| format!("simulate=true could not simulate this schematic: {e}"))?;
let state = sim
.registry_mut()
.intern(descriptor)
.map_err(|e| format!("simulate=true: interning {descriptor}: {e:?}"))?;
let pos = Pos::new(x - offset.0, y - offset.1, z - offset.2);
let placed_bounds = structure.bounds(4);
if pos.x < placed_bounds.min.x
|| pos.x > placed_bounds.max.x
|| pos.y < placed_bounds.min.y
|| pos.y > placed_bounds.max.y
|| pos.z < placed_bounds.min.z
|| pos.z > placed_bounds.max.z
{
return Err("simulate=true: position outside the simulated bounds".to_string());
}
sim.record();
sim.place_block_by_hand(pos, state);
sim.run_until_quiescent(255);
schematic.set_block_from_string(x, y, z, descriptor)?;
let mut finals: std::collections::HashMap<Pos, mc_tick::StateId> =
std::collections::HashMap::new();
for change in sim.recorded() {
finals.insert(change.pos, change.to);
}
let mut written = 1;
for (cell, state) in finals {
let descriptor = sim
.registry()
.descriptor(state)
.ok_or_else(|| "simulate=true: a written state with no descriptor".to_string())?;
schematic.set_block_from_string(
cell.x + offset.0,
cell.y + offset.1,
cell.z + offset.2,
descriptor,
)?;
written += 1;
}
Ok(written)
}
pub(crate) fn simulate_placements_into_world(
schematic: &mut crate::UniversalSchematic,
positions: &[(i32, i32, i32)],
descriptor: &str,
) -> Result<usize, String> {
use mc_tick::Pos;
use std::collections::HashMap;
if positions.is_empty() {
return Ok(0);
}
if positions.len() == 1 {
let (x, y, z) = positions[0];
return simulate_placement_into_world(schematic, x, y, z, descriptor);
}
let bb = schematic.get_bounding_box();
let mut min = bb.min;
let mut max = bb.max;
for &(x, y, z) in positions {
min.0 = min.0.min(x);
min.1 = min.1.min(y);
min.2 = min.2.min(z);
max.0 = max.0.max(x);
max.1 = max.1.max(y);
max.2 = max.2.max(z);
}
let dimensions = (
i64::from(max.0) - i64::from(min.0) + 1,
i64::from(max.1) - i64::from(min.1) + 1,
i64::from(max.2) - i64::from(min.2) + 1,
);
let volume = dimensions
.0
.checked_mul(dimensions.1)
.and_then(|xy| xy.checked_mul(dimensions.2))
.unwrap_or(i64::MAX);
if volume > MAX_VOLUME as i64 {
return Err(format!(
"simulated placement span is {} x {} x {} = {volume} cells, over the \
{MAX_VOLUME}-cell limit",
dimensions.0, dimensions.1, dimensions.2
));
}
let offset = bb.min;
let snbt = to_gametest_snbt(schematic);
let structure = mc_tick::Structure::parse(&snbt)
.map_err(|e| format!("simulated batch could not load this schematic: {e:?}"))?;
let mut sim = wire_simulation(
&structure,
Pos::new(0, 0, 0),
ffi::TickSettleMode::InWorld,
&[descriptor],
schematic.metadata.source_data_version,
)
.map_err(|e| format!("simulated batch could not simulate this schematic: {e}"))?;
let state = sim
.registry()
.get(descriptor)
.ok_or_else(|| format!("simulated batch did not intern `{descriptor}`"))?;
sim.clear_recorded();
let mut placed = Vec::with_capacity(positions.len());
for &(x, y, z) in positions {
let pos = Pos::new(
x.checked_sub(offset.0)
.ok_or("simulated x coordinate overflow")?,
y.checked_sub(offset.1)
.ok_or("simulated y coordinate overflow")?,
z.checked_sub(offset.2)
.ok_or("simulated z coordinate overflow")?,
);
sim.place_block_by_hand(pos, state);
sim.run_until_quiescent(255);
placed.push(pos);
}
let mut finals: HashMap<Pos, mc_tick::StateId> = HashMap::new();
for change in sim.recorded() {
finals.insert(change.pos, change.to);
}
for pos in placed {
finals.insert(pos, sim.world().get(pos));
}
let written = finals.len();
for (cell, state) in finals {
let block = sim
.registry()
.descriptor(state)
.ok_or_else(|| "simulated batch produced a state with no descriptor".to_string())?;
let x = cell
.x
.checked_add(offset.0)
.ok_or("simulated write-back x overflow")?;
let y = cell
.y
.checked_add(offset.1)
.ok_or("simulated write-back y overflow")?;
let z = cell
.z
.checked_add(offset.2)
.ok_or("simulated write-back z overflow")?;
schematic.set_block_from_string(x, y, z, block)?;
}
Ok(written)
}
fn needs_block_entity(name: &str) -> bool {
let short = name.strip_prefix("minecraft:").unwrap_or(name);
matches!(
short,
"comparator"
| "chest"
| "trapped_chest"
| "barrel"
| "hopper"
| "dropper"
| "dispenser"
| "furnace"
| "blast_furnace"
| "smoker"
| "brewing_stand"
| "crafter"
| "chiseled_bookshelf"
| "jukebox"
| "lectern"
| "decorated_pot"
) || short.ends_with("shulker_box")
}
fn block_entity_audit(schematic: &crate::UniversalSchematic) -> String {
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
let have: HashSet<(i32, i32, i32)> = schematic
.get_block_entities_as_list()
.into_iter()
.map(|be| be.position)
.collect();
let mut missing: HashMap<String, u32> = HashMap::new();
for (pos, state) in schematic.iter_blocks() {
if !needs_block_entity(&state.name) {
continue;
}
if !have.contains(&(pos.x, pos.y, pos.z)) {
*missing.entry(state.name.to_string()).or_default() += 1;
}
}
let mut rows: Vec<(String, u32)> = missing.into_iter().collect();
rows.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
let total: u32 = rows.iter().map(|(_, n)| *n).sum();
let mut json = String::from("{\"present\":");
let _ = write!(json, "{}", have.len());
let _ = write!(json, ",\"missing_total\":{total},\"missing\":[");
for (i, (name, count)) in rows.iter().enumerate() {
if i > 0 {
json.push(',');
}
let _ = write!(json, "{{\"name\":\"{name}\",\"count\":{count}}}");
}
json.push_str("],\"summary\":\"");
if total > 0 {
let named: Vec<String> = rows
.iter()
.take(3)
.map(|(name, count)| {
let short = name.strip_prefix("minecraft:").unwrap_or(name);
let plural = if *count == 1 { "" } else { "s" };
format!("{count} {short}{plural}")
})
.collect();
let more = if rows.len() > 3 { ", and others" } else { "" };
let _ = write!(
json,
"This schematic contains {}{} with no block-entity data. \
Comparator outputs and container contents are simulated as empty, \
so results may not reflect the original build.",
named.join(", "),
more
);
}
json.push_str("\"}");
json
}
fn structure_parse_detail(error: &mc_tick::structure::StructureError, converted: bool) -> String {
if let mc_tick::structure::StructureError::UnsupportedEntity { entity_type, .. } = error {
return format!(
"this build contains a `{entity_type}` entity, which the engine cannot simulate \
yet — loading it would mean dropping the entity, and a run without it would not \
match the real build"
);
}
if converted {
format!(
"converted structure did not parse: {error:?} — this is an engine fault, \
not a problem with the uploaded file"
)
} else {
format!("structure SNBT did not parse: {error:?}")
}
}
fn updates_json_range(sim: &mc_tick::Simulation, from: u64, to: u64) -> String {
use std::fmt::Write as _;
let mut json = String::from("[");
let mut first = true;
for update in sim.recorded_updates() {
if update.tick < from || update.tick >= to {
continue;
}
if !first {
json.push(',');
}
first = false;
let state = sim
.registry()
.descriptor(update.state)
.unwrap_or("minecraft:air");
let kind = match update.kind {
mc_tick::UpdateKind::Neighbor => "neighbor",
mc_tick::UpdateKind::Shape => "shape",
};
let phase = update.phase.map_or("boundary", |p| p.name());
let _ = write!(
json,
"{{\"tick\":{},\"seq\":{},\"pos\":[{},{},{}],\"from\":\"{:?}\",\"kind\":\"{}\",\"phase\":\"{}\",\"state\":\"{}\"}}",
update.tick,
update.seq,
update.pos.x,
update.pos.y,
update.pos.z,
update.from,
kind,
phase,
state
);
}
json.push(']');
json
}
fn cycle_json(cycle: Option<mc_tick::Cycle>) -> String {
match cycle {
None => "null".to_string(),
Some(c) => format!(
"{{\"start\":{},\"end\":{},\"period\":{},\"drift\":[{},{},{}]}}",
c.start_tick, c.end_tick, c.period, c.drift.x, c.drift.y, c.drift.z
),
}
}
fn phase_legend() -> Vec<&'static str> {
let mut names = vec!["boundary"];
names.extend(mc_tick::PHASE_ORDER.iter().map(|p| p.name()));
names
}
fn phase_code(update: &mc_tick::UpdateRecord) -> usize {
match update.phase {
None => 0,
Some(phase) => mc_tick::PHASE_ORDER
.iter()
.position(|p| *p == phase)
.map_or(0, |i| i + 1),
}
}
fn dir_code(dir: mc_tick::Dir) -> usize {
mc_tick::ALL_DIRS
.iter()
.position(|d| *d == dir)
.unwrap_or(0)
}
fn updates_heat_range(sim: &mc_tick::Simulation, from: u64, to: u64) -> String {
use std::collections::BTreeMap;
use std::fmt::Write as _;
let phases = phase_legend();
let mut per_tick: BTreeMap<u64, BTreeMap<(i32, i32, i32), (u32, u32, u32, Vec<u32>)>> =
BTreeMap::new();
for update in sim.recorded_updates() {
if update.tick < from || update.tick >= to {
continue;
}
let cells = per_tick.entry(update.tick).or_default();
let cell = cells
.entry((update.pos.x, update.pos.y, update.pos.z))
.or_insert_with(|| (0, 0, 0, vec![0; phases.len()]));
cell.0 += 1;
match update.kind {
mc_tick::UpdateKind::Neighbor => cell.1 += 1,
mc_tick::UpdateKind::Shape => cell.2 += 1,
}
cell.3[phase_code(update)] += 1;
}
let mut json = String::from("{\"phases\":[");
for (i, name) in phases.iter().enumerate() {
let _ = write!(json, "{}\"{name}\"", if i > 0 { "," } else { "" });
}
json.push_str("],\"ticks\":[");
for (i, (tick, cells)) in per_tick.iter().enumerate() {
if i > 0 {
json.push(',');
}
let total: u32 = cells.values().map(|c| c.0).sum();
let _ = write!(json, "{{\"tick\":{tick},\"total\":{total},\"cells\":[");
for (j, ((x, y, z), (n, nb, sh, ph))) in cells.iter().enumerate() {
if j > 0 {
json.push(',');
}
let _ = write!(
json,
"{{\"p\":[{x},{y},{z}],\"n\":{n},\"nb\":{nb},\"sh\":{sh},\"ph\":["
);
for (k, count) in ph.iter().enumerate() {
let _ = write!(json, "{}{count}", if k > 0 { "," } else { "" });
}
json.push_str("]}");
}
json.push_str("]}");
}
json.push_str("]}");
json
}
fn updates_wave(sim: &mc_tick::Simulation, tick: u64) -> String {
use std::collections::HashMap;
use std::fmt::Write as _;
let mut pos = String::new();
let mut kinds = String::new();
let mut phases_arr = String::new();
let mut froms = String::new();
let mut states_arr = String::new();
let mut table: Vec<&str> = Vec::new();
let mut seen: HashMap<mc_tick::StateId, usize> = HashMap::new();
let mut n = 0usize;
for update in sim.recorded_updates() {
if update.tick != tick {
continue;
}
let sep = if n > 0 { "," } else { "" };
let _ = write!(
pos,
"{sep}{},{},{}",
update.pos.x, update.pos.y, update.pos.z
);
let _ = write!(
kinds,
"{sep}{}",
match update.kind {
mc_tick::UpdateKind::Neighbor => 0,
mc_tick::UpdateKind::Shape => 1,
}
);
let _ = write!(phases_arr, "{sep}{}", phase_code(update));
let _ = write!(froms, "{sep}{}", dir_code(update.from));
let index = *seen.entry(update.state).or_insert_with(|| {
table.push(
sim.registry()
.descriptor(update.state)
.unwrap_or("minecraft:air"),
);
table.len() - 1
});
let _ = write!(states_arr, "{sep}{index}");
n += 1;
}
let mut json = String::new();
let _ = write!(
json,
"{{\"tick\":{tick},\"n\":{n},\"pos\":[{pos}],\"kind\":[{kinds}],"
);
let _ = write!(
json,
"\"phase\":[{phases_arr}],\"from\":[{froms}],\"state\":[{states_arr}],"
);
json.push_str("\"states\":[");
for (i, descriptor) in table.iter().enumerate() {
let _ = write!(json, "{}\"{descriptor}\"", if i > 0 { "," } else { "" });
}
json.push_str("],\"phases\":[");
for (i, name) in phase_legend().iter().enumerate() {
let _ = write!(json, "{}\"{name}\"", if i > 0 { "," } else { "" });
}
json.push_str("],\"dirs\":[");
for (i, dir) in mc_tick::ALL_DIRS.iter().enumerate() {
let _ = write!(json, "{}\"{dir:?}\"", if i > 0 { "," } else { "" });
}
json.push_str("],\"kinds\":[\"neighbor\",\"shape\"]}");
json
}
const MAX_VOLUME: usize = 8_000_000;
fn set_last_error(detail: impl Into<String>) {
crate::bridge::set_last_error_detail(detail);
}
fn clear_last_error() {
crate::bridge::clear_last_error_detail();
}
fn check_volume(size: (i32, i32, i32)) -> Result<(), String> {
let volume = (size.0 as i64) * (size.1 as i64) * (size.2 as i64);
if volume > MAX_VOLUME as i64 {
return Err(format!(
"build is {} x {} x {} = {volume} cells, over the {MAX_VOLUME}-cell limit — \
this looks like a saved world rather than a contraption",
size.0, size.1, size.2
));
}
Ok(())
}
pub(crate) fn wire_simulation(
structure: &mc_tick::Structure,
hash_origin: mc_tick::Pos,
settle: ffi::TickSettleMode,
extra_states: &[&str],
source_data_version: Option<i32>,
) -> Result<mc_tick::Simulation, String> {
use mc_tick::{Pos, Simulation};
const MARGIN: i32 = 4;
let mut sim = Simulation::new(structure.bounds(MARGIN));
{
let (registry, world) = sim.registry_and_world_mut();
structure.place(world, registry, Pos::new(0, 0, 0));
}
if let Some(version) = source_data_version {
sim.set_motion_semantics(mc_tick::MotionSemantics::for_data_version(version));
}
let mut wanted: Vec<String> = vec!["minecraft:redstone_block".to_string()];
wanted.extend(extra_states.iter().map(|s| s.to_string()));
for (_, stacks) in &structure.inventories {
for stack in stacks {
wanted.extend(mc_tick::vanilla::dispensable_states(&stack.id));
}
}
for descriptor in &wanted {
sim.registry_mut()
.intern(descriptor)
.map_err(|e| format!("interning {descriptor}: {e:?}"))?;
}
for pos in &structure.block_entities {
sim.mark_block_entity(*pos);
}
for (pos, strength) in &structure.comparator_outputs {
sim.set_comparator_output(*pos, *strength);
}
for (pos, stacks) in &structure.inventories {
let entry = structure
.blocks
.iter()
.find(|(p, _)| p == pos)
.map(|(_, e)| *e)
.ok_or_else(|| format!("inventory at {pos:?} with no block"))?;
let name = structure.palette[entry]
.split('[')
.next()
.unwrap_or_default()
.to_string();
let slots = mc_tick::vanilla::container_slots(&name)
.ok_or_else(|| format!("{name} has an inventory but no slot count"))?;
sim.set_inventory(
*pos,
mc_tick::Inventory {
slots,
stacks: stacks.clone(),
blocked_slots: structure.blocked_slots_at(*pos),
},
);
}
mc_tick::intern_companions(sim.registry_mut());
{
let mut table = std::mem::take(sim.behaviours_mut());
mc_tick::register_all_at(sim.registry_mut(), &mut table, hash_origin);
*sim.behaviours_mut() = table;
}
if let Some(report) = sim.unknown_report() {
return Err(format!("blocks without behaviour: {report}"));
}
{
let (solidity, frictions, heights, webs) = mc_tick::vanilla::physics_tables(sim.registry());
sim.set_physics_tables(solidity, frictions, heights, webs);
let (water_kinds, bubble_kinds) = mc_tick::vanilla::fluid_tables(sim.registry());
sim.set_fluid_tables(water_kinds, bubble_kinds);
let (rails, conductors) = mc_tick::vanilla::rail_tables(sim.registry());
sim.set_rail_tables(rails, conductors);
}
let mut refused: Vec<String> = Vec::new();
for spawned in &structure.entities {
match spawned {
mc_tick::structure::SpawnedEntity::Item(item) => {
sim.spawn_item(item.item.clone(), item.pos, item.motion, item.pickup_delay);
}
mc_tick::structure::SpawnedEntity::Minecart(cart) => {
let vehicle = sim.spawn_authored_minecart(cart, None);
for rider in &cart.passengers {
if let Err(why) = sim.spawn_authored_rider(vehicle, rider) {
refused.push(why);
}
}
}
mc_tick::structure::SpawnedEntity::FurnaceMinecart(cart) => {
if let Err(why) = sim.spawn_authored_furnace_minecart(cart, None) {
refused.push(why);
}
}
mc_tick::structure::SpawnedEntity::Body(body) => match sim.spawn_authored_body(body) {
Ok(vehicle) => {
for rider in &body.passengers {
if let Err(why) = sim.spawn_authored_rider(vehicle, rider) {
refused.push(why);
}
}
}
Err(why) => {
refused.push(why);
}
},
}
}
if !refused.is_empty() {
return Err(format!(
"{} entit{} in this build need behaviour that is not implemented, and the \
build is refused rather than simulated with them standing still:\n - {}",
refused.len(),
if refused.len() == 1 { "y" } else { "ies" },
refused.join("\n - ")
));
}
for (pos, entry) in &structure.blocks {
let state = sim.registry().get(&structure.palette[*entry]);
let is_ticker = state
.and_then(|s| sim.behaviours().get(s))
.is_some_and(|b| b.ticks_as_block_entity());
if is_ticker {
sim.add_block_entity_ticker(*pos);
}
}
let order = structure.placement_order(
mc_tick::vanilla::is_collision_full_cube,
mc_tick::vanilla::has_dynamic_shape,
);
if settle != ffi::TickSettleMode::InWorld {
sim.place_on_place(&order);
}
if settle == ffi::TickSettleMode::Placement {
sim.settle_with_order(&order);
}
sim.record();
Ok(sim)
}
fn is_named(descriptor: &str, needle: &str) -> bool {
descriptor
.split('[')
.next()
.unwrap_or(descriptor)
.contains(needle)
}
fn non_air_stats(sim: &mc_tick::Simulation) -> (u32, f64, i32, i32) {
let mut n = 0u32;
let mut sum = 0.0;
let mut min = i32::MAX;
let mut max = i32::MIN;
for (pos, _) in sim.world().iter_non_air() {
n += 1;
sum += f64::from(pos.x);
if pos.x < min {
min = pos.x;
}
if pos.x > max {
max = pos.x;
}
}
(
n,
if n == 0 { f64::NAN } else { sum / f64::from(n) },
min,
max,
)
}
fn structure_from_blocks(
bx: i32,
by: i32,
bz: i32,
travel: i32,
x_off: i32,
palette: &[String],
cells: &[u16],
air: u16,
) -> Result<mc_tick::Structure, String> {
let volume = (bx.max(0) as usize) * (by.max(0) as usize) * (bz.max(0) as usize);
if cells.len() != volume {
return Err(format!("cells len {} != bbox volume {volume}", cells.len()));
}
let mut blocks = Vec::new();
let mut i = 0usize;
for _y in 0..by {
for _z in 0..bz {
for _x in 0..bx {
let s = cells[i];
let (x, y, z) = (_x, _y, _z);
i += 1;
if s == air {
continue;
}
if s as usize >= palette.len() {
return Err(format!("palette index {s} out of range"));
}
blocks.push((mc_tick::Pos::new(x + x_off, y, z), s as usize));
}
}
}
Ok(mc_tick::Structure {
data_version: None,
size: (bx + travel, by + 2, bz + 2),
palette: palette.to_vec(),
blocks,
inventories: Vec::new(),
inventory_blocked_slots: Vec::new(),
comparator_outputs: Vec::new(),
block_entities: Vec::new(),
entities: Vec::new(),
item_entities: Vec::new(),
commands: Vec::new(),
})
}
fn modal_gap(gaps: &[u32]) -> u32 {
if gaps.len() < 3 {
return 0;
}
let mut order: Vec<u32> = Vec::new();
let mut counts: Vec<u32> = Vec::new();
for &g in gaps {
match order.iter().position(|&o| o == g) {
Some(i) => counts[i] += 1,
None => {
order.push(g);
counts.push(1);
}
}
}
let mut best = 0u32;
let mut best_gap: Option<u32> = None;
for (i, &g) in order.iter().enumerate() {
let n = counts[i];
if n > best || (n == best && best_gap.is_some_and(|b| g < b)) {
best = n;
best_gap = Some(g);
}
}
match best_gap {
Some(g) if (best as f64) / (gaps.len() as f64) >= 0.6 => g,
_ => 0,
}
}
#[allow(clippy::too_many_arguments)]
fn fly_metrics(
structure: &mc_tick::Structure,
extras: &[&str],
kick: (i32, i32, i32),
eval_ticks: u32,
seed: i64,
must_move_by_tick: i32,
need_period: bool,
early_exit: bool,
) -> Result<[f64; 11], String> {
let mut sim = wire_simulation(
structure,
mc_tick::Pos::new(0, 0, 0),
ffi::TickSettleMode::Quiet,
extras,
None,
)?;
fly_on(
&mut sim,
kick,
eval_ticks,
seed,
must_move_by_tick,
need_period,
early_exit,
)
}
#[allow(clippy::too_many_arguments)]
fn fly_on(
sim: &mut mc_tick::Simulation,
kick: (i32, i32, i32),
eval_ticks: u32,
seed: i64,
must_move_by_tick: i32,
need_period: bool,
early_exit: bool,
) -> Result<[f64; 11], String> {
const PERIOD_WINDOW: u32 = 120;
const EARLY_TICK: u32 = 40;
sim.set_rng_seed(seed);
let (n0, start_com, start_min, start_max) = non_air_stats(sim);
let mut row = [f64::NAN; 11];
row[0] = f64::from(n0);
row[1] = start_com;
row[2] = f64::from(start_min);
row[3] = f64::from(start_max);
if n0 == 0 {
return Ok(row); }
let redstone = sim
.registry()
.get("minecraft:redstone_block")
.ok_or("redstone_block not interned")?;
let kick_pos = mc_tick::Pos::new(kick.0, kick.1, kick.2);
sim.run(2);
sim.place_block(kick_pos, redstone);
sim.run(2);
sim.place_block(kick_pos, mc_tick::StateId::AIR);
let mut elapsed: u32 = 4;
let mid_tick = eval_ticks.min((eval_ticks / 2).max(elapsed));
let move_check: Option<u32> = if must_move_by_tick >= 0 {
Some((must_move_by_tick as u32).max(elapsed).min(eval_ticks))
} else {
None
};
let mut probes: Vec<u32> = Vec::new();
if let Some(mc) = move_check {
probes.push(mc);
}
probes.push(mid_tick);
if early_exit && eval_ticks > EARLY_TICK {
probes.push(EARLY_TICK.max(elapsed));
}
probes.sort_unstable();
probes.dedup();
let mut com_mid = start_com;
let mut com_move = f64::NAN;
let mut frozen = false;
for &t in &probes {
if t > elapsed {
sim.run(u64::from(t - elapsed));
elapsed = t;
}
let (_, com, _, _) = non_air_stats(&sim);
if Some(t) == move_check {
com_move = com;
}
if t == mid_tick {
com_mid = com;
}
if early_exit && t == EARLY_TICK && sim.is_quiescent() && (com - start_com).abs() < 0.25 {
frozen = true;
if mid_tick > t {
com_mid = com;
}
if let Some(mc) = move_check {
if mc > t && com_move.is_nan() {
com_move = com;
}
}
break;
}
}
let mut period = 0u32;
if need_period && !frozen {
let win_start = elapsed.max(eval_ticks.saturating_sub(PERIOD_WINDOW));
if win_start > elapsed {
sim.run(u64::from(win_start - elapsed));
elapsed = win_start;
}
let mut gaps: Vec<u32> = Vec::new();
let (_, _, mut prev_min, _) = non_air_stats(&sim);
let mut last_rise: i64 = -1;
while elapsed < eval_ticks {
sim.run(1);
elapsed += 1;
let (_, _, mx, _) = non_air_stats(&sim);
if mx > prev_min {
if last_rise >= 0 {
gaps.push(elapsed - last_rise as u32);
}
last_rise = i64::from(elapsed);
}
prev_min = mx;
}
period = modal_gap(&gaps);
}
if !frozen && eval_ticks > elapsed {
sim.run(u64::from(eval_ticks - elapsed));
}
let (n1, end_com, end_min, end_max) = non_air_stats(&sim);
row[4] = com_move;
row[5] = com_mid;
row[6] = f64::from(period);
row[7] = f64::from(n1);
row[8] = end_com;
row[9] = f64::from(end_min);
row[10] = f64::from(end_max);
Ok(row)
}
#[diplomat::bridge]
pub mod ffi {
use super::super::schematic::ffi::Schematic;
use super::super::shared::ffi::NucleationError;
use diplomat_runtime::{DiplomatStr, DiplomatWrite};
use std::fmt::Write;
#[derive(PartialEq, Eq)]
pub enum TickSettleMode {
Placement,
Quiet,
InWorld,
}
#[diplomat::opaque_mut]
pub struct TickSimulation {
pub(crate) sim: mc_tick::Simulation,
pub(crate) checkpoints: Vec<mc_tick::sim::Checkpoint>,
pub(crate) stopped_timeline: Option<mc_tick::timeline::RunTimeline>,
}
impl TickSimulation {
pub fn last_error_detail(out: &mut DiplomatWrite) {
let _ = write!(out, "{}", crate::bridge::last_error_detail());
}
pub fn max_volume() -> u32 {
super::MAX_VOLUME as u32
}
pub fn from_snbt(
snbt: &DiplomatStr,
settle: TickSettleMode,
origin_x: i32,
origin_y: i32,
origin_z: i32,
extra_states: &DiplomatStr,
) -> Result<Box<TickSimulation>, NucleationError> {
super::clear_last_error();
let snbt = std::str::from_utf8(snbt).map_err(|_| {
super::set_last_error("snbt is not valid UTF-8");
NucleationError::InvalidArgument
})?;
let extra = std::str::from_utf8(extra_states).map_err(|_| {
super::set_last_error("extra_states is not valid UTF-8");
NucleationError::InvalidArgument
})?;
let structure = mc_tick::Structure::parse(snbt).map_err(|e| {
super::set_last_error(super::structure_parse_detail(&e, false));
if matches!(
e,
mc_tick::structure::StructureError::UnsupportedEntity { .. }
) {
NucleationError::Simulation
} else {
NucleationError::Parse
}
})?;
super::check_volume(structure.size).map_err(|e| {
super::set_last_error(e);
NucleationError::InvalidArgument
})?;
let extras: Vec<&str> = extra
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
let sim = super::wire_simulation(
&structure,
mc_tick::Pos::new(origin_x, origin_y, origin_z),
settle,
&extras,
structure.data_version,
)
.map_err(|e| {
super::set_last_error(e);
NucleationError::Simulation
})?;
Ok(Box::new(TickSimulation {
sim,
checkpoints: Vec::new(),
stopped_timeline: None,
}))
}
pub fn from_schematic(
schematic: &Schematic,
settle: TickSettleMode,
origin_x: i32,
origin_y: i32,
origin_z: i32,
extra_states: &DiplomatStr,
) -> Result<Box<TickSimulation>, NucleationError> {
super::clear_last_error();
let extra = std::str::from_utf8(extra_states).map_err(|_| {
super::set_last_error("extra_states is not valid UTF-8");
NucleationError::InvalidArgument
})?;
let bb = schematic.0.get_bounding_box();
super::check_volume((
bb.max.0 - bb.min.0 + 1,
bb.max.1 - bb.min.1 + 1,
bb.max.2 - bb.min.2 + 1,
))
.map_err(|e| {
super::set_last_error(e);
NucleationError::InvalidArgument
})?;
let snbt = super::to_gametest_snbt(&schematic.0);
let structure = mc_tick::Structure::parse(&snbt).map_err(|e| {
super::set_last_error(super::structure_parse_detail(&e, true));
NucleationError::Simulation
})?;
let extras: Vec<&str> = extra
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
let sim = super::wire_simulation(
&structure,
mc_tick::Pos::new(origin_x, origin_y, origin_z),
settle,
&extras,
schematic.0.metadata.source_data_version,
)
.map_err(|e| {
super::set_last_error(e);
NucleationError::Simulation
})?;
Ok(Box::new(TickSimulation {
sim,
checkpoints: Vec::new(),
stopped_timeline: None,
}))
}
#[allow(clippy::too_many_arguments)]
pub fn from_blocks(
bx: i32,
by: i32,
bz: i32,
travel: i32,
x_off: i32,
palette: &DiplomatStr,
cells: &[u16],
air_index: u16,
settle: TickSettleMode,
origin_x: i32,
origin_y: i32,
origin_z: i32,
) -> Result<Box<TickSimulation>, NucleationError> {
super::clear_last_error();
let palette = std::str::from_utf8(palette).map_err(|_| {
super::set_last_error("palette is not valid UTF-8");
NucleationError::InvalidArgument
})?;
let pal: Vec<String> = palette
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let structure =
super::structure_from_blocks(bx, by, bz, travel, x_off, &pal, cells, air_index)
.map_err(|e| {
super::set_last_error(e);
NucleationError::InvalidArgument
})?;
let extras: Vec<&str> = pal.iter().map(String::as_str).collect();
let sim = super::wire_simulation(
&structure,
mc_tick::Pos::new(origin_x, origin_y, origin_z),
settle,
&extras,
None,
)
.map_err(|e| {
super::set_last_error(e);
NucleationError::Simulation
})?;
Ok(Box::new(TickSimulation {
sim,
checkpoints: Vec::new(),
stopped_timeline: None,
}))
}
#[allow(clippy::too_many_arguments)]
pub fn eval_flight_batch(
bx: i32,
by: i32,
bz: i32,
travel: i32,
x_off: i32,
palette: &DiplomatStr,
cells: &[u16],
air_index: u16,
kicks: &[i32],
eval_ticks: u32,
seed: i64,
must_move_by_tick: i32,
need_period: bool,
early_exit: bool,
out: &mut DiplomatWrite,
) -> Result<(), NucleationError> {
let palette =
std::str::from_utf8(palette).map_err(|_| NucleationError::InvalidArgument)?;
let pal: Vec<String> = palette
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let extras: Vec<&str> = pal.iter().map(String::as_str).collect();
let volume = (bx.max(0) as usize) * (by.max(0) as usize) * (bz.max(0) as usize);
if volume == 0 || cells.len() % volume != 0 || kicks.len() != (cells.len() / volume) * 3
{
return Err(NucleationError::InvalidArgument);
}
let n_genomes = cells.len() / volume;
let empty = vec![air_index; volume];
let empty_structure =
super::structure_from_blocks(bx, by, bz, travel, x_off, &pal, &empty, air_index)
.map_err(|_| NucleationError::InvalidArgument)?;
let mut sim = super::wire_simulation(
&empty_structure,
mc_tick::Pos::new(0, 0, 0),
TickSettleMode::Quiet,
&extras,
None,
)
.map_err(|_| NucleationError::Simulation)?;
let pristine = sim.checkpoint();
let mut json = String::from("[");
for g in 0..n_genomes {
let slice = &cells[g * volume..(g + 1) * volume];
let structure =
super::structure_from_blocks(bx, by, bz, travel, x_off, &pal, slice, air_index)
.map_err(|_| NucleationError::InvalidArgument)?;
sim.restore(&pristine);
{
let (registry, world) = sim.registry_and_world_mut();
structure.place(world, registry, mc_tick::Pos::new(0, 0, 0));
}
let order = structure.placement_order(
mc_tick::vanilla::is_collision_full_cube,
mc_tick::vanilla::has_dynamic_shape,
);
sim.place_on_place(&order);
sim.record();
let kick = (kicks[g * 3], kicks[g * 3 + 1], kicks[g * 3 + 2]);
let row = super::fly_on(
&mut sim,
kick,
eval_ticks,
seed,
must_move_by_tick,
need_period,
early_exit,
)
.map_err(|_| NucleationError::Simulation)?;
if g > 0 {
json.push(',');
}
json.push('[');
for (i, v) in row.iter().enumerate() {
if i > 0 {
json.push(',');
}
if v.is_nan() {
json.push_str("null");
} else {
let _ = write!(json, "{v:?}");
}
}
json.push(']');
}
json.push(']');
let _ = write!(out, "{json}");
Ok(())
}
pub fn set_rng_seed(&mut self, seed: i64) {
self.sim.set_rng_seed(seed);
}
pub fn step(&mut self) {
self.sim.step();
}
pub fn run(&mut self, ticks: u32) {
self.sim.run(u64::from(ticks));
}
pub fn run_until_quiescent(&mut self, budget: u32) -> bool {
self.sim.run_until_quiescent(u64::from(budget));
self.sim.is_quiescent()
}
pub fn tick_count(&self) -> u32 {
self.sim.tick_count() as u32
}
pub fn is_quiescent(&self) -> bool {
self.sim.is_quiescent()
}
pub fn use_block(&mut self, x: i32, y: i32, z: i32) {
self.sim.use_block(mc_tick::Pos::new(x, y, z));
}
pub fn place_block(
&mut self,
x: i32,
y: i32,
z: i32,
state: &DiplomatStr,
) -> Result<(), NucleationError> {
let state = std::str::from_utf8(state).map_err(|_| NucleationError::InvalidArgument)?;
let id = self
.sim
.registry()
.get(state)
.ok_or(NucleationError::NotFound)?;
self.sim.place_block(mc_tick::Pos::new(x, y, z), id);
Ok(())
}
pub fn get_block(&self, x: i32, y: i32, z: i32, out: &mut DiplomatWrite) {
let id = self.sim.world().get(mc_tick::Pos::new(x, y, z));
let descriptor = self
.sim
.registry()
.descriptor(id)
.unwrap_or("minecraft:air");
let _ = write!(out, "{descriptor}");
}
pub fn read_probes(
&self,
positions_json: &DiplomatStr,
out: &mut DiplomatWrite,
) -> Result<(), NucleationError> {
let positions: Vec<[i32; 3]> = serde_json::from_slice(positions_json).map_err(|e| {
crate::bridge::set_last_error_detail(format!(
"positions_json must be [[x,y,z], ...]: {e}"
));
NucleationError::InvalidArgument
})?;
let states: Vec<&str> = positions
.iter()
.map(|&[x, y, z]| {
let id = self.sim.world().get(mc_tick::Pos::new(x, y, z));
self.sim
.registry()
.descriptor(id)
.unwrap_or("minecraft:air")
})
.collect();
let json = serde_json::to_string(&states).map_err(|_| NucleationError::Serialize)?;
let _ = write!(out, "{json}");
Ok(())
}
pub fn conduction_trace(&self, x: i32, y: i32, z: i32, out: &mut DiplomatWrite) {
let _ = write!(
out,
"{}",
super::conduction_trace_json(&self.sim, mc_tick::Pos::new(x, y, z))
);
}
pub fn bake_to(&self, schematic: &mut Schematic) -> u32 {
super::bake_into(&self.sim, &mut schematic.0)
}
pub fn checkpoint(&mut self) -> u32 {
self.checkpoints.push(self.sim.checkpoint());
(self.checkpoints.len() - 1) as u32
}
pub fn restore(&mut self, id: u32) -> Result<(), NucleationError> {
let checkpoint = self
.checkpoints
.get(id as usize)
.ok_or(NucleationError::NotFound)?;
self.sim.restore(checkpoint);
Ok(())
}
pub fn gametest_snbt(schematic: &Schematic, out: &mut DiplomatWrite) {
let _ = write!(out, "{}", super::to_gametest_snbt(&schematic.0));
}
pub fn block_entity_audit_json(schematic: &Schematic, out: &mut DiplomatWrite) {
let _ = write!(out, "{}", super::block_entity_audit(&schematic.0));
}
pub fn record_updates(&mut self, on: bool) {
self.sim.record_updates(on);
}
pub fn clear_updates(&mut self) {
self.sim.clear_updates();
}
pub fn record_timeline(&mut self) {
self.stopped_timeline = None;
self.sim.record_timeline();
}
pub fn stop_timeline(&mut self) {
if let Some(timeline) = self.sim.stop_timeline() {
self.stopped_timeline = Some(timeline);
}
}
fn timeline(&self) -> Option<mc_tick::timeline::TimelineView<'_>> {
self.sim.timeline_view().or_else(|| {
self.stopped_timeline
.as_ref()
.map(mc_tick::timeline::TimelineView::of)
})
}
pub fn timeline_activity_json(&self, out: &mut DiplomatWrite) {
let Some(timeline) = self.timeline() else {
let _ = write!(out, "{{\"start\":0,\"end\":0,\"ticks\":[]}}");
return;
};
let mut active: std::collections::BTreeMap<u64, [u32; 3]> =
std::collections::BTreeMap::new();
for change in timeline.changes {
active.entry(change.tick).or_default()[0] += 1;
}
for input in timeline.inputs {
active.entry(input.tick()).or_default()[1] += 1;
}
for piston in timeline.pistons {
active.entry(piston.tick).or_default()[2] += 1;
}
let mut json = format!(
"{{\"start\":{},\"end\":{},\"ticks\":[",
timeline.start_tick, timeline.end_tick
);
for (i, (tick, counts)) in active.iter().enumerate() {
if i > 0 {
json.push(',');
}
let _ = write!(
json,
"{{\"tick\":{},\"changes\":{},\"inputs\":{},\"pistons\":{}}}",
tick, counts[0], counts[1], counts[2]
);
}
json.push_str("]}");
let _ = write!(out, "{json}");
}
pub fn timeline_cycles_json(&self, out: &mut DiplomatWrite) {
let Some(view) = self.timeline() else {
let _ = write!(out, "{{\"exact\":null,\"translated\":null}}");
return;
};
let timeline = view.to_timeline();
let report = timeline.detect_cycles(self.sim.registry());
let _ = write!(
out,
"{{\"exact\":{},\"translated\":{}}}",
super::cycle_json(report.exact),
super::cycle_json(report.translated)
);
}
pub fn animation_timeline_json(
&self,
start_tick: u32,
end_tick: u32,
tick_ms: f32,
out: &mut DiplomatWrite,
) -> Result<(), NucleationError> {
let Some(view) = self.timeline() else {
super::set_last_error("no timeline has been recorded on this simulation");
return Err(NucleationError::NotFound);
};
let timeline = view.to_timeline();
let selection = timeline
.select_ticks(u64::from(start_tick), u64::from(end_tick))
.map_err(|e| {
super::set_last_error(e.to_string());
NucleationError::InvalidArgument
})?;
let (json, _warnings) = crate::tick_timeline::mesher_timeline_json(
&timeline,
selection,
self.sim.registry(),
tick_ms,
)
.map_err(|e| {
super::set_last_error(e);
NucleationError::Simulation
})?;
let _ = write!(out, "{json}");
Ok(())
}
pub fn selection_schematic_b64(
&self,
start_tick: u32,
end_tick: u32,
out: &mut DiplomatWrite,
) -> Result<(), NucleationError> {
let Some(view) = self.timeline() else {
super::set_last_error("no timeline has been recorded on this simulation");
return Err(NucleationError::NotFound);
};
let timeline = view.to_timeline();
let selection = timeline
.select_ticks(u64::from(start_tick), u64::from(end_tick))
.map_err(|e| {
super::set_last_error(e.to_string());
NucleationError::InvalidArgument
})?;
let schematic = crate::tick_timeline::selection_schematic(
&timeline,
selection,
self.sim.registry(),
)
.map_err(|e| {
super::set_last_error(e);
NucleationError::Simulation
})?;
let data = crate::formats::schematic::to_schematic(&schematic).map_err(|e| {
super::set_last_error(e.to_string());
NucleationError::Serialize
})?;
let _ = write!(out, "{}", super::super::schematic::b64(&data));
Ok(())
}
pub fn updates_count(&self) -> u32 {
self.sim.recorded_updates().len() as u32
}
pub fn updates_json(&self, out: &mut DiplomatWrite) {
let _ = write!(out, "{}", super::updates_json_range(&self.sim, 0, u64::MAX));
}
pub fn updates_json_between(&self, from_tick: u32, to_tick: u32, out: &mut DiplomatWrite) {
let _ = write!(
out,
"{}",
super::updates_json_range(&self.sim, u64::from(from_tick), u64::from(to_tick))
);
}
pub fn updates_heat_json(&self, from_tick: u32, to_tick: u32, out: &mut DiplomatWrite) {
let _ = write!(
out,
"{}",
super::updates_heat_range(&self.sim, u64::from(from_tick), u64::from(to_tick))
);
}
pub fn updates_wave_json(&self, tick: u32, out: &mut DiplomatWrite) {
let _ = write!(out, "{}", super::updates_wave(&self.sim, u64::from(tick)));
}
pub fn moving_blocks_json(&self, out: &mut DiplomatWrite) {
let mut json = String::from("[");
for (i, m) in self.sim.moving_blocks().iter().enumerate() {
if i > 0 {
json.push(',');
}
let state = self.sim.registry().descriptor(m.state).unwrap_or("?");
let carried = self.sim.registry().descriptor(m.carried).unwrap_or("?");
let quoted = |s: Option<mc_tick::StateId>| match s
.and_then(|s| self.sim.registry().descriptor(s))
{
Some(descriptor) => format!("\"{descriptor}\""),
None => "null".to_string(),
};
let carried_short = quoted(m.carried_short);
let remains = quoted(m.remains);
let _ = write!(
json,
"{{\"to\":[{},{},{}],\"from\":[{},{},{}],\"state\":\"{}\",\
\"carried\":\"{}\",\"carried_short\":{},\"remains\":{},\
\"dir\":\"{}\",\"extending\":{},\"started\":{},\"lands\":{},\
\"source_piston\":{}}}",
m.to.x,
m.to.y,
m.to.z,
m.from.x,
m.from.y,
m.from.z,
state,
carried,
carried_short,
remains,
m.travel.name(),
m.extending,
m.started_on,
m.lands_on,
m.source_piston
);
}
json.push(']');
let _ = write!(out, "{json}");
}
pub fn clear_changes(&mut self) -> bool {
self.sim.clear_recorded()
}
pub fn changes_json(&self, out: &mut DiplomatWrite) {
self.changes_json_from(0, out);
}
pub fn changes_json_from(&self, start: u32, out: &mut DiplomatWrite) {
let recorded = self.sim.recorded();
let start = (start as usize).min(recorded.len());
let mut json = String::from("[");
for (i, change) in recorded[start..].iter().enumerate() {
if i > 0 {
json.push(',');
}
let from = self.sim.registry().descriptor(change.from).unwrap_or("?");
let to = self.sim.registry().descriptor(change.to).unwrap_or("?");
let _ = write!(
json,
"{{\"tick\":{},\"pos\":[{},{},{}],\"from\":\"{}\",\"to\":\"{}\"}}",
change.tick, change.pos.x, change.pos.y, change.pos.z, from, to
);
}
json.push(']');
let _ = write!(out, "{json}");
}
pub fn item_entities_json(&self, out: &mut DiplomatWrite) {
let mut json = String::from("{\"items\":[");
let mut first = true;
for entity in self.sim.item_entities() {
if entity.removed {
continue;
}
if !first {
json.push(',');
}
first = false;
let _ = write!(
json,
"{{\"id\":{},\"item\":\"{}\",\"count\":{},\"pos\":[{},{},{}],\"vel\":[{},{},{}],\"on_ground\":{}",
entity.id,
entity.item.0,
entity.item.1,
entity.pos[0], entity.pos[1], entity.pos[2],
entity.vel[0], entity.vel[1], entity.vel[2],
entity.on_ground,
);
json.push_str(",\"contents\":[");
let contents = self.sim.item_contents(entity.id).unwrap_or(&[]);
for (i, stack) in contents.iter().enumerate() {
if i > 0 {
json.push(',');
}
let _ = write!(
json,
"{{\"id\":\"{}\",\"count\":{}}}",
stack.id, stack.count
);
}
json.push_str("]}");
}
json.push_str("],\"minecarts\":[");
let mut first = true;
for cart in self.sim.minecarts() {
if cart.removed {
continue;
}
if !first {
json.push(',');
}
first = false;
let _ = write!(
json,
"{{\"id\":{},\"kind\":\"{}\",\"pos\":[{},{},{}],\"vel\":[{},{},{}]}}",
cart.id,
cart.kind,
cart.pos[0],
cart.pos[1],
cart.pos[2],
cart.vel[0],
cart.vel[1],
cart.vel[2],
);
}
json.push_str("],\"frozen\":[");
let mut first = true;
for body in self.sim.entity_bodies() {
if body.is_minecart {
continue;
}
if !first {
json.push(',');
}
first = false;
let _ = write!(
json,
"{{\"id\":{},\"kind\":\"{}\",\"pos\":[{},{},{}],\"size\":[{},{},{}],\"leashed\":{}}}",
body.id,
body.kind,
(body.min[0] + body.max[0]) / 2.0,
body.min[1],
(body.min[2] + body.max[2]) / 2.0,
body.max[0] - body.min[0],
body.max[1] - body.min[1],
body.max[2] - body.min[2],
body.leashed,
);
}
json.push_str("]}");
let _ = write!(out, "{json}");
}
pub fn motion_semantics(&self, out: &mut DiplomatWrite) {
let name = match self.sim.motion_semantics() {
mc_tick::MotionSemantics::ClampAbsTen => "clamp_abs_ten",
mc_tick::MotionSemantics::DropNonFinite => "drop_non_finite",
};
let _ = write!(out, "{name}");
}
pub fn piston_retract_contacts(&self) -> u32 {
self.sim.piston_retract_contacts().len() as u32
}
pub fn events_summary_json(&self, out: &mut DiplomatWrite) {
use std::collections::BTreeMap;
#[derive(Default)]
struct Row {
changes: u32,
piston: u32,
redstone: u32,
}
let mut rows: BTreeMap<u64, Row> = BTreeMap::new();
for change in self.sim.recorded() {
let from = self.sim.registry().descriptor(change.from).unwrap_or("");
let to = self.sim.registry().descriptor(change.to).unwrap_or("");
let row = rows.entry(change.tick).or_default();
row.changes += 1;
let named =
|needle: &str| super::is_named(from, needle) || super::is_named(to, needle);
if named("piston") {
row.piston += 1;
}
if named("redstone")
|| named("repeater")
|| named("comparator")
|| named("observer")
|| named("lever")
|| named("button")
|| named("pressure_plate")
|| named("lamp")
{
row.redstone += 1;
}
}
let mut json = String::from("[");
for (i, (tick, row)) in rows.iter().enumerate() {
if i > 0 {
json.push(',');
}
let _ = write!(
json,
"{{\"tick\":{},\"changes\":{},\"piston\":{},\"redstone\":{}}}",
tick, row.changes, row.piston, row.redstone
);
}
json.push(']');
let _ = write!(out, "{json}");
}
pub fn non_air_count(&self) -> u32 {
self.sim.world().non_air_count() as u32
}
pub fn non_air_center_x(&self) -> f64 {
let mut sum = 0.0;
let mut n = 0u32;
for (pos, _) in self.sim.world().iter_non_air() {
sum += f64::from(pos.x);
n += 1;
}
if n == 0 {
f64::NAN
} else {
sum / f64::from(n)
}
}
pub fn non_air_min_x(&self) -> i32 {
self.sim
.world()
.iter_non_air()
.map(|(pos, _)| pos.x)
.min()
.unwrap_or(i32::MAX)
}
pub fn non_air_max_x(&self) -> i32 {
self.sim
.world()
.iter_non_air()
.map(|(pos, _)| pos.x)
.max()
.unwrap_or(i32::MIN)
}
pub fn changes_count(&self) -> u32 {
self.sim.recorded().len() as u32
}
pub fn world_snapshot_json(&self, out: &mut DiplomatWrite) {
let mut json = String::from("[");
let mut first = true;
for (pos, id) in self.sim.world().iter_non_air() {
if !first {
json.push(',');
}
first = false;
let state = self.sim.registry().descriptor(id).unwrap_or("?");
let _ = write!(
json,
"{{\"pos\":[{},{},{}],\"state\":\"{}\"}}",
pos.x, pos.y, pos.z, state
);
}
json.push(']');
let _ = write!(out, "{json}");
}
pub fn machine_graph_json(&self, out: &mut DiplomatWrite) {
let graph = super::analyse_world(self.sim.world(), self.sim.registry());
let _ = write!(out, "{}", graph.to_json());
}
#[allow(clippy::too_many_arguments)]
pub fn machine_graph_batch_json(
bx: i32,
by: i32,
bz: i32,
travel: i32,
x_off: i32,
palette: &DiplomatStr,
cells: &[u16],
air_index: u16,
out: &mut DiplomatWrite,
) -> Result<(), NucleationError> {
let palette =
std::str::from_utf8(palette).map_err(|_| NucleationError::InvalidArgument)?;
let json =
super::machine_graph_batch(bx, by, bz, travel, x_off, palette, cells, air_index)
.map_err(|_| NucleationError::InvalidArgument)?;
let _ = write!(out, "{json}");
Ok(())
}
}
}
fn machine_graph_batch(
bx: i32,
by: i32,
bz: i32,
travel: i32,
x_off: i32,
palette: &str,
cells: &[u16],
air_index: u16,
) -> Result<String, String> {
use std::fmt::Write as _;
let pal: Vec<String> = palette
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let volume = (bx.max(0) as usize) * (by.max(0) as usize) * (bz.max(0) as usize);
if volume == 0 || cells.is_empty() || cells.len() % volume != 0 {
return Err("cells length is not a whole number of bbox volumes".into());
}
let n_genomes = cells.len() / volume;
let mut registry = mc_tick::StateRegistry::new();
for descriptor in &pal {
registry.intern(descriptor).map_err(|e| format!("{e:?}"))?;
}
mc_tick::intern_companions(&mut registry);
let mut table = mc_tick::BehaviourTable::default();
let rules = mc_tick::register_all_at(&mut registry, &mut table, mc_tick::Pos::new(0, 0, 0));
let empty = vec![air_index; volume];
let reference = structure_from_blocks(bx, by, bz, travel, x_off, &pal, &empty, air_index)?;
let bounds = reference.bounds(4);
let mut json = String::from("[");
for g in 0..n_genomes {
let slice = &cells[g * volume..(g + 1) * volume];
let structure = structure_from_blocks(bx, by, bz, travel, x_off, &pal, slice, air_index)?;
let mut world = mc_tick::World::new(bounds);
structure.place(&mut world, &mut registry, mc_tick::Pos::new(0, 0, 0));
let graph = mc_tick::machine_graph::analyse(&world, ®istry, &rules);
let codes: Vec<&str> = graph.rejections.iter().map(|r| r.code).collect();
let engine_cells: usize = graph.engines.iter().map(|e| e.cells.len()).sum();
if g > 0 {
json.push(',');
}
let _ = write!(
json,
"[{},{},{},{},{},\"{}\"]",
graph.rejected(),
graph.rejected_for_sustained(),
engine_cells,
graph.payload.len(),
graph.dead_weight.len(),
codes.join("|")
);
}
json.push(']');
Ok(json)
}
fn analyse_world(
world: &mc_tick::World,
registry: &mc_tick::StateRegistry,
) -> mc_tick::machine_graph::MachineGraph {
let rules = rebuild_rules(registry);
mc_tick::machine_graph::analyse(world, registry, &rules)
}
fn rebuild_rules(registry: &mc_tick::StateRegistry) -> mc_tick::VanillaRules {
let mut scratch = registry.clone();
let mut table = mc_tick::BehaviourTable::default();
mc_tick::register_all_at(&mut scratch, &mut table, mc_tick::Pos::new(0, 0, 0))
}
fn conduction_trace_json(sim: &mc_tick::Simulation, pos: mc_tick::Pos) -> String {
let rules = rebuild_rules(sim.registry());
rules.conduction_trace(sim.registry(), sim.world(), sim.comparator_outputs(), pos)
}
pub(crate) fn bake_into(sim: &mc_tick::Simulation, schem: &mut crate::UniversalSchematic) -> u32 {
let (mx, my, mz) = schem.get_bounding_box().min;
let mut changed = 0u32;
for (pos, id) in sim.world().iter_non_air() {
let Some(descriptor) = sim.registry().descriptor(id) else {
continue;
};
let (x, y, z) = (pos.x + mx, pos.y + my, pos.z + mz);
if schem
.get_block(x, y, z)
.is_some_and(|current| current.to_string() == descriptor)
{
continue;
}
if schem.set_block_from_string(x, y, z, descriptor).is_ok() {
changed += 1;
}
}
changed
}
#[cfg(test)]
mod tests {
use super::{
block_entity_audit, machine_graph_batch, needs_block_entity, simulate_placement_into,
simulate_placement_into_world, simulate_placements_into, simulate_placements_into_world,
to_gametest_snbt,
};
use crate::{BlockState, UniversalSchematic};
#[test]
fn simulate_tag_derives_wire_power_and_connections() {
let mut schem = UniversalSchematic::new("wired".into());
for x in 0..4 {
schem.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
schem.set_block(0, 1, 0, &BlockState::new("minecraft:redstone_block"));
schem
.set_block_from_string(1, 1, 0, "minecraft:redstone_wire{simulate=true}")
.expect("simulated placement");
let wire = schem.get_block(1, 1, 0).expect("wire exists").to_string();
assert!(
wire.contains("power=15"),
"wire next to a redstone block reads 15, got {wire}"
);
assert!(
wire.contains("west=side"),
"wire connects toward the block powering it, got {wire}"
);
}
#[test]
fn simulate_world_tag_is_an_explicit_full_world_opt_in() {
let mut schem = UniversalSchematic::new("wired world".into());
for x in 0..4 {
schem.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
schem.set_block(0, 1, 0, &BlockState::new("minecraft:redstone_block"));
schem
.set_block_from_string(1, 1, 0, "minecraft:redstone_wire{simulate=world}")
.expect("full-world simulated placement");
let wire = schem.get_block(1, 1, 0).expect("wire exists").to_string();
assert!(
wire.contains("power=15"),
"unexpected full-world result: {wire}"
);
}
#[test]
fn simulated_batch_matches_sequential_convenience_placements() {
fn base() -> UniversalSchematic {
let mut schematic = UniversalSchematic::new("wired".into());
for x in 0..7 {
schematic.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
schematic.set_block(0, 1, 0, &BlockState::new("minecraft:redstone_block"));
schematic
}
let positions = [(1, 1, 0), (2, 1, 0), (3, 1, 0), (4, 1, 0)];
let mut sequential = base();
for &(x, y, z) in &positions {
sequential
.set_block_from_string(x, y, z, "minecraft:redstone_wire{simulate=true}")
.expect("sequential simulated placement");
}
let mut batched = base();
let written = simulate_placements_into(&mut batched, &positions, "minecraft:redstone_wire")
.expect("batched simulated placements");
assert!(written >= positions.len());
for x in 0..7 {
for y in 0..=1 {
assert_eq!(
batched.get_block(x, y, 0).map(ToString::to_string),
sequential.get_block(x, y, 0).map(ToString::to_string),
"different final state at ({x},{y},0)"
);
}
}
let last = batched
.get_block(4, 1, 0)
.expect("last wire exists")
.to_string();
assert!(last.contains("power=12"), "unexpected final wire: {last}");
}
#[test]
fn simple_wire_resolver_matches_the_event_engine() {
fn base() -> UniversalSchematic {
let mut schematic = UniversalSchematic::new("wired".into());
for x in 0..7 {
schematic.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
schematic.set_block(0, 1, 0, &BlockState::new("minecraft:redstone_block"));
schematic
}
let positions = [(1, 1, 0), (2, 1, 0), (3, 1, 0), (4, 1, 0)];
let mut resolved = base();
simulate_placements_into(&mut resolved, &positions, "minecraft:redstone_wire")
.expect("static resolver");
let mut simulated = base();
simulate_placements_into_world(&mut simulated, &positions, "minecraft:redstone_wire")
.expect("event engine");
for x in 0..7 {
for y in 0..=1 {
assert_eq!(
resolved.get_block(x, y, 0).map(ToString::to_string),
simulated.get_block(x, y, 0).map(ToString::to_string),
"different final state at ({x},{y},0)"
);
}
}
}
#[test]
fn source_placement_resolver_matches_the_event_engine() {
fn base() -> UniversalSchematic {
let mut schematic = UniversalSchematic::new("unpowered line".into());
for x in 0..7 {
schematic.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
for x in 1..=4 {
schematic
.set_block_from_string(
x,
1,
0,
"minecraft:redstone_wire[east=side,north=none,power=0,south=none,west=side]",
)
.unwrap();
}
schematic
}
let positions = [(0, 1, 0)];
let mut resolved = base();
simulate_placements_into(&mut resolved, &positions, "minecraft:redstone_block")
.expect("static source resolver");
let mut simulated = base();
simulate_placements_into_world(&mut simulated, &positions, "minecraft:redstone_block")
.expect("event engine");
for x in 0..7 {
for y in 0..=1 {
assert_eq!(
resolved.get_block(x, y, 0).map(ToString::to_string),
simulated.get_block(x, y, 0).map(ToString::to_string),
"different final state at ({x},{y},0)"
);
}
}
}
#[test]
fn active_neighbour_forces_the_event_engine_fallback() {
let mut schematic = UniversalSchematic::new("lamp".into());
for x in 0..4 {
schematic.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
schematic.set_block(0, 1, 0, &BlockState::new("minecraft:redstone_block"));
schematic.set_block(
2,
1,
0,
&BlockState::from_block_string("minecraft:redstone_lamp[lit=false]").unwrap(),
);
simulate_placement_into(&mut schematic, 1, 1, 0, "minecraft:redstone_wire")
.expect("wire placement beside a lamp");
let lamp = schematic
.get_block(2, 1, 0)
.expect("lamp exists")
.to_string();
assert!(
lamp.contains("lit=true"),
"event side effect was lost: {lamp}"
);
}
#[test]
fn passive_resolver_skips_simulation_even_for_a_sparse_batch() {
let mut schematic = UniversalSchematic::new("sparse passive edits".into());
schematic.set_block(0, 0, 0, &BlockState::new("minecraft:smooth_stone"));
let positions = [(10, 0, 0), (10_000_000, 0, 0)];
let written =
simulate_placements_into(&mut schematic, &positions, "minecraft:quartz_block")
.expect("passive writes need no bounded simulated world");
assert_eq!(written, 2);
for &(x, y, z) in &positions {
assert_eq!(
schematic
.get_block(x, y, z)
.expect("block exists")
.get_name(),
"minecraft:quartz_block"
);
}
}
#[test]
fn local_simulation_cost_ignores_unrelated_world_span() {
let mut local = UniversalSchematic::new("sparse world".into());
for x in 0..4 {
local.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
local.set_block(0, 1, 0, &BlockState::new("minecraft:redstone_block"));
local.set_block(10_000_000, 0, 0, &BlockState::new("minecraft:smooth_stone"));
let mut whole_world = local.clone();
let full_error =
simulate_placement_into_world(&mut whole_world, 1, 1, 0, "minecraft:redstone_wire")
.expect_err("the complete sparse span is intentionally over the world limit");
assert!(full_error.contains("over the 8000000-cell limit"));
simulate_placement_into(&mut local, 1, 1, 0, "minecraft:redstone_wire")
.expect("local component remains small");
let wire = local.get_block(1, 1, 0).expect("wire exists").to_string();
assert!(wire.contains("power=15"), "unexpected local result: {wire}");
assert_eq!(
local
.get_block(10_000_000, 0, 0)
.expect("unrelated environment survives")
.get_name(),
"minecraft:smooth_stone"
);
}
#[test]
fn simulate_tag_on_an_isolated_block_is_a_plain_write() {
let mut schem = UniversalSchematic::new("empty".into());
schem
.set_block_from_string(0, 0, 0, "minecraft:redstone_wire{simulate=true}")
.expect("plain write");
let wire = schem.get_block(0, 0, 0).expect("wire exists").to_string();
assert!(wire.contains("redstone_wire"), "got {wire}");
}
#[test]
fn simulate_tag_refuses_company_in_the_braces() {
let mut schem = UniversalSchematic::new("combo".into());
let err = schem
.set_block_from_string(0, 0, 0, "minecraft:barrel{signal=3,simulate=true}")
.unwrap_err();
assert!(err.contains("only tag"), "got {err}");
}
#[test]
fn the_batch_prefilter_keeps_an_engine_and_rejects_a_lone_block() {
const PALETTE: &str = "minecraft:air;minecraft:slime_block;\
minecraft:sticky_piston[extended=false,facing=east];\
minecraft:sticky_piston[extended=false,facing=west];\
minecraft:observer[facing=east,powered=false];\
minecraft:observer[facing=west,powered=false]";
let engine_b: [u16; 8] = [5, 1, 3, 0, 0, 2, 1, 4];
let lone_slime: [u16; 8] = [0, 1, 0, 0, 0, 0, 0, 0];
let mut cells = engine_b.to_vec();
cells.extend_from_slice(&lone_slime);
let json =
machine_graph_batch(4, 1, 2, 26, 1, PALETTE, &cells, 0).expect("batch analysis runs");
let rows: Vec<&str> = json
.trim_start_matches('[')
.trim_end_matches(']')
.split("],[")
.map(|r| r.trim_matches(|c| c == '[' || c == ']'))
.collect();
assert_eq!(rows.len(), 2, "one row per genome: {json}");
assert!(
rows[0].starts_with("false,false,"),
"engine B must survive both filter tiers, got {}",
rows[0]
);
assert!(
rows[0].contains(",6,"),
"engine B's engine is its six blocks, got {}",
rows[0]
);
assert!(
rows[1].starts_with("true,true,"),
"a lone slime block cannot move, got {}",
rows[1]
);
assert!(
rows[1].contains("no_piston"),
"and the reason is why: {}",
rows[1]
);
}
#[test]
fn pre_flattening_ids_are_converted_before_the_engine_sees_them() {
let mut schem = UniversalSchematic::new("legacy".into());
schem.metadata.source_data_version = Some(1343); schem.set_block(0, 0, 0, &BlockState::new("minecraft:slime"));
schem.set_block(
1,
0,
0,
&BlockState::new("minecraft:stonebrick").with_property("variant", "stonebrick"),
);
let snbt = to_gametest_snbt(&schem);
assert!(
snbt.contains("minecraft:slime_block"),
"slime block not flattened: {snbt}"
);
assert!(
snbt.contains("minecraft:stone_bricks"),
"stone brick not flattened: {snbt}"
);
assert!(
!snbt.contains("\"minecraft:slime\""),
"the 1.12 id survived into the engine's input: {snbt}"
);
}
#[test]
fn modern_builds_are_passed_through_untouched() {
let mut schem = UniversalSchematic::new("modern".into());
schem.metadata.source_data_version = Some(3955);
schem.set_block(0, 0, 0, &BlockState::new("minecraft:slime_block"));
assert!(to_gametest_snbt(&schem).contains("minecraft:slime_block"));
}
#[test]
fn audit_names_blocks_whose_block_entity_is_missing() {
let mut schem = UniversalSchematic::new("stripped".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:comparator"));
schem.set_block(1, 0, 0, &BlockState::new("minecraft:comparator"));
schem.set_block(2, 0, 0, &BlockState::new("minecraft:furnace"));
schem.set_block(3, 0, 0, &BlockState::new("minecraft:stone"));
let json = block_entity_audit(&schem);
assert!(json.contains("\"missing_total\":3"), "{json}");
assert!(
json.contains("\"name\":\"minecraft:comparator\",\"count\":2"),
"{json}"
);
assert!(
json.contains("\"name\":\"minecraft:furnace\",\"count\":1"),
"{json}"
);
assert!(
!json.contains("stone"),
"a block with no ticking NBT was reported: {json}"
);
assert!(
json.contains("2 comparators"),
"summary not written: {json}"
);
}
#[test]
fn audit_is_silent_when_nothing_is_missing() {
let mut schem = UniversalSchematic::new("plain".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:stone"));
let json = block_entity_audit(&schem);
assert!(json.contains("\"missing_total\":0"), "{json}");
assert!(json.contains("\"summary\":\"\""), "{json}");
}
#[test]
fn every_colour_of_shulker_box_counts_as_a_container() {
assert!(needs_block_entity("minecraft:shulker_box"));
assert!(needs_block_entity("minecraft:lime_shulker_box"));
assert!(!needs_block_entity("minecraft:oak_sign"));
}
#[test]
fn entities_round_trip_from_schematic_into_the_engines_parser() {
use crate::entity::{Entity, NbtValue};
use std::collections::HashMap;
let mut schem = UniversalSchematic::new("carts".into());
schem.set_block(10, 0, 5, &BlockState::new("minecraft:rail"));
schem.set_block(12, 2, 7, &BlockState::new("minecraft:stone"));
let mut cart = Entity::new("minecraft:minecart".into(), (10.5, 0.0625, 5.5));
cart.nbt.insert(
"Motion".into(),
NbtValue::List(vec![
NbtValue::Double(0.25),
NbtValue::Double(0.0),
NbtValue::Double(-0.5),
]),
);
assert!(schem.add_entity(cart));
let mut stack = HashMap::new();
stack.insert(
"id".to_string(),
NbtValue::String("minecraft:redstone".into()),
);
stack.insert("count".to_string(), NbtValue::Byte(7));
let mut item = Entity::new("minecraft:item".into(), (11.5, 1.0, 6.5));
item.nbt.insert("Item".into(), NbtValue::Compound(stack));
item.nbt.insert("PickupDelay".into(), NbtValue::Short(40));
assert!(schem.add_entity(item));
let snbt = to_gametest_snbt(&schem);
let parsed = mc_tick::Structure::parse(&snbt)
.unwrap_or_else(|e| panic!("engine rejected our own output: {e}\n{snbt}"));
assert_eq!(parsed.entities.len(), 2, "entities dropped: {snbt}");
match &parsed.entities[0] {
mc_tick::structure::SpawnedEntity::Minecart(cart) => {
assert_eq!(cart.kind, "minecraft:minecart");
assert_eq!(
cart.pos,
[0.5, 0.0625, 0.5],
"position not shifted into structure space"
);
assert_eq!(cart.motion, [0.25, 0.0, -0.5]);
}
other => panic!("expected a minecart, got {other:?}"),
}
match &parsed.entities[1] {
mc_tick::structure::SpawnedEntity::Item(item) => {
assert_eq!(item.pos, [1.5, 1.0, 1.5]);
assert_eq!(item.item, ("minecraft:redstone".to_string(), 7));
assert_eq!(item.pickup_delay, 40);
}
other => panic!("expected an item, got {other:?}"),
}
assert_eq!(parsed.item_entities.len(), 1);
}
#[test]
fn tiny_motions_are_written_without_an_exponent() {
use crate::entity::{Entity, NbtValue};
let mut schem = UniversalSchematic::new("denormal".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:rail"));
let mut cart = Entity::new("minecraft:minecart".into(), (0.5, 0.0, 0.5));
cart.nbt.insert(
"Motion".into(),
NbtValue::List(vec![
NbtValue::Double(4.27987680632209e-59),
NbtValue::Double(0.0),
NbtValue::Double(0.0),
]),
);
assert!(schem.add_entity(cart));
let snbt = to_gametest_snbt(&schem);
let (_, entities) = snbt.split_once("entities:").expect("an entities section");
assert!(
!entities.contains("e-") && !entities.contains("e+"),
"an exponent reached the engine's input: {entities}"
);
let parsed = mc_tick::Structure::parse(&snbt).expect("parse");
match &parsed.entities[0] {
mc_tick::structure::SpawnedEntity::Minecart(cart) => {
assert_eq!(cart.motion, [4.27987680632209e-59, 0.0, 0.0]);
}
other => panic!("expected a minecart, got {other:?}"),
}
}
#[test]
fn a_nan_cart_velocity_survives_the_round_trip() {
use crate::entity::{Entity, NbtValue};
let mut schem = UniversalSchematic::new("nan cart".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:rail"));
let mut cart = Entity::new("minecraft:minecart".into(), (0.5, 0.0, 0.5));
cart.nbt.insert(
"Motion".into(),
NbtValue::List(vec![
NbtValue::Double(4.27987680632209e-59),
NbtValue::Double(0.0),
NbtValue::Double(f64::NAN),
]),
);
assert!(schem.add_entity(cart));
let snbt = to_gametest_snbt(&schem);
let parsed = mc_tick::Structure::parse(&snbt)
.unwrap_or_else(|e| panic!("a NaN motion must parse, not error: {e}\n{snbt}"));
match &parsed.entities[0] {
mc_tick::structure::SpawnedEntity::Minecart(cart) => {
assert_eq!(
cart.motion[0], 4.27987680632209e-59,
"denormal mangled: {snbt}"
);
assert_eq!(cart.motion[1], 0.0);
assert!(
cart.motion[2].is_nan(),
"the NaN was sanitised to {} — this un-glues the door: {snbt}",
cart.motion[2]
);
}
other => panic!("expected a minecart, got {other:?}"),
}
}
#[test]
fn infinite_velocities_survive_the_round_trip() {
use crate::entity::{Entity, NbtValue};
let mut schem = UniversalSchematic::new("overflowed".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:rail"));
let mut cart = Entity::new("minecraft:minecart".into(), (0.5, 0.0, 0.5));
cart.nbt.insert(
"Motion".into(),
NbtValue::List(vec![
NbtValue::Double(f64::INFINITY),
NbtValue::Double(f64::NEG_INFINITY),
NbtValue::Double(0.0),
]),
);
assert!(schem.add_entity(cart));
let snbt = to_gametest_snbt(&schem);
let parsed = mc_tick::Structure::parse(&snbt).expect("infinities must parse");
match &parsed.entities[0] {
mc_tick::structure::SpawnedEntity::Minecart(cart) => {
assert_eq!(cart.motion[0], f64::INFINITY);
assert_eq!(
cart.motion[1],
f64::NEG_INFINITY,
"the sign was lost: {snbt}"
);
}
other => panic!("expected a minecart, got {other:?}"),
}
}
fn record_door_schematic() -> UniversalSchematic {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/samples/55_3x3.zip");
let bytes = std::fs::read(&path).expect("the record-door sample must be present");
crate::formats::world::from_world_zip(&bytes).expect("the sample loads")
}
fn nan_carts(sim: &mc_tick::Simulation) -> usize {
sim.minecarts()
.iter()
.filter(|c| c.vel.iter().any(|v| v.is_nan()))
.count()
}
fn from_snbt(snbt: &str) -> mc_tick::Simulation {
super::ffi::TickSimulation::from_snbt(
snbt.as_bytes(),
super::ffi::TickSettleMode::InWorld,
0,
0,
0,
b"",
)
.expect("the round-tripped text must load")
.sim
}
#[test]
fn a_settled_line_bakes_back_and_traces_to_its_lever() {
let mut schem = UniversalSchematic::new("line".into());
for x in 0..3 {
schem.set_block(x, 0, 0, &BlockState::new("minecraft:smooth_stone"));
}
schem
.set_block_from_string(
0,
1,
0,
"minecraft:lever[face=floor,facing=north,powered=true]",
)
.expect("lever");
for x in 1..3 {
schem
.set_block_from_string(
x,
1,
0,
"minecraft:redstone_wire[east=none,north=none,power=0,south=none,west=none]",
)
.expect("wire");
}
let mut sim = super::ffi::TickSimulation::from_snbt(
to_gametest_snbt(&schem).as_bytes(),
super::ffi::TickSettleMode::Placement,
0,
0,
0,
b"",
)
.expect("the line loads")
.sim;
sim.run_until_quiescent(64);
let trace = super::conduction_trace_json(&sim, mc_tick::Pos::new(2, 1, 0));
assert!(
trace.contains("\"kind\":\"wire\",\"power\":14"),
"the far dust carries 14: {trace}"
);
assert!(
trace.contains("\"mechanism\":\"wire\""),
"it is fed by a one-level wire step: {trace}"
);
assert!(
trace.contains("minecraft:lever"),
"the tree reaches the lever: {trace}"
);
let changed = super::bake_into(&sim, &mut schem);
assert!(changed >= 2, "both dust cells changed, got {changed}");
let near = schem.get_block(1, 1, 0).expect("dust stays").to_string();
assert!(
near.contains("power=15"),
"the baked schematic carries settled power: {near}"
);
assert_eq!(
super::bake_into(&sim, &mut schem),
0,
"a second bake finds nothing left to write"
);
}
#[test]
fn the_emitted_snbt_states_the_schematics_own_data_version() {
let mut schem = UniversalSchematic::new("versioned".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:stone"));
schem.metadata.source_data_version = Some(4082);
let snbt = to_gametest_snbt(&schem);
assert!(
snbt.contains("DataVersion: 4082"),
"the file's own version must win: {snbt}"
);
assert_eq!(
mc_tick::Structure::parse(&snbt)
.expect("parses")
.data_version,
Some(4082),
"and it must survive being read back"
);
schem.metadata.source_data_version = None;
schem.metadata.mc_version = None;
let snbt = to_gametest_snbt(&schem);
assert!(
snbt.contains(&format!(
"DataVersion: {}",
crate::dataconverter::CANONICAL_DATA_VERSION
)),
"a schematic with no version must still stamp one: {snbt}"
);
}
#[test]
fn the_snbt_round_trip_keeps_the_record_doors_nan_carts() {
let schematic = record_door_schematic();
assert_eq!(
schematic.metadata.source_data_version,
Some(4082),
"the record door is a 1.21.3 save — if that changed, this test is measuring nothing"
);
let direct = wire_record_door(super::ffi::TickSettleMode::InWorld);
assert_eq!(
direct.motion_semantics(),
mc_tick::MotionSemantics::ClampAbsTen,
"4082 is below the boundary, so a cold load keeps NaN"
);
assert_eq!(nan_carts(&direct), 6, "the reference path's nan carts");
let snbt = to_gametest_snbt(&schematic);
let round_tripped = from_snbt(&snbt);
assert_eq!(
round_tripped.motion_semantics(),
mc_tick::MotionSemantics::ClampAbsTen,
"the round trip changed which game loaded the door"
);
assert_eq!(
nan_carts(&round_tripped),
nan_carts(&direct),
"the same door, through its own SNBT, must be the same machine"
);
}
#[test]
fn a_build_stamped_after_the_boundary_still_drops_its_nan_carts() {
let mut schematic = record_door_schematic();
schematic.metadata.source_data_version =
Some(mc_tick::motion::FIRST_NAN_DROPPING_DATA_VERSION);
let snbt = to_gametest_snbt(&schematic);
assert!(
snbt.contains("DataVersion: 4671"),
"the restamp must reach the text"
);
let sim = from_snbt(&snbt);
assert_eq!(
sim.motion_semantics(),
mc_tick::MotionSemantics::DropNonFinite,
"4671 and later guard the whole vector on `isFinite`"
);
assert_eq!(
nan_carts(&sim),
0,
"the same six carts must come back finite — the door is un-glued, correctly"
);
}
fn wire_record_door(settle: super::ffi::TickSettleMode) -> mc_tick::Simulation {
let schematic = record_door_schematic();
let snbt = to_gametest_snbt(&schematic);
let structure = mc_tick::Structure::parse(&snbt).expect("the sample parses");
super::wire_simulation(
&structure,
mc_tick::Pos::new(0, 0, 0),
settle,
&[],
schematic.metadata.source_data_version,
)
.expect("the engine must accept the record door")
}
#[test]
fn the_record_door_is_at_rest_under_in_world_and_disturbed_under_quiet() {
let mut at_rest = wire_record_door(super::ffi::TickSettleMode::InWorld);
at_rest.run(200);
assert_eq!(
at_rest.recorded().len(),
0,
"nobody touched this door: vanilla changes no block ticking the same save in \
place, so neither may we. First few: {:?}",
at_rest.recorded().iter().take(4).collect::<Vec<_>>()
);
assert!(
at_rest.is_quiescent(),
"a build at rest has nothing pending"
);
let mut placed = wire_record_door(super::ffi::TickSettleMode::Quiet);
placed.run(200);
assert!(
!placed.recorded().is_empty(),
"placing this build must disturb it — an observer whose neighbour just \
appeared pulses. If this is empty, `InWorld` proves nothing."
);
}
#[test]
fn the_record_doors_two_blazes_are_seated_passengers() {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/samples/55_3x3.zip");
let bytes = std::fs::read(&path).expect("the record-door sample must be present");
let schematic = crate::formats::world::from_world_zip(&bytes).expect("the sample loads");
let snbt = to_gametest_snbt(&schematic);
let structure = mc_tick::Structure::parse(&snbt).expect("the sample parses");
assert_eq!(
structure.entities.len(),
22,
"the control: the *top level* is 22, so a 24 below cannot be a recount \
of it — the two extra bodies have to come from somewhere else"
);
let sim = wire_record_door(super::ffi::TickSettleMode::InWorld);
assert_eq!(
sim.entity_bodies().len(),
24,
"22 top-level entities plus two riders is what vanilla counts in this world"
);
let riders = sim.riders();
assert_eq!(
riders.len(),
2,
"two blazes ride two of the four plain carts"
);
let mut seats: Vec<f64> = riders
.iter()
.map(|(_, kind, pos)| {
assert_eq!(kind, "minecraft:blaze");
pos[1]
})
.collect();
seats.sort_by(f64::total_cmp);
assert_eq!(seats, vec![2.1875, 2.25], "the exact y the save records");
for (_, _, pos) in &riders {
let vehicle = sim
.minecarts()
.iter()
.find(|c| (c.pos[1] + 0.1875 - pos[1]).abs() < 1.0e-12)
.expect("every rider has a vehicle 0.1875 below it");
assert_eq!([vehicle.pos[0], vehicle.pos[2]], [pos[0], pos[2]]);
}
}
fn wire_with_entities(entities: &str) -> Result<mc_tick::Simulation, String> {
let snbt = format!(
"{{DataVersion: 4903, size: [1, 1, 1], \
palette: [{{Name: \"minecraft:rail\"}}], \
blocks: [{{pos: [0, 0, 0], state: 0}}], entities: [{entities}]}}"
);
let structure = mc_tick::Structure::parse(&snbt)
.unwrap_or_else(|e| panic!("the parser must accept this: {e}\n{snbt}"));
super::wire_simulation(
&structure,
mc_tick::Pos::new(0, 0, 0),
super::ffi::TickSettleMode::InWorld,
&[],
None,
)
}
#[test]
fn entities_needing_unimplemented_behaviour_are_refused_by_name() {
for (entity, expected) in [
(
r#"{pos: [0.5d, 0.0d, 0.5d], nbt: {id: "minecraft:furnace_minecart", Fuel: 3600}}"#,
"minecraft:furnace_minecart",
),
(
r#"{pos: [0.5d, 0.0d, 0.5d], nbt: {id: "minecraft:furnace_minecart", PushX: 1.0d}}"#,
"minecraft:furnace_minecart",
),
(
r#"{pos: [0.5d, 0.0d, 0.5d], nbt: {id: "minecraft:dragon_fireball", Motion: [0.5d, 0.0d, 0.0d]}}"#,
"minecraft:dragon_fireball",
),
(
r#"{pos: [0.5d, 0.0d, 0.5d], nbt: {id: "minecraft:small_fireball", Motion: [0.0d, -0.1d, 0.0d]}}"#,
"minecraft:small_fireball",
),
(
r#"{pos: [0.5d, 0.0d, 0.5d], nbt: {id: "minecraft:villager", Motion: [0.0d, 0.0d, 0.2d]}}"#,
"minecraft:villager",
),
] {
let error = wire_with_entities(entity)
.err()
.unwrap_or_else(|| panic!("{expected} needs behaviour that does not exist"));
assert!(
error.contains(expected),
"refusal does not name the type: {error}"
);
}
}
#[test]
fn frozen_scaffolding_entities_load_as_hitboxes() {
let sim = wire_with_entities(
r#"{pos: [0.5d, 0.0625d, 0.5d], nbt: {id: "minecraft:furnace_minecart", Fuel: 0}},
{pos: [1.5d, 1.0d, 0.5d], nbt: {id: "minecraft:dragon_fireball"}},
{pos: [2.5d, 1.0d, 0.5d], nbt: {id: "minecraft:small_fireball"}},
{pos: [3.5d, 1.0d, 0.5d], nbt: {id: "minecraft:villager"}}"#,
)
.expect("the record doors' scaffolding is exactly what this supports");
assert_eq!(sim.minecarts().len(), 1, "a furnace cart is a cart");
let frozen: Vec<&str> = sim
.entity_bodies()
.iter()
.filter(|b| !b.is_minecart)
.map(|b| b.kind.as_str())
.collect();
assert_eq!(
frozen,
[
"minecraft:dragon_fireball",
"minecraft:small_fireball",
"minecraft:villager"
],
"each keeps its own identity, because each has its own hitbox"
);
}
#[test]
fn entities_that_do_have_behaviour_still_load() {
let sim = wire_with_entities(
r#"{pos: [0.5d, 0.0625d, 0.5d], nbt: {id: "minecraft:minecart", Motion: [0.0d, 0.0d, 0.0d]}},
{pos: [0.5d, 1.0d, 0.5d], nbt: {id: "minecraft:item", Item: {id: "minecraft:redstone", count: 1b}}}"#,
)
.expect("a plain cart and an item are both simulated today");
assert_eq!(sim.minecarts().len(), 1, "the cart should be live");
assert_eq!(sim.item_entities().len(), 1, "the item should be live");
}
#[test]
fn an_unrepresentable_entity_is_refused_and_named() {
use crate::entity::Entity;
let mut schem = UniversalSchematic::new("creeper".into());
schem.set_block(0, 0, 0, &BlockState::new("minecraft:rail"));
assert!(schem.add_entity(Entity::new("minecraft:creeper".into(), (0.5, 0.0, 0.5))));
let snbt = to_gametest_snbt(&schem);
let err = mc_tick::Structure::parse(&snbt).expect_err("should refuse the creeper");
assert!(
matches!(
&err,
mc_tick::structure::StructureError::UnsupportedEntity { entity_type, .. }
if entity_type == "minecraft:creeper"
),
"wrong error: {err}"
);
let detail = super::structure_parse_detail(&err, true);
assert!(detail.contains("minecraft:creeper"), "{detail}");
assert!(!detail.contains("engine fault"), "{detail}");
}
}