use std::collections::BTreeMap;
use crate::block_entity::BlockEntity;
use crate::block_position::BlockPosition;
use crate::universal_schematic::UniversalSchematic;
use crate::world_segment::ids::ContentId;
use crate::world_segment::provenance::{Provenance, StableBuildId};
use crate::world_segment::score::Tier;
use crate::world_segment::stitch::Build;
use crate::BlockState;
use crate::{ProvenanceBounds, SchematicProvenance};
pub struct MaterializeCtx<'a> {
pub source_id: &'a str,
pub snapshot_id: &'a str,
pub config_hash: ContentId,
pub profile_hash: ContentId,
pub extracted_at: i64,
}
pub fn materialize(
build: &Build,
blocks: &BTreeMap<(i32, i32, i32), BlockState>,
tier: Tier,
stable_id: StableBuildId,
ctx: &MaterializeCtx,
) -> (UniversalSchematic, Provenance) {
materialize_with_block_entities(build, blocks, &BTreeMap::new(), tier, stable_id, ctx)
}
pub fn materialize_with_block_entities(
build: &Build,
blocks: &BTreeMap<(i32, i32, i32), BlockState>,
block_entities: &BTreeMap<(i32, i32, i32), BlockEntity>,
tier: Tier,
stable_id: StableBuildId,
ctx: &MaterializeCtx,
) -> (UniversalSchematic, Provenance) {
debug_assert!(
blocks.len() as u64 == build.block_count,
"materialize: {} blocks vs build.block_count {}",
blocks.len(),
build.block_count
);
let min = build.bbox.0;
let mut schem = UniversalSchematic::new(stable_id.to_string());
for (&(x, y, z), state) in blocks.iter() {
schem.set_block(x - min.0, y - min.1, z - min.2, state);
}
for (&(x, y, z), block_entity) in block_entities {
if !blocks.contains_key(&(x, y, z)) {
continue;
}
let local = (x - min.0, y - min.1, z - min.2);
let mut translated = block_entity.clone();
translated.position = local;
schem.set_block_entity(
BlockPosition {
x: local.0,
y: local.1,
z: local.2,
},
translated,
);
}
let fp = crate::fingerprint::fingerprint(&schem, &fingerprint_spec());
let prov = Provenance {
stable_build_id: stable_id,
snapshot_build_id: build.id,
source_id: ctx.source_id.to_string(),
snapshot_id: ctx.snapshot_id.to_string(),
world_bbox: build.bbox,
origin_offset: min,
partition_id: build.partition_id.clone(),
block_count: blocks.len() as u64,
cluster_count: build.cluster_ids.len() as u32,
fingerprint: fp.0,
tier,
config_hash: ctx.config_hash,
profile_hash: ctx.profile_hash,
extracted_at: ctx.extracted_at,
};
let mut embedded =
SchematicProvenance::new(ctx.source_id).expect("world-segment source_id must be non-empty");
embedded.snapshot_id = Some(ctx.snapshot_id.to_string());
embedded.world_bbox = Some(
ProvenanceBounds::new(
[build.bbox.0 .0, build.bbox.0 .1, build.bbox.0 .2],
[build.bbox.1 .0, build.bbox.1 .1, build.bbox.1 .2],
)
.expect("build bbox is ordered"),
);
embedded.origin = Some([min.0, min.1, min.2]);
embedded.partition_id = build.partition_id.clone();
embedded.stable_build_id = Some(stable_id.to_string());
embedded.extracted_at = Some(ctx.extracted_at);
embedded.config_hash = Some(ctx.config_hash.to_string());
embedded.profile_hash = Some(ctx.profile_hash.to_string());
schem.metadata.provenance = Some(embedded);
(schem, prov)
}
fn fingerprint_spec() -> crate::fingerprint::FingerprintSpec {
crate::fingerprint::FingerprintSpec::exact()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world_segment::ids::{ClusterId, ContentId, TileId};
use crate::world_segment::provenance::StableBuildId;
use crate::world_segment::score::Tier;
use crate::world_segment::stitch::Build;
use crate::BlockState;
use std::collections::BTreeMap;
fn build() -> Build {
let id = ClusterId::new(
ContentId::of(&[b"b"]),
TileId { x: 0, z: 0 },
None,
(0, 0, 0),
);
Build {
id,
cluster_ids: vec![id],
bbox: ((10, -60, 10), (11, -60, 10)),
block_count: 2,
cell_count: 1,
partition_id: None,
}
}
fn ctx() -> MaterializeCtx<'static> {
MaterializeCtx {
source_id: "w",
snapshot_id: "s",
config_hash: ContentId::of(&[b"c"]),
profile_hash: ContentId::of(&[b"p"]),
extracted_at: 100,
}
}
#[test]
fn schematic_is_local_origin_normalized() {
let mut blocks = BTreeMap::new();
blocks.insert((10, -60, 10), BlockState::new("minecraft:redstone_wire"));
blocks.insert((11, -60, 10), BlockState::new("minecraft:repeater"));
let sid = StableBuildId::seed("w", build().id);
let (schem, prov) = materialize(&build(), &blocks, Tier::Confident, sid, &ctx());
assert_eq!(
schem.get_block(0, 0, 0).map(|b| b.get_name().to_string()),
Some("minecraft:redstone_wire".to_string())
);
assert_eq!(
schem.get_block(1, 0, 0).map(|b| b.get_name().to_string()),
Some("minecraft:repeater".to_string())
);
assert_eq!(prov.origin_offset, (10, -60, 10));
assert_eq!(prov.world_bbox, ((10, -60, 10), (11, -60, 10)));
assert_eq!(prov.block_count, 2);
assert_eq!(prov.tier, Tier::Confident);
}
#[test]
fn materialize_is_deterministic() {
let mut blocks = BTreeMap::new();
blocks.insert((10, -60, 10), BlockState::new("minecraft:redstone_wire"));
blocks.insert((11, -60, 10), BlockState::new("minecraft:repeater"));
let sid = StableBuildId::seed("w", build().id);
let (_, p1) = materialize(&build(), &blocks, Tier::Confident, sid, &ctx());
let (_, p2) = materialize(&build(), &blocks, Tier::Confident, sid, &ctx());
assert_eq!(
p1, p2,
"same inputs → identical provenance (incl. fingerprint)"
);
}
#[test]
fn materialize_translates_and_preserves_block_entities() {
let mut blocks = BTreeMap::new();
blocks.insert((10, -60, 10), BlockState::new("minecraft:chest"));
blocks.insert((11, -60, 10), BlockState::new("minecraft:repeater"));
let mut block_entities = BTreeMap::new();
block_entities.insert(
(10, -60, 10),
BlockEntity::new("minecraft:chest".to_string(), (10, -60, 10)),
);
let sid = StableBuildId::seed("w", build().id);
let (schem, _) = materialize_with_block_entities(
&build(),
&blocks,
&block_entities,
Tier::Confident,
sid,
&ctx(),
);
let entities = schem.get_block_entities_as_list();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].position, (0, 0, 0));
assert_eq!(entities[0].id, "minecraft:chest");
}
#[test]
fn provenance_block_count_tracks_materialized_blocks_not_build() {
let mut blocks = BTreeMap::new();
blocks.insert((10, -60, 10), BlockState::new("minecraft:redstone_wire"));
blocks.insert((11, -60, 10), BlockState::new("minecraft:repeater"));
let sid = StableBuildId::seed("w", build().id);
let (_, prov) = materialize(&build(), &blocks, Tier::Confident, sid, &ctx());
assert_eq!(prov.block_count, blocks.len() as u64);
}
}