#![cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;
use crate::bounding_box::BoundingBox;
use crate::formats::world_stream::{WorldChunkView, WorldSink};
use crate::universal_schematic::UniversalSchematic;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
const CHUNK: i32 = 16;
fn floor_div(a: i32, b: i32) -> i32 {
let d = a / b;
let r = a % b;
if r != 0 && (r < 0) != (b < 0) {
d - 1
} else {
d
}
}
fn is_air(name: &str) -> bool {
matches!(
name,
"minecraft:air" | "minecraft:cave_air" | "minecraft:void_air"
)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Placement {
pub key: String,
pub offset: (i32, i32, i32),
pub local_bbox: BoundingBox,
}
impl Placement {
pub fn world_bbox(&self) -> BoundingBox {
let (ox, oy, oz) = self.offset;
BoundingBox::new(
(
self.local_bbox.min.0 + ox,
self.local_bbox.min.1 + oy,
self.local_bbox.min.2 + oz,
),
(
self.local_bbox.max.0 + ox,
self.local_bbox.max.1 + oy,
self.local_bbox.max.2 + oz,
),
)
}
fn chunk_span(&self) -> (i32, i32, i32, i32) {
let wb = self.world_bbox();
(
floor_div(wb.min.0, CHUNK),
floor_div(wb.min.2, CHUNK),
floor_div(wb.max.0, CHUNK),
floor_div(wb.max.2, CHUNK),
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PackStats {
pub schematics: usize,
pub blocks_written: u64,
pub chunks_written: usize,
pub bounds: Option<BoundingBox>,
pub peak_live_chunks: usize,
}
pub fn pack<L>(placements: &[Placement], mut load: L, sink: &mut WorldSink) -> Result<PackStats>
where
L: FnMut(&Placement) -> Result<UniversalSchematic>,
{
let mut order: Vec<usize> = (0..placements.len()).collect();
order.sort_by(|&a, &b| {
let pa = &placements[a];
let pb = &placements[b];
pa.key
.cmp(&pb.key)
.then_with(|| pa.offset.cmp(&pb.offset))
});
let mut last_touch: HashMap<(i32, i32), usize> = HashMap::new();
for (pos, &idx) in order.iter().enumerate() {
let (cx0, cz0, cx1, cz1) = placements[idx].chunk_span();
for cx in cx0..=cx1 {
for cz in cz0..=cz1 {
last_touch.insert((cx, cz), pos);
}
}
}
let mut live: HashMap<(i32, i32), WorldChunkView> = HashMap::new();
let mut stats = PackStats {
schematics: placements.len(),
blocks_written: 0,
chunks_written: 0,
bounds: None,
peak_live_chunks: 0,
};
for (pos, &idx) in order.iter().enumerate() {
let placement = &placements[idx];
let (ox, oy, oz) = placement.offset;
let schematic = load(placement)?;
for (bp, block) in schematic.iter_blocks() {
if is_air(&block.name) {
continue;
}
let (wx, wy, wz) = (bp.x + ox, bp.y + oy, bp.z + oz);
let (cx, cz) = (floor_div(wx, CHUNK), floor_div(wz, CHUNK));
let view = live
.entry((cx, cz))
.or_insert_with(|| WorldChunkView::new(cx, cz));
view.set_block(wx, wy, wz, block);
stats.bounds = Some(match stats.bounds.take() {
None => BoundingBox::new((wx, wy, wz), (wx, wy, wz)),
Some(bb) => BoundingBox::new(
(bb.min.0.min(wx), bb.min.1.min(wy), bb.min.2.min(wz)),
(bb.max.0.max(wx), bb.max.1.max(wy), bb.max.2.max(wz)),
),
});
}
drop(schematic);
stats.peak_live_chunks = stats.peak_live_chunks.max(live.len());
let mut ready: Vec<(i32, i32)> = live
.keys()
.copied()
.filter(|c| last_touch.get(c) == Some(&pos))
.collect();
ready.sort(); for c in ready {
let view = live.remove(&c).expect("ready chunk is live");
stats.blocks_written += view.blocks().count() as u64;
sink.write_chunk(&view)?;
stats.chunks_written += 1;
}
}
let mut leftover: Vec<(i32, i32)> = live.keys().copied().collect();
leftover.sort();
for c in leftover {
let view = live.remove(&c).unwrap();
stats.blocks_written += view.blocks().count() as u64;
sink.write_chunk(&view)?;
stats.chunks_written += 1;
}
Ok(stats)
}
pub fn grid_layout(
items: &[(String, BoundingBox)],
spacing_chunks: i32,
base_y: i32,
) -> Vec<Placement> {
if items.is_empty() {
return Vec::new();
}
let mut sorted: Vec<&(String, BoundingBox)> = items.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
let chunks_of = |lo: i32, hi: i32| -> i32 { (hi - lo).max(0) / CHUNK + 1 };
let max_w = sorted
.iter()
.map(|(_, bb)| chunks_of(bb.min.0, bb.max.0))
.max()
.unwrap_or(1);
let max_l = sorted
.iter()
.map(|(_, bb)| chunks_of(bb.min.2, bb.max.2))
.max()
.unwrap_or(1);
let stride_x = (max_w + spacing_chunks.max(0)) * CHUNK;
let stride_z = (max_l + spacing_chunks.max(0)) * CHUNK;
let cols = (sorted.len() as f64).sqrt().ceil() as i32;
let cols = cols.max(1);
sorted
.iter()
.enumerate()
.map(|(i, (key, bb))| {
let col = (i as i32) % cols;
let row = (i as i32) / cols;
let cell_x = col * stride_x;
let cell_z = row * stride_z;
let offset = (cell_x - bb.min.0, base_y - bb.min.1, cell_z - bb.min.2);
Placement {
key: key.clone(),
offset,
local_bbox: bb.clone(),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::world_stream::WorldSource;
fn schem(name: &str, cells: &[(i32, i32, i32, &str)]) -> UniversalSchematic {
let mut s = UniversalSchematic::new(name.to_string());
for &(x, y, z, b) in cells {
s.set_block_str(x, y, z, b);
}
s
}
fn read_world(dir: &std::path::Path) -> HashMap<(i32, i32, i32), String> {
let source = WorldSource::open_dir(dir).expect("open world");
let mut out = HashMap::new();
for chunk in source.chunks().expect("chunks") {
let chunk = chunk.expect("chunk decode");
for (x, y, z, state) in chunk.blocks() {
out.insert((x, y, z), state.name.to_string());
}
}
out
}
fn place(key: &str, off: (i32, i32, i32), s: &UniversalSchematic) -> Placement {
Placement {
key: key.to_string(),
offset: off,
local_bbox: s.get_bounding_box(),
}
}
#[test]
fn packs_blocks_at_placement_plus_local() {
let a = schem("a", &[(0, 0, 0, "minecraft:stone"), (1, 0, 0, "minecraft:dirt")]);
let b = schem("b", &[(0, 0, 0, "minecraft:gold_block")]);
let placements = vec![
place("a", (0, 70, 0), &a),
place("b", (40, 70, 40), &b),
];
let dir = tempdir();
let mut sink = WorldSink::create(&dir, None).unwrap();
let loads = [("a", &a), ("b", &b)];
let stats = pack(
&placements,
|p| {
let s = loads.iter().find(|(k, _)| *k == p.key).unwrap().1;
Ok(s.clone())
},
&mut sink,
)
.unwrap();
sink.finish().unwrap();
assert_eq!(stats.blocks_written, 3);
let world = read_world(&dir);
assert_eq!(world.get(&(0, 70, 0)).map(String::as_str), Some("minecraft:stone"));
assert_eq!(world.get(&(1, 70, 0)).map(String::as_str), Some("minecraft:dirt"));
assert_eq!(world.get(&(40, 70, 40)).map(String::as_str), Some("minecraft:gold_block"));
cleanup(&dir);
}
#[test]
fn air_cells_are_not_written() {
let mut a = schem("a", &[(0, 0, 0, "minecraft:stone"), (2, 0, 0, "minecraft:stone")]);
a.set_block_str(1, 0, 0, "minecraft:air");
let placements = vec![place("a", (0, 64, 0), &a)];
let dir = tempdir();
let mut sink = WorldSink::create(&dir, None).unwrap();
pack(&placements, |_| Ok(a.clone()), &mut sink).unwrap();
sink.finish().unwrap();
let world = read_world(&dir);
assert_eq!(world.get(&(0, 64, 0)).map(String::as_str), Some("minecraft:stone"));
assert_eq!(world.get(&(2, 64, 0)).map(String::as_str), Some("minecraft:stone"));
assert!(world.get(&(1, 64, 0)).is_none(), "air must not be written");
cleanup(&dir);
}
#[test]
fn overlap_resolves_last_placement_wins() {
let a = schem("a", &[(0, 0, 0, "minecraft:stone")]);
let b = schem("b", &[(0, 0, 0, "minecraft:gold_block")]);
let placements = vec![
place("b", (5, 64, 5), &b),
place("a", (5, 64, 5), &a), ];
let dir = tempdir();
let mut sink = WorldSink::create(&dir, None).unwrap();
pack(
&placements,
|p| Ok(if p.key == "a" { a.clone() } else { b.clone() }),
&mut sink,
)
.unwrap();
sink.finish().unwrap();
let world = read_world(&dir);
assert_eq!(
world.get(&(5, 64, 5)).map(String::as_str),
Some("minecraft:gold_block"),
"later placement (key b) must win"
);
cleanup(&dir);
}
#[test]
fn determinism_same_inputs_same_world() {
let a = schem("a", &[(0, 0, 0, "minecraft:stone")]);
let b = schem("b", &[(0, 0, 0, "minecraft:dirt")]);
let items = vec![
("a".to_string(), a.get_bounding_box()),
("b".to_string(), b.get_bounding_box()),
];
let run = |placements: &[Placement]| -> HashMap<(i32, i32, i32), String> {
let dir = tempdir();
let mut sink = WorldSink::create(&dir, None).unwrap();
pack(
placements,
|p| Ok(if p.key == "a" { a.clone() } else { b.clone() }),
&mut sink,
)
.unwrap();
sink.finish().unwrap();
let w = read_world(&dir);
cleanup(&dir);
w
};
let p1 = grid_layout(&items, 1, 64);
let mut rev = items.clone();
rev.reverse();
let p2 = grid_layout(&rev, 1, 64);
assert_eq!(p1, p2, "layout must be independent of input order");
assert_eq!(run(&p1), run(&p2), "same inputs -> identical world");
}
#[test]
fn streaming_live_set_is_bounded() {
let mut items = Vec::new();
let mut map = HashMap::new();
for i in 0..9 {
let key = format!("s{i}");
let s = schem(&key, &[(0, 0, 0, "minecraft:stone")]);
items.push((key.clone(), s.get_bounding_box()));
map.insert(key, s);
}
let placements = grid_layout(&items, 1, 64);
let dir = tempdir();
let mut sink = WorldSink::create(&dir, None).unwrap();
let stats = pack(&placements, |p| Ok(map.get(&p.key).unwrap().clone()), &mut sink).unwrap();
sink.finish().unwrap();
assert_eq!(stats.schematics, 9);
assert_eq!(stats.blocks_written, 9);
assert!(
stats.peak_live_chunks <= 1,
"grid layout must keep the live set to one schematic's chunks, got {}",
stats.peak_live_chunks
);
cleanup(&dir);
}
fn tempdir() -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let n = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!("nucleation_world_pack_{}_{:p}", n, &n as *const _));
std::fs::create_dir_all(&p).unwrap();
p
}
fn cleanup(p: &std::path::Path) {
let _ = std::fs::remove_dir_all(p);
}
}