use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::world_segment::classify::{classify, BlockClass};
use crate::world_segment::grid::OccupancyGrid;
use crate::world_segment::ids::{ClusterId, ContentId, TileId};
use crate::world_segment::partition::{PartitionIndex, PartitionPolicy};
use crate::world_segment::profile::WorldProfile;
use crate::world_segment::tile::VoxelTile;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct SegConfig {
pub cell_size: u32,
pub closing_radius: u32,
pub min_cluster_blocks: u64,
pub partition_policy: PartitionPolicy,
pub algorithm_version: u32,
pub partition_floor_share: Option<f32>,
}
impl SegConfig {
pub fn config_hash(&self, profile: &WorldProfile, partitions: &PartitionIndex) -> ContentId {
let policy: u8 = match self.partition_policy {
PartitionPolicy::HardCut => 0,
PartitionPolicy::Prefer => 1,
PartitionPolicy::Off => 2,
};
let hints = match self.partition_policy {
PartitionPolicy::HardCut => partitions.hints_hash(),
PartitionPolicy::Prefer | PartitionPolicy::Off => {
ContentId::of(&[b"parthints.ignored"])
}
};
let floor_share: Option<[u8; 5]> = self.partition_floor_share.map(|s| {
let mut v = [1u8; 5];
v[1..].copy_from_slice(&s.to_le_bytes());
v
});
let cell = self.cell_size.to_le_bytes();
let closing = self.closing_radius.to_le_bytes();
let min = self.min_cluster_blocks.to_le_bytes();
let algo = self.algorithm_version.to_le_bytes();
let policy_bytes = [policy];
let profile_hash = profile.profile_hash();
let mut parts: Vec<&[u8]> = vec![
b"segconfig.v2",
&cell,
&closing,
&min,
&policy_bytes,
&algo,
profile_hash.as_bytes(),
hints.as_bytes(),
];
if let Some(bytes) = floor_share.as_ref() {
parts.push(bytes);
}
ContentId::of(&parts)
}
}
impl Default for SegConfig {
fn default() -> Self {
SegConfig {
cell_size: 4,
closing_radius: 2,
min_cluster_blocks: 1,
partition_policy: PartitionPolicy::Off,
algorithm_version: 1,
partition_floor_share: None,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Cluster {
pub id: ClusterId,
pub bbox: ((i32, i32, i32), (i32, i32, i32)),
pub block_count: u64,
pub cell_count: u64,
pub partition_id: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct MarginCell {
pub cell: (i32, i32, i32),
pub cluster: ClusterId,
pub partition: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct TileSegments {
pub tile_id: TileId,
pub clusters: Vec<Cluster>,
pub margin: Vec<MarginCell>,
}
pub fn segment_tile(
tile: &VoxelTile,
profile: &WorldProfile,
config: &SegConfig,
partitions: &PartitionIndex,
) -> TileSegments {
segment_tile_inner(tile, profile, config, partitions, false).0
}
pub fn segment_tile_membership(
tile: &VoxelTile,
profile: &WorldProfile,
config: &SegConfig,
partitions: &PartitionIndex,
) -> (TileSegments, BTreeMap<(i32, i32, i32), ClusterId>) {
segment_tile_inner(tile, profile, config, partitions, true)
}
fn segment_tile_inner(
tile: &VoxelTile,
profile: &WorldProfile,
config: &SegConfig,
partitions: &PartitionIndex,
want_membership: bool,
) -> (TileSegments, BTreeMap<(i32, i32, i32), ClusterId>) {
let bounds = tile.bounds();
let cell = config.cell_size.max(1);
let config_id = config.config_hash(profile, partitions);
let dims = (
span_cells(bounds.min.0, bounds.max.0, cell),
span_cells(bounds.min.1, bounds.max.1, cell),
span_cells(bounds.min.2, bounds.max.2, cell),
);
let origin = bounds.min;
let use_partitions =
config.partition_policy == PartitionPolicy::HardCut && !partitions.is_empty();
let (band_lo, band_hi) = profile.substrate_y_band;
let floor_materials: BTreeMap<u32, std::collections::BTreeSet<String>> =
match (use_partitions, config.partition_floor_share) {
(true, Some(share)) => partition_floor_materials(tile, profile, partitions, share),
_ => BTreeMap::new(),
};
let mut artificial: Vec<((i32, i32, i32), Option<u32>)> = Vec::new();
let mut grids: BTreeMap<Option<u32>, OccupancyGrid> = BTreeMap::new();
for (pos, state) in tile.blocks() {
if classify(state, pos.1, profile) == BlockClass::Substrate {
continue;
}
let pidx =
if use_partitions { partitions.id_index_at(pos.0, pos.1, pos.2) } else { None };
if let Some(p) = pidx {
if pos.1 >= band_lo && pos.1 <= band_hi {
if let Some(names) = floor_materials.get(&p) {
if names.contains(state.get_name()) {
continue;
}
}
}
}
artificial.push((pos, pidx));
grids
.entry(pidx)
.or_insert_with(|| OccupancyGrid::new(origin, dims, cell))
.mark(pos.0, pos.1, pos.2);
}
if artificial.is_empty() {
return (
TileSegments { tile_id: tile.id(), clusters: Vec::new(), margin: Vec::new() },
BTreeMap::new(),
);
}
let geometry = OccupancyGrid::new(origin, dims, cell);
let mut groups: BTreeMap<GroupKey, GroupAcc> = BTreeMap::new();
for (pidx, part_grid) in &grids {
let name = pidx.map(|i| partitions.id_of_index(i).to_string());
group_into(part_grid, config.closing_radius, *pidx, &name, &mut groups);
}
let (cluster_of_cell, partition_of_cluster) = assign_ids(config_id, tile.id(), groups);
let mut acc: BTreeMap<ClusterId, ClusterAcc> = BTreeMap::new();
let mut pos_to_cluster: BTreeMap<(i32, i32, i32), ClusterId> = BTreeMap::new();
for (pos, pidx) in artificial {
let cell_coord = geometry.cell_of(pos.0, pos.1, pos.2);
let Some(id) = cluster_of_cell.get(&(pidx, cell_coord)) else { continue };
acc.entry(*id).or_insert_with(ClusterAcc::new).push(pos, cell_coord);
if want_membership {
pos_to_cluster.insert(pos, *id);
}
}
let mut clusters: Vec<Cluster> = Vec::new();
for (id, a) in &acc {
if a.block_count < config.min_cluster_blocks {
continue;
}
clusters.push(Cluster {
id: *id,
bbox: (a.min, a.max),
block_count: a.block_count,
cell_count: a.cells.len() as u64,
partition_id: partition_of_cluster.get(id).cloned().flatten(),
});
}
clusters.sort_by_key(|c| c.id);
let band = (config.closing_radius * 2 + 1) as i32;
let kept: std::collections::BTreeSet<ClusterId> = clusters.iter().map(|c| c.id).collect();
let mut margin: Vec<MarginCell> = Vec::new();
for (id, a) in &acc {
if !kept.contains(id) {
continue;
}
let partition = partition_of_cluster.get(id).cloned().flatten();
for cell_coord in &a.cells {
if in_margin(*cell_coord, dims, band) {
margin.push(MarginCell {
cell: *cell_coord,
cluster: *id,
partition: partition.clone(),
});
}
}
}
margin.sort_by(|a, b| {
a.cell
.cmp(&b.cell)
.then_with(|| a.cluster.cmp(&b.cluster))
.then_with(|| a.partition.cmp(&b.partition))
});
let membership = if want_membership {
pos_to_cluster.into_iter().filter(|(_, id)| kept.contains(id)).collect()
} else {
BTreeMap::new()
};
(TileSegments { tile_id: tile.id(), clusters, margin }, membership)
}
struct ClusterAcc {
min: (i32, i32, i32),
max: (i32, i32, i32),
block_count: u64,
cells: std::collections::BTreeSet<(i32, i32, i32)>,
}
impl ClusterAcc {
fn new() -> Self {
ClusterAcc {
min: (i32::MAX, i32::MAX, i32::MAX),
max: (i32::MIN, i32::MIN, i32::MIN),
block_count: 0,
cells: std::collections::BTreeSet::new(),
}
}
fn push(&mut self, pos: (i32, i32, i32), cell: (i32, i32, i32)) {
self.min = (self.min.0.min(pos.0), self.min.1.min(pos.1), self.min.2.min(pos.2));
self.max = (self.max.0.max(pos.0), self.max.1.max(pos.1), self.max.2.max(pos.2));
self.block_count += 1;
self.cells.insert(cell);
}
}
type GroupKey = (Option<u32>, u32);
struct GroupAcc {
cells: std::collections::BTreeSet<(i32, i32, i32)>,
partition: Option<String>,
}
fn group_into(
grid: &OccupancyGrid,
radius: u32,
pidx: Option<u32>,
partition: &Option<String>,
groups: &mut BTreeMap<GroupKey, GroupAcc>,
) {
let labels = grid.dilated(radius).label_components();
for cell in grid.occupied_cells() {
let Some(label) = labels.label_of(cell) else { continue };
groups
.entry((pidx, label))
.or_insert_with(|| GroupAcc {
cells: std::collections::BTreeSet::new(),
partition: partition.clone(),
})
.cells
.insert(cell);
}
}
fn assign_ids(
config: ContentId,
tile: TileId,
groups: BTreeMap<GroupKey, GroupAcc>,
) -> (
BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId>,
BTreeMap<ClusterId, Option<String>>,
) {
let mut cluster_of_cell: BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId> = BTreeMap::new();
let mut partition_of_cluster: BTreeMap<ClusterId, Option<String>> = BTreeMap::new();
for ((pidx, _label), group) in groups {
let GroupAcc { cells, partition } = group;
let Some(anchor) = cells.iter().next().copied() else { continue };
let id = ClusterId::new(config, tile, partition.as_deref(), anchor);
debug_assert!(
!partition_of_cluster.contains_key(&id),
"ClusterId collision on anchor {anchor:?} in partition {partition:?}: \
(partition, anchor) must be unique across groups"
);
partition_of_cluster.insert(id, partition);
for cell in cells {
cluster_of_cell.insert((pidx, cell), id);
}
}
(cluster_of_cell, partition_of_cluster)
}
fn partition_floor_materials(
tile: &VoxelTile,
profile: &WorldProfile,
partitions: &PartitionIndex,
share: f32,
) -> BTreeMap<u32, std::collections::BTreeSet<String>> {
let (lo, hi) = profile.substrate_y_band;
let mut counts: BTreeMap<(u32, String), u64> = BTreeMap::new();
let mut totals: BTreeMap<u32, u64> = BTreeMap::new();
for (pos, state) in tile.blocks() {
if pos.1 < lo || pos.1 > hi {
continue;
}
let Some(p) = partitions.id_index_at(pos.0, pos.1, pos.2) else { continue };
*counts.entry((p, state.get_name().to_string())).or_insert(0) += 1;
*totals.entry(p).or_insert(0) += 1;
}
let share = share as f64;
let mut floors: BTreeMap<u32, std::collections::BTreeSet<String>> = BTreeMap::new();
for ((p, name), count) in counts {
let total = totals[&p];
if (count as f64) / (total as f64) >= share {
floors.entry(p).or_default().insert(name);
}
}
floors
}
fn span_cells(lo: i32, hi: i32, cell: u32) -> usize {
debug_assert!(hi >= lo, "span_cells: hi ({hi}) must be >= lo ({lo})");
debug_assert!(cell > 0, "span_cells: cell size must be positive");
let span = i64::from(hi) - i64::from(lo);
if span < 0 {
return 0;
}
((span / i64::from(cell.max(1))) + 1) as usize
}
fn in_margin(cell: (i32, i32, i32), dims: (usize, usize, usize), band: i32) -> bool {
cell.0 < band
|| cell.2 < band
|| cell.0 >= dims.0 as i32 - band
|| cell.2 >= dims.2 as i32 - band
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block_state::BlockState;
use crate::world_segment::ids::TileId;
use crate::world_segment::partition::{PartitionHint, PartitionIndex};
use crate::world_segment::tile::TileBounds;
fn profile() -> WorldProfile {
WorldProfile::new(
["minecraft:stone", "minecraft:bedrock"].iter().map(|s| s.to_string()).collect(),
(-64, -50),
)
}
fn cfg() -> SegConfig {
SegConfig { cell_size: 4, closing_radius: 2, min_cluster_blocks: 1, ..SegConfig::default() }
}
fn bounds() -> TileBounds {
TileBounds { min: (0, -64, 0), max: (127, 63, 127) }
}
fn tile(blocks: Vec<((i32, i32, i32), &str)>) -> VoxelTile {
VoxelTile::from_blocks(
TileId { x: 0, z: 0 },
bounds(),
blocks.into_iter().map(|(p, n)| (p, BlockState::new(n))),
)
}
fn no_hints() -> PartitionIndex {
PartitionIndex::new(vec![])
}
#[test]
fn substrate_is_dropped_entirely() {
let t = tile(vec![
((10, -60, 10), "minecraft:stone"),
((11, -60, 10), "minecraft:stone"),
]);
let segs = segment_tile(&t, &profile(), &cfg(), &no_hints());
assert!(segs.clusters.is_empty(), "a tile of pure substrate yields no clusters");
}
#[test]
fn a_build_standing_on_substrate_does_not_merge_with_it() {
let mut blocks = vec![];
for x in 0..40 {
for z in 0..40 {
blocks.push(((x, -60, z), "minecraft:stone")); }
}
blocks.push(((10, -59, 10), "minecraft:redstone_wire")); blocks.push(((30, -59, 30), "minecraft:redstone_wire")); let segs = segment_tile(&tile(blocks), &profile(), &cfg(), &no_hints());
assert_eq!(segs.clusters.len(), 2, "two builds, ground removed");
}
#[test]
fn a_detached_floating_component_does_not_split() {
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1, "an 8-block gap must bridge");
}
#[test]
fn structures_beyond_the_closing_distance_stay_separate() {
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((90, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 2);
}
#[test]
fn cluster_bbox_and_block_count_describe_the_original_blocks() {
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((12, 14, 11), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1);
let c = &segs.clusters[0];
assert_eq!(c.block_count, 2);
assert_eq!(c.bbox, ((10, 10, 10), (12, 14, 11)));
}
#[test]
fn min_cluster_blocks_filters_small_clusters() {
let config = SegConfig { min_cluster_blocks: 2, ..cfg() };
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((90, 10, 10), "minecraft:redstone_wire"),
((91, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1, "the single-block cluster is dropped");
assert_eq!(segs.clusters[0].block_count, 2);
}
#[test]
fn hard_cut_prevents_merging_across_a_partition_boundary() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "left".into(), bbox_xz: (0, 13, 0, 127), y_range: None },
PartitionHint { id: "right".into(), bbox_xz: (14, 127, 0, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(segs.clusters.len(), 2, "the boundary splits them");
let mut got: Vec<_> =
segs.clusters.iter().map(|c| c.partition_id.clone().unwrap()).collect();
got.sort();
assert_eq!(got, vec!["left".to_string(), "right".to_string()]);
}
#[test]
fn hard_cut_clusters_near_a_grid_min_face_keep_distinct_identities() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "near".into(), bbox_xz: (0, 127, 0, 3), y_range: None },
PartitionHint { id: "far".into(), bbox_xz: (0, 127, 4, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let segs = segment_tile(
&tile(vec![
((0, 8, 0), "minecraft:redstone_wire"),
((0, 8, 4), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(
segs.clusters.len(),
2,
"a clipped dilation anchor must not fuse two partitions' clusters"
);
assert_ne!(segs.clusters[0].id, segs.clusters[1].id, "ids must stay distinct");
let mut got: Vec<_> =
segs.clusters.iter().map(|c| c.partition_id.clone().unwrap()).collect();
got.sort();
assert_eq!(got, vec!["far".to_string(), "near".to_string()]);
}
#[test]
fn hard_cut_holds_on_a_boundary_that_is_not_cell_aligned() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "L".into(), bbox_xz: (0, 61, 0, 127), y_range: None },
PartitionHint { id: "R".into(), bbox_xz: (62, 127, 0, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let segs = segment_tile(
&tile(vec![
((60, 10, 40), "minecraft:redstone_wire"),
((63, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
for c in &segs.clusters {
assert!(
!(c.bbox.0 .0 <= 61 && c.bbox.1 .0 >= 62),
"cluster {} spans the boundary at x=62: bbox {:?}",
c.id,
c.bbox
);
}
assert_eq!(segs.clusters.len(), 2, "the boundary splits the straddling cell");
let mut got: Vec<_> = segs
.clusters
.iter()
.map(|c| (c.partition_id.clone().unwrap(), c.bbox))
.collect();
got.sort();
assert_eq!(
got,
vec![
("L".to_string(), ((60, 10, 40), (60, 10, 40))),
("R".to_string(), ((63, 10, 40), (63, 10, 40))),
],
"each block is attributed to the partition it actually sits in"
);
}
#[test]
fn a_cell_shared_by_two_partitions_yields_two_distinct_ids() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "L".into(), bbox_xz: (0, 61, 0, 127), y_range: None },
PartitionHint { id: "R".into(), bbox_xz: (62, 127, 0, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let segs = segment_tile(
&tile(vec![
((60, 10, 40), "minecraft:redstone_wire"),
((63, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(segs.clusters.len(), 2);
assert_ne!(
segs.clusters[0].id, segs.clusters[1].id,
"a shared anchor cell in two partitions must still give two ids"
);
}
#[test]
fn margin_cells_at_a_shared_cell_carry_their_own_partitions() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "L".into(), bbox_xz: (0, 1, 0, 127), y_range: None },
PartitionHint { id: "R".into(), bbox_xz: (2, 127, 0, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let segs = segment_tile(
&tile(vec![
((1, 10, 40), "minecraft:redstone_wire"),
((2, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(segs.clusters.len(), 2, "the boundary splits the straddling cell");
let shared: Vec<&MarginCell> =
segs.margin.iter().filter(|m| m.cell == (0, 18, 10)).collect();
assert_eq!(
shared.len(),
2,
"the shared cell must appear once per partition, got {:?}",
segs.margin
);
assert_ne!(
shared[0].cluster, shared[1].cluster,
"two distinct clusters occupy the shared cell"
);
assert_ne!(
shared[0].partition, shared[1].partition,
"margin entries at a shared cell must be distinguishable by partition; \
without this a stitcher keying on `cell` unions across the boundary"
);
for m in &shared {
let owner = segs
.clusters
.iter()
.find(|c| c.id == m.cluster)
.expect("margin entry must reference a surviving cluster");
assert_eq!(&m.partition, &owner.partition_id);
}
let mut names: Vec<Option<String>> =
shared.iter().map(|m| m.partition.clone()).collect();
names.sort();
assert_eq!(names, vec![Some("L".to_string()), Some("R".to_string())]);
}
#[test]
fn policy_off_reproduces_the_unpartitioned_result() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "left".into(), bbox_xz: (0, 13, 0, 127), y_range: None },
PartitionHint { id: "right".into(), bbox_xz: (14, 127, 0, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::Off, ..cfg() };
let blocks = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
let with = segment_tile(&tile(blocks.clone()), &profile(), &config, &hints);
let without = segment_tile(&tile(blocks), &profile(), &cfg(), &no_hints());
assert_eq!(with, without);
}
#[allow(clippy::type_complexity)]
fn shape_of(
segs: &TileSegments,
) -> (
Vec<(((i32, i32, i32), (i32, i32, i32)), u64, u64, Option<String>)>,
Vec<(i32, i32, i32)>,
) {
let mut clusters: Vec<_> = segs
.clusters
.iter()
.map(|c| (c.bbox, c.block_count, c.cell_count, c.partition_id.clone()))
.collect();
clusters.sort();
let mut margin: Vec<_> = segs.margin.iter().map(|m| m.cell).collect();
margin.sort();
(clusters, margin)
}
#[test]
fn prefer_policy_is_currently_inert_and_behaves_like_off() {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "left".into(), bbox_xz: (0, 13, 0, 127), y_range: None },
PartitionHint { id: "right".into(), bbox_xz: (14, 127, 0, 127), y_range: None },
]);
let blocks = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
let prefer = SegConfig { partition_policy: PartitionPolicy::Prefer, ..cfg() };
let off = SegConfig { partition_policy: PartitionPolicy::Off, ..cfg() };
let prefer_segs = segment_tile(&tile(blocks.clone()), &profile(), &prefer, &hints);
let off_segs = segment_tile(&tile(blocks.clone()), &profile(), &off, &hints);
assert_eq!(prefer_segs.tile_id, off_segs.tile_id);
assert_eq!(
shape_of(&prefer_segs),
shape_of(&off_segs),
"Prefer must segment exactly as Off does"
);
assert_ne!(
prefer_segs.clusters[0].id, off_segs.clusters[0].id,
"different configs must still mint different ids"
);
assert_eq!(prefer_segs.clusters.len(), 1, "the boundary is not enforced under Prefer");
assert!(
prefer_segs.clusters.iter().all(|c| c.partition_id.is_none()),
"nothing is recorded: every partition_id is None"
);
let hard = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let hard_segs = segment_tile(&tile(blocks), &profile(), &hard, &hints);
assert_eq!(hard_segs.clusters.len(), 2);
}
#[test]
fn a_cluster_dropped_by_min_cluster_blocks_leaves_no_margin_entry() {
let config = SegConfig { min_cluster_blocks: 2, ..cfg() };
let segs = segment_tile(
&tile(vec![
((2, 10, 2), "minecraft:redstone_wire"),
((64, 10, 64), "minecraft:redstone_wire"),
((65, 10, 64), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1, "the near-face single-block cluster is dropped");
assert_eq!(segs.clusters[0].block_count, 2);
let kept: std::collections::BTreeSet<ClusterId> =
segs.clusters.iter().map(|c| c.id).collect();
assert!(
segs.margin.iter().all(|m| kept.contains(&m.cluster)),
"every margin entry must reference a surviving cluster"
);
assert!(
segs.margin.is_empty(),
"the dropped cluster was the only one in the band, so margin is empty"
);
}
fn margin_cells(block: (i32, i32, i32)) -> Vec<(i32, i32, i32)> {
let segs = segment_tile(
&tile(vec![(block, "minecraft:redstone_wire")]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1, "one block must give exactly one cluster");
segs.margin.iter().map(|m| m.cell).collect()
}
#[test]
fn two_cells_exactly_2r_plus_1_apart_are_one_cluster() {
let segs = segment_tile(
&tile(vec![
((40, 10, 40), "minecraft:redstone_wire"),
((60, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
1,
"cell distance 2R+1 must merge: the dilated cubes are face-adjacent"
);
}
#[test]
fn two_cells_2r_plus_2_apart_are_two_clusters() {
let segs = segment_tile(
&tile(vec![
((40, 10, 40), "minecraft:redstone_wire"),
((64, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 2, "cell distance 2R+2 leaves a one-cell gap");
}
#[test]
fn margin_band_covers_cell_depths_0_through_2r() {
assert_eq!(margin_cells((2, 10, 2)), vec![(0, 18, 0)]);
assert_eq!(margin_cells((13, 10, 13)), vec![(3, 18, 3)]);
assert_eq!(
margin_cells((17, 10, 17)),
vec![(4, 18, 4)],
"depth 2R must be IN band; this fails for any band width <= 4"
);
assert!(
margin_cells((21, 10, 21)).is_empty(),
"depth 2R+1 must be outside the band; this fails for any width >= 6"
);
assert_eq!(margin_cells((108, 10, 108)), vec![(27, 18, 27)]);
assert!(margin_cells((107, 10, 107)).is_empty(), "cell 26 is interior");
}
fn margin_cells_of(b: TileBounds, block: (i32, i32, i32)) -> Vec<(i32, i32, i32)> {
let t = VoxelTile::from_blocks(
TileId { x: 0, z: 0 },
b,
std::iter::once((block, BlockState::new("minecraft:redstone_wire"))),
);
let segs = segment_tile(&t, &profile(), &cfg(), &no_hints());
assert_eq!(segs.clusters.len(), 1, "one block must give exactly one cluster");
segs.margin.iter().map(|m| m.cell).collect()
}
#[test]
fn margin_band_measures_each_axis_against_its_own_extent() {
let b = TileBounds { min: (0, -64, 0), max: (127, 63, 255) };
assert_eq!(margin_cells_of(b, (13, 10, 64)), vec![(3, 18, 16)]);
assert_eq!(margin_cells_of(b, (64, 10, 13)), vec![(16, 18, 3)]);
assert_eq!(
margin_cells_of(b, (108, 10, 64)),
vec![(27, 18, 16)],
"the far-X band starts at dims.0 - band, not dims.2 - band"
);
assert_eq!(
margin_cells_of(b, (64, 10, 236)),
vec![(16, 18, 59)],
"the far-Z band starts at dims.2 - band"
);
assert!(
margin_cells_of(b, (64, 10, 108)).is_empty(),
"cell z = 27 is interior on a 64-cell Z axis"
);
assert!(margin_cells_of(b, (64, 10, 232)).is_empty(), "cell z = 58 is interior");
}
#[test]
fn duplicate_positions_do_not_make_the_result_depend_on_input_order() {
let forward = vec![
((10, -60, 10), "minecraft:stone"),
((10, -60, 10), "minecraft:redstone_wire"),
];
let mut reverse = forward.clone();
reverse.reverse();
let a = segment_tile(&tile(forward), &profile(), &cfg(), &no_hints());
let b = segment_tile(&tile(reverse), &profile(), &cfg(), &no_hints());
assert_eq!(a, b, "a duplicated position must not let input order reach the output");
assert_eq!(a.clusters.len(), 1, "redstone_wire wins the position, so one cluster");
}
fn ids(config: &SegConfig, profile: &WorldProfile) -> Vec<ClusterId> {
let segs = segment_tile(
&tile(vec![
((40, 10, 40), "minecraft:redstone_wire"),
((41, 10, 40), "minecraft:repeater"),
]),
profile,
config,
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1, "the probe input must be a single cluster");
segs.clusters.iter().map(|c| c.id).collect()
}
#[test]
fn identical_config_and_profile_give_identical_cluster_ids() {
assert_eq!(ids(&cfg(), &profile()), ids(&cfg(), &profile()));
}
#[test]
fn cluster_ids_change_when_only_a_non_geometric_config_field_changes() {
let v2 = SegConfig { algorithm_version: 2, ..cfg() };
assert_ne!(
ids(&cfg(), &profile()),
ids(&v2, &profile()),
"a config change with no geometric effect must still change ids"
);
}
#[test]
fn cluster_ids_change_when_only_the_hint_geometry_changes() {
let wide = PartitionIndex::new(vec![
PartitionHint { id: "L".into(), bbox_xz: (0, 61, 0, 127), y_range: None },
PartitionHint { id: "R".into(), bbox_xz: (62, 127, 0, 127), y_range: None },
]);
let narrow = PartitionIndex::new(vec![
PartitionHint { id: "L".into(), bbox_xz: (0, 40, 0, 127), y_range: None },
PartitionHint { id: "R".into(), bbox_xz: (41, 127, 0, 127), y_range: None },
]);
let config = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let blocks = vec![((10, 10, 40), "minecraft:redstone_wire")];
let a = segment_tile(&tile(blocks.clone()), &profile(), &config, &wide);
let b = segment_tile(&tile(blocks), &profile(), &config, &narrow);
assert_eq!(a.clusters.len(), 1);
assert_eq!(b.clusters.len(), 1);
assert_eq!(a.clusters[0].partition_id.as_deref(), Some("L"));
assert_eq!(b.clusters[0].partition_id.as_deref(), Some("L"));
assert_eq!(a.clusters[0].bbox, b.clusters[0].bbox);
assert_ne!(
a.clusters[0].id, b.clusters[0].id,
"hints sharing ids but differing in extent describe different \
segmentations and must not mint the same ClusterId"
);
}
#[test]
fn cluster_ids_change_when_only_the_profile_changes() {
let other = WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(),
(-64, -50),
);
assert_ne!(ids(&cfg(), &profile()), ids(&cfg(), &other));
}
#[test]
fn interior_clusters_emit_no_margin_cells() {
let segs = segment_tile(
&tile(vec![((64, 10, 64), "minecraft:redstone_wire")]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1);
assert!(segs.margin.is_empty(), "a centre cluster is nowhere near a face");
}
#[test]
fn membership_maps_every_emitted_block_to_its_cluster() {
let profile = WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(), (-64,-50));
let cfg = SegConfig { cell_size:4, closing_radius:2, min_cluster_blocks:1, ..SegConfig::default() };
let mut blocks = vec![];
for x in 0..40 { for z in 0..40 { blocks.push(((x,-60,z), BlockState::new("minecraft:stone"))); } }
blocks.push(((10,-59,10), BlockState::new("minecraft:redstone_wire")));
let t = VoxelTile::from_blocks(TileId{x:0,z:0},
TileBounds{min:(0,-64,0),max:(127,63,127)}, blocks.into_iter());
let (segs, membership) = segment_tile_membership(&t, &profile, &cfg, &PartitionIndex::new(vec![]));
assert_eq!(segs.clusters.len(), 1);
let cluster = segs.clusters[0].id;
assert_eq!(membership.get(&(10,-59,10)), Some(&cluster), "build block maps to its cluster");
assert_eq!(membership.get(&(0,-60,0)), None, "substrate is not in the membership map");
for (_pos, cid) in &membership {
assert!(segs.clusters.iter().any(|c| c.id == *cid));
}
}
#[test]
fn segment_tile_and_membership_agree_on_segments() {
let profile = WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(), (-64,-50));
let cfg = SegConfig::default();
let t = VoxelTile::from_blocks(TileId{x:0,z:0},
TileBounds{min:(0,-64,0),max:(127,63,127)},
vec![((10,10,10), BlockState::new("minecraft:redstone_wire"))].into_iter());
let a = segment_tile(&t, &profile, &cfg, &PartitionIndex::new(vec![]));
let (b, _) = segment_tile_membership(&t, &profile, &cfg, &PartitionIndex::new(vec![]));
assert_eq!(a, b, "membership variant must return identical TileSegments");
}
fn floor_profile() -> WorldProfile {
WorldProfile::new(["minecraft:stone"].iter().map(|s| s.to_string()).collect(), (-64, -57))
}
fn two_plots() -> (PartitionIndex, Vec<((i32, i32, i32), &'static str)>) {
let hints = PartitionIndex::new(vec![
PartitionHint { id: "L".into(), bbox_xz: (0, 63, 0, 127), y_range: None },
PartitionHint { id: "R".into(), bbox_xz: (64, 127, 0, 127), y_range: None },
]);
let mut blocks: Vec<((i32, i32, i32), &'static str)> = Vec::new();
for x in 4..=35 {
for z in 4..=15 {
blocks.push(((x, -60, z), "minecraft:blue_wool"));
}
}
for x in 68..=99 {
for z in 4..=15 {
blocks.push(((x, -60, z), "minecraft:white_concrete"));
}
}
blocks.push(((8, -59, 10), "minecraft:redstone_wire"));
blocks.push(((32, -59, 10), "minecraft:redstone_wire"));
blocks.push(((72, -59, 10), "minecraft:redstone_wire"));
blocks.push(((96, -59, 10), "minecraft:redstone_wire"));
(hints, blocks)
}
#[test]
fn partition_floor_is_subtracted_per_partition() {
let (hints, blocks) = two_plots();
let on = SegConfig {
partition_policy: PartitionPolicy::HardCut,
partition_floor_share: Some(0.3),
..cfg()
};
let segs = segment_tile(&tile(blocks.clone()), &floor_profile(), &on, &hints);
assert_eq!(
segs.clusters.len(),
4,
"floor subtracted: two separate builds per plot, four total; got {:?}",
segs.clusters.iter().map(|c| (c.bbox, c.block_count)).collect::<Vec<_>>()
);
for c in &segs.clusters {
let x_extent = c.bbox.1 .0 - c.bbox.0 .0;
assert!(
x_extent < 15,
"each build is small; a plot-sheet bbox would span the whole plot. bbox {:?}",
c.bbox
);
assert_eq!(
c.block_count, 1,
"only the one-block build survives, not the 384-block floor sheet"
);
}
let off = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
assert!(off.partition_floor_share.is_none());
let merged = segment_tile(&tile(blocks), &floor_profile(), &off, &hints);
assert_eq!(
merged.clusters.len(),
2,
"no floor subtraction: one whole-plot mega-cluster per partition"
);
for c in &merged.clusters {
assert!(
c.block_count > 300,
"the merged cluster swallows the ~384-block floor sheet; got {}",
c.block_count
);
}
}
#[test]
fn floor_share_none_is_behavior_preserving() {
assert!(SegConfig::default().partition_floor_share.is_none());
let explicit_none = SegConfig { partition_floor_share: None, ..cfg() };
let plain = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
assert_eq!(
segment_tile(&tile(plain.clone()), &profile(), &cfg(), &no_hints()),
segment_tile(&tile(plain), &profile(), &explicit_none, &no_hints()),
);
let mut slab = vec![];
for x in 0..40 {
for z in 0..40 {
slab.push(((x, -60, z), "minecraft:stone"));
}
}
slab.push(((10, -59, 10), "minecraft:redstone_wire"));
slab.push(((30, -59, 30), "minecraft:redstone_wire"));
assert_eq!(
segment_tile(&tile(slab.clone()), &profile(), &cfg(), &no_hints()),
segment_tile(&tile(slab), &profile(), &explicit_none, &no_hints()),
);
let hints = PartitionIndex::new(vec![
PartitionHint { id: "left".into(), bbox_xz: (0, 13, 0, 127), y_range: None },
PartitionHint { id: "right".into(), bbox_xz: (14, 127, 0, 127), y_range: None },
]);
let hard = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let hard_none = SegConfig { partition_floor_share: None, ..hard.clone() };
let split = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
assert_eq!(
segment_tile(&tile(split.clone()), &profile(), &hard, &hints),
segment_tile(&tile(split), &profile(), &hard_none, &hints),
);
}
#[test]
fn config_hash_changes_with_floor_share() {
let parts = PartitionIndex::new(vec![PartitionHint {
id: "L".into(),
bbox_xz: (0, 63, 0, 127),
y_range: None,
}]);
let base = SegConfig { partition_policy: PartitionPolicy::HardCut, ..cfg() };
let with_floor = SegConfig { partition_floor_share: Some(0.3), ..base.clone() };
assert_ne!(
base.config_hash(&floor_profile(), &parts),
with_floor.config_hash(&floor_profile(), &parts),
"partition_floor_share must be folded into config_hash"
);
let probe = vec![((10, 10, 40), "minecraft:redstone_wire")];
let a = segment_tile(&tile(probe.clone()), &floor_profile(), &base, &parts);
let b = segment_tile(&tile(probe), &floor_profile(), &with_floor, &parts);
assert_eq!(a.clusters.len(), 1);
assert_eq!(b.clusters.len(), 1);
assert_eq!(a.clusters[0].bbox, b.clusters[0].bbox, "geometry identical");
assert_ne!(
a.clusters[0].id, b.clusters[0].id,
"different partition_floor_share must mint different ids"
);
}
}