1use crate::parsed::encode_consecutive_roots;
8use crate::{
9 BedrockWorld, BedrockWorldError, Biome3d, BlockPalette, BlockState, ChunkKey, ChunkPos,
10 ChunkRecord, ChunkRecordTag, NbtReader, NbtTag, NbtWriter, Result, SubChunkFormat,
11 WorldStorageHandle, WriteGuard, block_storage_index,
12};
13use bytes::Bytes;
14use indexmap::IndexMap;
15use std::collections::{BTreeMap, BTreeSet, HashMap, hash_map::Entry};
16use std::path::Path;
17
18const STRUCTURE_FORMAT_VERSION: i32 = 1;
19const STRUCTURE_LAYER_COUNT: usize = 2;
20const DEFAULT_BLOCK_VERSION: i32 = 18_002_711;
21const AIR_BLOCK_NAME: &str = "minecraft:air";
22const BLOCKS_PER_SUBCHUNK: usize = 4096;
23const MAX_STRUCTURE_BLOCKS: i64 = 134_217_728;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct McStructureSize {
28 pub x: i32,
30 pub y: i32,
32 pub z: i32,
34}
35
36impl McStructureSize {
37 pub fn new(x: i32, y: i32, z: i32) -> Result<Self> {
39 if x <= 0 || y <= 0 || z <= 0 {
40 return Err(BedrockWorldError::Validation(format!(
41 "structure size must be positive, got {x}x{y}x{z}"
42 )));
43 }
44 let block_count = i64::from(x)
45 .checked_mul(i64::from(y))
46 .and_then(|value| value.checked_mul(i64::from(z)))
47 .ok_or_else(|| {
48 BedrockWorldError::Validation("structure size overflowed".to_string())
49 })?;
50 if block_count > MAX_STRUCTURE_BLOCKS {
51 return Err(BedrockWorldError::Validation(format!(
52 "structure contains too many blocks: {block_count}; limit is {MAX_STRUCTURE_BLOCKS}"
53 )));
54 }
55 Ok(Self { x, y, z })
56 }
57
58 pub fn block_count(self) -> Result<usize> {
60 let count = i64::from(self.x)
61 .checked_mul(i64::from(self.y))
62 .and_then(|value| value.checked_mul(i64::from(self.z)))
63 .ok_or_else(|| {
64 BedrockWorldError::Validation("structure size overflowed".to_string())
65 })?;
66 usize::try_from(count).map_err(|_| {
67 BedrockWorldError::Validation("structure block count does not fit usize".to_string())
68 })
69 }
70
71 pub fn index(self, x: i32, y: i32, z: i32) -> Result<usize> {
73 if x < 0 || y < 0 || z < 0 || x >= self.x || y >= self.y || z >= self.z {
74 return Err(BedrockWorldError::Validation(format!(
75 "structure coordinate out of bounds: ({x}, {y}, {z}) in {}x{}x{}",
76 self.x, self.y, self.z
77 )));
78 }
79 let index = i64::from(x)
80 .checked_mul(i64::from(self.y))
81 .and_then(|value| value.checked_mul(i64::from(self.z)))
82 .and_then(|value| value.checked_add(i64::from(y) * i64::from(self.z)))
83 .and_then(|value| value.checked_add(i64::from(z)))
84 .ok_or_else(|| {
85 BedrockWorldError::Validation("structure index overflowed".to_string())
86 })?;
87 usize::try_from(index).map_err(|_| {
88 BedrockWorldError::Validation("structure index does not fit usize".to_string())
89 })
90 }
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct McStructurePaletteEntry {
96 pub name: String,
98 pub states: BTreeMap<String, NbtTag>,
100 pub version: Option<i32>,
102}
103
104impl McStructurePaletteEntry {
105 pub fn from_block_state(state: &BlockState) -> Self {
107 Self {
108 name: state.name.clone(),
109 states: state.states.clone(),
110 version: state.version,
111 }
112 }
113
114 pub fn air() -> Self {
116 Self {
117 name: AIR_BLOCK_NAME.to_string(),
118 states: BTreeMap::new(),
119 version: Some(DEFAULT_BLOCK_VERSION),
120 }
121 }
122
123 fn key(&self) -> String {
124 let states = self
125 .states
126 .iter()
127 .map(|(key, value)| format!("{key}={value:?}"))
128 .collect::<Vec<_>>()
129 .join(",");
130 format!("{}|{}|{:?}", self.name, states, self.version)
131 }
132
133 fn to_nbt(&self) -> NbtTag {
134 NbtTag::Compound(IndexMap::from([
135 ("name".to_string(), NbtTag::String(self.name.clone())),
136 (
137 "states".to_string(),
138 NbtTag::Compound(
139 self.states
140 .iter()
141 .map(|(key, value)| (key.clone(), value.clone()))
142 .collect(),
143 ),
144 ),
145 (
146 "version".to_string(),
147 NbtTag::Int(self.version.unwrap_or(DEFAULT_BLOCK_VERSION)),
148 ),
149 ]))
150 }
151
152 fn is_air(&self) -> bool {
153 self.name == AIR_BLOCK_NAME
154 }
155}
156
157#[derive(Debug, Clone, PartialEq)]
159pub struct McStructureBlock {
160 pub x: i32,
162 pub y: i32,
164 pub z: i32,
166 pub primary: i32,
168 pub secondary: i32,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum McStructureRotation {
175 None,
177 Clockwise90,
179 Rotate180,
181 CounterClockwise90,
183}
184
185impl McStructureRotation {
186 #[must_use]
187 pub const fn rotate_chunk_delta(self, delta_x: i32, delta_z: i32) -> (i32, i32) {
189 match self {
190 Self::None => (delta_x, delta_z),
191 Self::Clockwise90 => (-delta_z, delta_x),
192 Self::Rotate180 => (-delta_x, -delta_z),
193 Self::CounterClockwise90 => (delta_z, -delta_x),
194 }
195 }
196
197 const fn rotate_local_xz(self, local_x: u8, local_z: u8) -> (u8, u8) {
198 match self {
199 Self::None => (local_x, local_z),
200 Self::Clockwise90 => (15 - local_z, local_x),
201 Self::Rotate180 => (15 - local_x, 15 - local_z),
202 Self::CounterClockwise90 => (local_z, 15 - local_x),
203 }
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct McStructurePlacement {
210 pub source_anchor: ChunkPos,
212 pub target_anchor: ChunkPos,
214 pub origin_y: i32,
216 pub rotation: McStructureRotation,
218 pub mirror_x: bool,
220 pub mirror_z: bool,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum McStructureWritePhase {
227 Prepare,
229 WriteChunks,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub struct McStructureWriteProgress {
236 pub phase: McStructureWritePhase,
238 pub completed: usize,
240 pub total: usize,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct McStructureWriteResult {
247 pub affected_chunks: BTreeSet<ChunkPos>,
249 pub placed_blocks: usize,
251}
252
253#[derive(Debug, Clone, PartialEq)]
255pub struct McStructureFile {
256 pub size: McStructureSize,
258 pub world_origin: [i32; 3],
260 pub palette: Vec<McStructurePaletteEntry>,
262 pub primary_indices: Vec<i32>,
264 pub secondary_indices: Vec<i32>,
266 pub entities: Vec<NbtTag>,
268 pub block_position_data: IndexMap<String, NbtTag>,
270}
271
272impl McStructureFile {
273 pub fn new_air(size: McStructureSize, world_origin: [i32; 3]) -> Result<Self> {
275 let block_count = size.block_count()?;
276 Ok(Self {
277 size,
278 world_origin,
279 palette: vec![McStructurePaletteEntry::air()],
280 primary_indices: vec![0; block_count],
281 secondary_indices: vec![-1; block_count],
282 entities: Vec::new(),
283 block_position_data: IndexMap::new(),
284 })
285 }
286
287 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
289 let root = NbtReader::new(bytes).parse_root()?;
290 Self::from_nbt(root)
291 }
292
293 pub fn to_bytes(&self) -> Result<Vec<u8>> {
295 NbtWriter::write_root(&self.to_nbt()?)
296 }
297
298 pub fn read_from_path(path: &Path) -> Result<Self> {
300 read_mcstructure_file(path)
301 }
302
303 pub fn write_to_path(&self, path: &Path) -> Result<()> {
305 write_mcstructure_file(path, self)
306 }
307
308 pub fn from_nbt(root: NbtTag) -> Result<Self> {
310 let NbtTag::Compound(root) = root else {
311 return Err(BedrockWorldError::Nbt(
312 "mcstructure root must be a compound".to_string(),
313 ));
314 };
315 let format_version = compound_i32(&root, "format_version")?;
316 if format_version != STRUCTURE_FORMAT_VERSION {
317 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
318 "unsupported mcstructure format_version {format_version}"
319 )));
320 }
321
322 let size_values = compound_i32_list(&root, "size", 3)?;
323 let size = McStructureSize::new(size_values[0], size_values[1], size_values[2])?;
324 let world_origin_values = compound_i32_list(&root, "structure_world_origin", 3)?;
325 let structure = compound_child(&root, "structure")?;
326 let block_indices = compound_list(structure, "block_indices")?;
327 if block_indices.len() != STRUCTURE_LAYER_COUNT {
328 return Err(BedrockWorldError::Validation(format!(
329 "block_indices must contain 2 layers, got {}",
330 block_indices.len()
331 )));
332 }
333 let primary_indices = nbt_i32_list(&block_indices[0])?;
334 let secondary_indices = nbt_i32_list(&block_indices[1])?;
335 let block_count = size.block_count()?;
336 if primary_indices.len() != block_count || secondary_indices.len() != block_count {
337 return Err(BedrockWorldError::Validation(format!(
338 "block_indices length must match size product {block_count}, got {}/{}",
339 primary_indices.len(),
340 secondary_indices.len()
341 )));
342 }
343
344 let palette_root = compound_child(structure, "palette")?;
345 let default_palette = compound_child(palette_root, "default")?;
346 let palette = compound_list(default_palette, "block_palette")?
347 .iter()
348 .map(palette_entry_from_nbt)
349 .collect::<Result<Vec<_>>>()?;
350 let block_position_data = match default_palette.get("block_position_data") {
351 Some(NbtTag::Compound(values)) => values.clone(),
352 Some(_) => {
353 return Err(BedrockWorldError::Nbt(
354 "block_position_data must be a compound".to_string(),
355 ));
356 }
357 None => IndexMap::new(),
358 };
359 let entities = match structure.get("entities") {
360 Some(NbtTag::List(values)) => values.clone(),
361 Some(_) => {
362 return Err(BedrockWorldError::Nbt(
363 "structure.entities must be a list".to_string(),
364 ));
365 }
366 None => Vec::new(),
367 };
368
369 Ok(Self {
370 size,
371 world_origin: [
372 world_origin_values[0],
373 world_origin_values[1],
374 world_origin_values[2],
375 ],
376 palette,
377 primary_indices,
378 secondary_indices,
379 entities,
380 block_position_data,
381 })
382 }
383
384 pub fn to_nbt(&self) -> Result<NbtTag> {
386 let block_count = self.size.block_count()?;
387 if self.primary_indices.len() != block_count || self.secondary_indices.len() != block_count
388 {
389 return Err(BedrockWorldError::Validation(format!(
390 "mcstructure index arrays must contain {block_count} values"
391 )));
392 }
393
394 let default_palette = NbtTag::Compound(IndexMap::from([
395 (
396 "block_palette".to_string(),
397 NbtTag::List(
398 self.palette
399 .iter()
400 .map(McStructurePaletteEntry::to_nbt)
401 .collect(),
402 ),
403 ),
404 (
405 "block_position_data".to_string(),
406 NbtTag::Compound(self.block_position_data.clone()),
407 ),
408 ]));
409 let structure = NbtTag::Compound(IndexMap::from([
410 (
411 "block_indices".to_string(),
412 NbtTag::List(vec![
413 NbtTag::IntArray(self.primary_indices.clone()),
414 NbtTag::IntArray(self.secondary_indices.clone()),
415 ]),
416 ),
417 ("entities".to_string(), NbtTag::List(self.entities.clone())),
418 (
419 "palette".to_string(),
420 NbtTag::Compound(IndexMap::from([("default".to_string(), default_palette)])),
421 ),
422 ]));
423 Ok(NbtTag::Compound(IndexMap::from([
424 (
425 "format_version".to_string(),
426 NbtTag::Int(STRUCTURE_FORMAT_VERSION),
427 ),
428 (
429 "size".to_string(),
430 NbtTag::List(vec![
431 NbtTag::Int(self.size.x),
432 NbtTag::Int(self.size.y),
433 NbtTag::Int(self.size.z),
434 ]),
435 ),
436 ("structure".to_string(), structure),
437 (
438 "structure_world_origin".to_string(),
439 NbtTag::List(self.world_origin.iter().copied().map(NbtTag::Int).collect()),
440 ),
441 ])))
442 }
443
444 pub fn primary_index_at(&self, x: i32, y: i32, z: i32) -> Result<i32> {
446 let index = self.size.index(x, y, z)?;
447 self.primary_indices.get(index).copied().ok_or_else(|| {
448 BedrockWorldError::CorruptWorld("primary index array is truncated".to_string())
449 })
450 }
451
452 pub fn blocks(&self) -> Result<Vec<McStructureBlock>> {
454 let mut blocks = Vec::with_capacity(self.size.block_count()?);
455 for x in 0..self.size.x {
456 for y in 0..self.size.y {
457 for z in 0..self.size.z {
458 let index = self.size.index(x, y, z)?;
459 blocks.push(McStructureBlock {
460 x,
461 y,
462 z,
463 primary: self.primary_indices[index],
464 secondary: self.secondary_indices[index],
465 });
466 }
467 }
468 }
469 Ok(blocks)
470 }
471
472 pub fn palette_entry_or_air(&self, index: i32) -> McStructurePaletteEntry {
474 if index < 0 {
475 return McStructurePaletteEntry::air();
476 }
477 let Ok(index) = usize::try_from(index) else {
478 return McStructurePaletteEntry::air();
479 };
480 self.palette
481 .get(index)
482 .cloned()
483 .unwrap_or_else(McStructurePaletteEntry::air)
484 }
485
486 #[must_use]
487 pub fn index_is_air(&self, index: i32) -> bool {
489 self.palette_entry_or_air(index).is_air()
490 }
491
492 pub fn target_chunks(&self, placement: McStructurePlacement) -> Result<BTreeSet<ChunkPos>> {
494 let source_origin_x = checked_mul_16(placement.source_anchor.x, "structure source x")?;
495 let source_origin_z = checked_mul_16(placement.source_anchor.z, "structure source z")?;
496 let source_max_x = checked_add_i32(
497 source_origin_x,
498 self.size.x.saturating_sub(1),
499 "structure source x",
500 )?;
501 let source_max_z = checked_add_i32(
502 source_origin_z,
503 self.size.z.saturating_sub(1),
504 "structure source z",
505 )?;
506 let min_source_chunk_x = source_origin_x.div_euclid(16);
507 let max_source_chunk_x = source_max_x.div_euclid(16);
508 let min_source_chunk_z = source_origin_z.div_euclid(16);
509 let max_source_chunk_z = source_max_z.div_euclid(16);
510 let mut chunks = BTreeSet::new();
511 for source_chunk_x in min_source_chunk_x..=max_source_chunk_x {
512 for source_chunk_z in min_source_chunk_z..=max_source_chunk_z {
513 let source_chunk_x = if placement.mirror_x {
514 min_source_chunk_x
515 .saturating_add(max_source_chunk_x)
516 .saturating_sub(source_chunk_x)
517 } else {
518 source_chunk_x
519 };
520 let source_chunk_z = if placement.mirror_z {
521 min_source_chunk_z
522 .saturating_add(max_source_chunk_z)
523 .saturating_sub(source_chunk_z)
524 } else {
525 source_chunk_z
526 };
527 let delta_x = source_chunk_x.saturating_sub(placement.source_anchor.x);
528 let delta_z = source_chunk_z.saturating_sub(placement.source_anchor.z);
529 let (target_delta_x, target_delta_z) =
530 placement.rotation.rotate_chunk_delta(delta_x, delta_z);
531 chunks.insert(ChunkPos {
532 x: placement.target_anchor.x.saturating_add(target_delta_x),
533 z: placement.target_anchor.z.saturating_add(target_delta_z),
534 dimension: placement.target_anchor.dimension,
535 });
536 }
537 }
538 Ok(chunks)
539 }
540
541 pub fn write_to_world_blocking<S>(
547 &self,
548 world: &BedrockWorld<S>,
549 placement: McStructurePlacement,
550 guard: &WriteGuard,
551 mut progress: impl FnMut(McStructureWriteProgress),
552 ) -> Result<McStructureWriteResult>
553 where
554 S: WorldStorageHandle,
555 {
556 guard.validate(world)?;
557 let mut placements: BTreeMap<ChunkPos, BTreeMap<i8, Vec<StructureBlockPlacement>>> =
558 BTreeMap::new();
559 let mut block_entities: BTreeMap<ChunkPos, Vec<NbtTag>> = BTreeMap::new();
560 let blocks = self.blocks()?;
561 progress(McStructureWriteProgress {
562 phase: McStructureWritePhase::Prepare,
563 completed: 0,
564 total: blocks.len(),
565 });
566
567 for block in blocks {
568 let block_placement = self.structure_block_placement(&block, placement)?;
569 if let Some(block_entity) = self.block_entity_for_block(&block, &block_placement)? {
570 block_entities
571 .entry(block_placement.chunk)
572 .or_default()
573 .push(block_entity);
574 }
575 placements
576 .entry(block_placement.chunk)
577 .or_default()
578 .entry(block_placement.subchunk_y)
579 .or_default()
580 .push(block_placement);
581 }
582
583 if placements.is_empty() {
584 return Err(BedrockWorldError::Validation(
585 "structure contains no blocks to place".to_string(),
586 ));
587 }
588
589 let total_chunks = placements.len();
590 progress(McStructureWriteProgress {
591 phase: McStructureWritePhase::WriteChunks,
592 completed: 0,
593 total: total_chunks,
594 });
595
596 let mut affected_chunks = BTreeSet::new();
597 let mut transaction = world.transaction();
598 for (index, (chunk, subchunks)) in placements.into_iter().enumerate() {
599 let existing_chunk = world.get_chunk_blocking(chunk)?;
600 if !chunk_has_biome_record(&existing_chunk.records) {
601 transaction.put_raw_record(
602 &ChunkKey::new(chunk, ChunkRecordTag::Data3D),
603 default_data3d_bytes()?,
604 );
605 }
606
607 for (subchunk_y, subchunk_placements) in subchunks {
608 let mut subchunk = EntrySubchunkBuilder::from_chunk(&existing_chunk, subchunk_y)?;
609 for placement in subchunk_placements {
610 let storage_index = block_storage_index(
611 placement.local_x,
612 placement.local_y,
613 placement.local_z,
614 );
615 subchunk.primary[storage_index] = placement.primary;
616 subchunk.secondary[storage_index] = placement.secondary;
617 }
618 let bytes = encode_entry_subchunk(subchunk)?;
619 transaction
620 .put_raw_record(&ChunkKey::subchunk(chunk, subchunk_y), Bytes::from(bytes));
621 }
622
623 transaction.put_raw_record(
624 &ChunkKey::new(chunk, ChunkRecordTag::FinalizedState),
625 Bytes::from(2_i32.to_le_bytes().to_vec()),
626 );
627 if let Some(structure_entities) = block_entities.remove(&chunk) {
628 let mut roots = world
629 .block_entities_in_chunk_blocking(chunk)?
630 .into_iter()
631 .filter_map(|record| match record.entity.position {
632 Some([x, y, z])
633 if structure_entities
634 .iter()
635 .any(|tag| compound_position_matches(tag, x, y, z)) =>
636 {
637 None
638 }
639 _ => Some(record.entity.nbt),
640 })
641 .collect::<Vec<_>>();
642 roots.extend(structure_entities);
643 if !roots.is_empty() {
644 let value = encode_consecutive_roots(&roots)?;
645 transaction
646 .put_raw_record(&ChunkKey::new(chunk, ChunkRecordTag::BlockEntity), value);
647 }
648 }
649 affected_chunks.insert(chunk);
650 progress(McStructureWriteProgress {
651 phase: McStructureWritePhase::WriteChunks,
652 completed: index + 1,
653 total: total_chunks,
654 });
655 }
656 transaction.commit()?;
657
658 Ok(McStructureWriteResult {
659 affected_chunks,
660 placed_blocks: self.size.block_count()?,
661 })
662 }
663
664 fn structure_block_placement(
665 &self,
666 block: &McStructureBlock,
667 placement: McStructurePlacement,
668 ) -> Result<StructureBlockPlacement> {
669 let source_origin_x = checked_mul_16(placement.source_anchor.x, "structure source x")?;
670 let source_origin_z = checked_mul_16(placement.source_anchor.z, "structure source z")?;
671 let relative_x = mirrored_structure_offset(self.size.x, block.x, placement.mirror_x, "x")?;
672 let relative_z = mirrored_structure_offset(self.size.z, block.z, placement.mirror_z, "z")?;
673 let source_world_x = checked_add_i32(source_origin_x, relative_x, "structure source x")?;
674 let source_world_z = checked_add_i32(source_origin_z, relative_z, "structure source z")?;
675 let source_chunk_x = source_world_x.div_euclid(16);
676 let source_chunk_z = source_world_z.div_euclid(16);
677 let local_x = u8::try_from(source_world_x.rem_euclid(16)).map_err(|_| {
678 BedrockWorldError::Validation(format!(
679 "structure source x has invalid local value: {source_world_x}"
680 ))
681 })?;
682 let local_z = u8::try_from(source_world_z.rem_euclid(16)).map_err(|_| {
683 BedrockWorldError::Validation(format!(
684 "structure source z has invalid local value: {source_world_z}"
685 ))
686 })?;
687
688 let delta_x = source_chunk_x.saturating_sub(placement.source_anchor.x);
689 let delta_z = source_chunk_z.saturating_sub(placement.source_anchor.z);
690 let (target_delta_x, target_delta_z) =
691 placement.rotation.rotate_chunk_delta(delta_x, delta_z);
692 let target_chunk = ChunkPos {
693 x: placement.target_anchor.x.saturating_add(target_delta_x),
694 z: placement.target_anchor.z.saturating_add(target_delta_z),
695 dimension: placement.target_anchor.dimension,
696 };
697 let (target_local_x, target_local_z) = placement.rotation.rotate_local_xz(local_x, local_z);
698 let target_world_y = checked_add_i32(placement.origin_y, block.y, "structure target y")?;
699 let subchunk_y = i8::try_from(target_world_y.div_euclid(16)).map_err(|_| {
700 BedrockWorldError::Validation(format!(
701 "structure target y cannot be represented as subchunk: {target_world_y}"
702 ))
703 })?;
704 let local_y = u8::try_from(target_world_y.rem_euclid(16)).map_err(|_| {
705 BedrockWorldError::Validation(format!(
706 "structure target y has invalid local value: {target_world_y}"
707 ))
708 })?;
709
710 Ok(StructureBlockPlacement {
711 chunk: target_chunk,
712 subchunk_y,
713 local_x: target_local_x,
714 local_y,
715 local_z: target_local_z,
716 primary: transform_palette_entry(self.palette_entry_or_air(block.primary), placement),
717 secondary: transform_palette_entry(
718 self.palette_entry_or_air(block.secondary),
719 placement,
720 ),
721 })
722 }
723
724 fn block_entity_for_block(
725 &self,
726 block: &McStructureBlock,
727 placement: &StructureBlockPlacement,
728 ) -> Result<Option<NbtTag>> {
729 let index = self.size.index(block.x, block.y, block.z)?;
730 let Some(NbtTag::Compound(position_data)) =
731 self.block_position_data.get(&index.to_string())
732 else {
733 return Ok(None);
734 };
735 let Some(NbtTag::Compound(block_entity_data)) = position_data.get("block_entity_data")
736 else {
737 return Ok(None);
738 };
739 let mut entity = block_entity_data.clone();
740 let world_x = placement
741 .chunk
742 .x
743 .checked_mul(16)
744 .and_then(|value| value.checked_add(i32::from(placement.local_x)))
745 .ok_or_else(|| {
746 BedrockWorldError::Validation("block entity x overflowed".to_string())
747 })?;
748 let world_y = i32::from(placement.subchunk_y)
749 .checked_mul(16)
750 .and_then(|value| value.checked_add(i32::from(placement.local_y)))
751 .ok_or_else(|| {
752 BedrockWorldError::Validation("block entity y overflowed".to_string())
753 })?;
754 let world_z = placement
755 .chunk
756 .z
757 .checked_mul(16)
758 .and_then(|value| value.checked_add(i32::from(placement.local_z)))
759 .ok_or_else(|| {
760 BedrockWorldError::Validation("block entity z overflowed".to_string())
761 })?;
762 entity.insert("x".to_string(), NbtTag::Int(world_x));
763 entity.insert("y".to_string(), NbtTag::Int(world_y));
764 entity.insert("z".to_string(), NbtTag::Int(world_z));
765 Ok(Some(NbtTag::Compound(entity)))
766 }
767
768 pub fn from_world_region_blocking(
770 world: &BedrockWorld,
771 dimension: crate::Dimension,
772 min_x: i32,
773 min_y: i32,
774 min_z: i32,
775 size: McStructureSize,
776 ) -> Result<Self> {
777 let mut structure = Self::new_air(size, [min_x, min_y, min_z])?;
778 let mut palette_indices = HashMap::new();
779 palette_indices.insert(structure.palette[0].key(), 0_i32);
780 let mut chunk_cache = HashMap::new();
781
782 for x in 0..size.x {
783 let world_x = min_x + x;
784 let chunk_x = world_x.div_euclid(16);
785 let local_x = u8::try_from(world_x.rem_euclid(16)).map_err(|_| {
786 BedrockWorldError::Validation(format!("invalid local x for block {world_x}"))
787 })?;
788 for z in 0..size.z {
789 let world_z = min_z + z;
790 let chunk_z = world_z.div_euclid(16);
791 let local_z = u8::try_from(world_z.rem_euclid(16)).map_err(|_| {
792 BedrockWorldError::Validation(format!("invalid local z for block {world_z}"))
793 })?;
794 let chunk_pos = ChunkPos {
795 x: chunk_x,
796 z: chunk_z,
797 dimension,
798 };
799 if let Entry::Vacant(entry) = chunk_cache.entry(chunk_pos) {
800 let chunk = world.get_chunk_blocking(chunk_pos)?;
801 entry.insert(chunk);
802 }
803 let chunk = chunk_cache.get(&chunk_pos).ok_or_else(|| {
804 BedrockWorldError::CorruptWorld("chunk cache insert failed".to_string())
805 })?;
806 for y in 0..size.y {
807 let world_y = min_y + y;
808 let state = match chunk.get_block(
809 local_x,
810 i16::try_from(world_y).map_err(|_| {
811 BedrockWorldError::Validation(format!(
812 "block y={world_y} cannot be represented as i16"
813 ))
814 })?,
815 local_z,
816 ) {
817 Ok(state) => state,
818 Err(error)
819 if matches!(
820 error.kind(),
821 crate::BedrockWorldErrorKind::UnsupportedChunkFormat
822 ) =>
823 {
824 BlockState {
825 name: AIR_BLOCK_NAME.to_string(),
826 states: BTreeMap::new(),
827 version: Some(DEFAULT_BLOCK_VERSION),
828 }
829 }
830 Err(error) => return Err(error),
831 };
832 let entry = McStructurePaletteEntry::from_block_state(&state);
833 let key = entry.key();
834 let palette_index = if let Some(existing) = palette_indices.get(&key) {
835 *existing
836 } else {
837 let new_index = i32::try_from(structure.palette.len()).map_err(|_| {
838 BedrockWorldError::Validation(
839 "mcstructure palette length exceeds i32".to_string(),
840 )
841 })?;
842 structure.palette.push(entry);
843 palette_indices.insert(key, new_index);
844 new_index
845 };
846 let block_index = size.index(x, y, z)?;
847 structure.primary_indices[block_index] = palette_index;
848 }
849 }
850 }
851
852 Ok(structure)
853 }
854}
855
856pub fn read_mcstructure_file(path: &Path) -> Result<McStructureFile> {
858 let bytes = std::fs::read(path)?;
859 McStructureFile::from_bytes(&bytes)
860}
861
862pub fn write_mcstructure_file(path: &Path, structure: &McStructureFile) -> Result<()> {
864 let bytes = structure.to_bytes()?;
865 std::fs::write(path, bytes)?;
866 Ok(())
867}
868
869struct StructureBlockPlacement {
870 chunk: ChunkPos,
871 subchunk_y: i8,
872 local_x: u8,
873 local_y: u8,
874 local_z: u8,
875 primary: McStructurePaletteEntry,
876 secondary: McStructurePaletteEntry,
877}
878
879#[derive(Clone)]
880struct EntrySubchunkBuilder {
881 primary: Vec<McStructurePaletteEntry>,
882 secondary: Vec<McStructurePaletteEntry>,
883}
884
885impl EntrySubchunkBuilder {
886 fn new_air() -> Self {
887 let air = McStructurePaletteEntry::air();
888 Self {
889 primary: vec![air.clone(); BLOCKS_PER_SUBCHUNK],
890 secondary: vec![air; BLOCKS_PER_SUBCHUNK],
891 }
892 }
893
894 fn from_chunk(chunk: &crate::Chunk, y: i8) -> Result<Self> {
895 let Some(subchunk) = chunk.get_subchunk(y)? else {
896 return Ok(Self::new_air());
897 };
898 let SubChunkFormat::Paletted { storages, .. } = &subchunk.format else {
899 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
900 "chunk {},{} subchunk {y} is not a mergeable paletted format",
901 chunk.pos.x, chunk.pos.z
902 )));
903 };
904 if storages.len() > 2 && storages.iter().skip(2).any(block_palette_contains_non_air) {
905 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
906 "chunk {},{} subchunk {y} contains non-air blocks above two layers",
907 chunk.pos.x, chunk.pos.z
908 )));
909 }
910
911 let mut builder = Self::new_air();
912 if let Some(primary) = storages.first() {
913 fill_entries_from_palette(primary, &mut builder.primary)?;
914 }
915 if let Some(secondary) = storages.get(1) {
916 fill_entries_from_palette(secondary, &mut builder.secondary)?;
917 }
918 Ok(builder)
919 }
920}
921
922fn block_palette_contains_non_air(palette: &BlockPalette) -> bool {
923 palette.counts.as_ref().is_some_and(|counts| {
924 palette
925 .states
926 .iter()
927 .zip(counts)
928 .any(|(state, count)| *count > 0 && state.name != AIR_BLOCK_NAME)
929 })
930}
931
932fn fill_entries_from_palette(
933 palette: &BlockPalette,
934 entries: &mut [McStructurePaletteEntry],
935) -> Result<()> {
936 if entries.len() != BLOCKS_PER_SUBCHUNK {
937 return Err(BedrockWorldError::Validation(format!(
938 "subchunk entry count invalid: {}",
939 entries.len()
940 )));
941 }
942 let Some(indices) = palette.indices.as_ref() else {
943 return Err(BedrockWorldError::UnsupportedChunkFormat(
944 "subchunk palette has no full indices".to_string(),
945 ));
946 };
947 if indices.len() != BLOCKS_PER_SUBCHUNK {
948 return Err(BedrockWorldError::Validation(format!(
949 "subchunk palette index count invalid: {}",
950 indices.len()
951 )));
952 }
953 for (storage_index, palette_index) in indices.iter().enumerate() {
954 let state = palette
955 .states
956 .get(usize::from(*palette_index))
957 .ok_or_else(|| {
958 BedrockWorldError::CorruptWorld(format!(
959 "subchunk palette index {} out of range {}",
960 palette_index,
961 palette.states.len()
962 ))
963 })?;
964 entries[storage_index] = McStructurePaletteEntry::from_block_state(state);
965 }
966 Ok(())
967}
968
969fn encode_entry_subchunk(subchunk: EntrySubchunkBuilder) -> Result<Vec<u8>> {
970 let secondary_has_blocks = subchunk.secondary.iter().any(|entry| !entry.is_air());
971 let mut bytes = vec![8, if secondary_has_blocks { 2 } else { 1 }];
972 bytes.extend_from_slice(&encode_palette_storage_entries(&subchunk.primary)?);
973 if secondary_has_blocks {
974 bytes.extend_from_slice(&encode_palette_storage_entries(&subchunk.secondary)?);
975 }
976 Ok(bytes)
977}
978
979fn encode_palette_storage_entries(entries: &[McStructurePaletteEntry]) -> Result<Vec<u8>> {
980 if entries.len() != BLOCKS_PER_SUBCHUNK {
981 return Err(BedrockWorldError::Validation(format!(
982 "subchunk palette entry count invalid: {}",
983 entries.len()
984 )));
985 }
986 let mut palette = vec![McStructurePaletteEntry::air()];
987 let mut palette_lookup = HashMap::from([(palette[0].key(), 0_u16)]);
988 let mut local_indices = Vec::with_capacity(BLOCKS_PER_SUBCHUNK);
989 for entry in entries {
990 let key = entry.key();
991 let local_index = if let Some(existing) = palette_lookup.get(&key) {
992 *existing
993 } else {
994 let new_index = u16::try_from(palette.len()).map_err(|_| {
995 BedrockWorldError::Validation("subchunk palette is too large".to_string())
996 })?;
997 palette.push(entry.clone());
998 palette_lookup.insert(key, new_index);
999 new_index
1000 };
1001 local_indices.push(local_index);
1002 }
1003
1004 let bits = bits_per_palette_index(palette.len())?;
1005 let mut bytes = Vec::new();
1006 bytes.push(bits << 1);
1007 if bits > 0 {
1008 for word in pack_palette_indices(&local_indices, bits)? {
1009 bytes.extend_from_slice(&word.to_le_bytes());
1010 }
1011 bytes.extend_from_slice(
1012 &i32::try_from(palette.len())
1013 .map_err(|_| {
1014 BedrockWorldError::Validation(
1015 "subchunk palette length does not fit i32".to_string(),
1016 )
1017 })?
1018 .to_le_bytes(),
1019 );
1020 }
1021 for entry in &palette {
1022 bytes.extend_from_slice(&NbtWriter::write_root(&entry.to_nbt())?);
1023 }
1024 Ok(bytes)
1025}
1026
1027fn bits_per_palette_index(palette_len: usize) -> Result<u8> {
1028 match palette_len {
1029 0 | 1 => Ok(0),
1030 2 => Ok(1),
1031 3..=4 => Ok(2),
1032 5..=8 => Ok(3),
1033 9..=16 => Ok(4),
1034 17..=32 => Ok(5),
1035 33..=64 => Ok(6),
1036 65..=256 => Ok(8),
1037 257..=4096 => Ok(16),
1038 _ => Err(BedrockWorldError::Validation(format!(
1039 "subchunk palette length exceeds 4096: {palette_len}"
1040 ))),
1041 }
1042}
1043
1044fn pack_palette_indices(indices: &[u16], bits: u8) -> Result<Vec<u32>> {
1045 if bits == 0 {
1046 return Ok(Vec::new());
1047 }
1048 let values_per_word = usize::from(32 / bits);
1049 let mask = (1_u32 << bits) - 1;
1050 let word_count = indices.len().div_ceil(values_per_word);
1051 let mut words = vec![0_u32; word_count];
1052 for (index, value) in indices.iter().enumerate() {
1053 let value = u32::from(*value);
1054 if value > mask {
1055 return Err(BedrockWorldError::Validation(format!(
1056 "palette index {value} does not fit {bits} bits"
1057 )));
1058 }
1059 let word_index = index / values_per_word;
1060 let shift = (index % values_per_word) * usize::from(bits);
1061 let Some(word) = words.get_mut(word_index) else {
1062 return Err(BedrockWorldError::Validation(
1063 "packed palette word index out of bounds".to_string(),
1064 ));
1065 };
1066 *word |= value << shift;
1067 }
1068 Ok(words)
1069}
1070
1071fn chunk_has_biome_record(records: &[ChunkRecord]) -> bool {
1072 records.iter().any(|record| {
1073 matches!(
1074 record.key.tag,
1075 ChunkRecordTag::Data2D | ChunkRecordTag::Data2DLegacy | ChunkRecordTag::Data3D
1076 )
1077 })
1078}
1079
1080fn default_data3d_bytes() -> Result<Vec<u8>> {
1081 Biome3d::new(vec![0; 256], Vec::new()).and_then(|biome| biome.encode())
1082}
1083
1084fn checked_mul_16(value: i32, label: &str) -> Result<i32> {
1085 value.checked_mul(16).ok_or_else(|| {
1086 BedrockWorldError::Validation(format!(
1087 "{label} coordinate overflowed while multiplying by 16"
1088 ))
1089 })
1090}
1091
1092fn checked_add_i32(left: i32, right: i32, label: &str) -> Result<i32> {
1093 left.checked_add(right).ok_or_else(|| {
1094 BedrockWorldError::Validation(format!("{label} coordinate addition overflowed"))
1095 })
1096}
1097
1098fn mirrored_structure_offset(size: i32, offset: i32, mirror: bool, axis: &str) -> Result<i32> {
1099 if !mirror {
1100 return Ok(offset);
1101 }
1102 let max_offset = size.checked_sub(1).ok_or_else(|| {
1103 BedrockWorldError::Validation(format!("structure {axis} size must be positive"))
1104 })?;
1105 max_offset.checked_sub(offset).ok_or_else(|| {
1106 BedrockWorldError::Validation(format!(
1107 "structure {axis} offset is outside mirrored bounds"
1108 ))
1109 })
1110}
1111
1112fn transform_palette_entry(
1113 mut entry: McStructurePaletteEntry,
1114 placement: McStructurePlacement,
1115) -> McStructurePaletteEntry {
1116 if entry.states.is_empty() || !placement_changes_horizontal_state(placement) {
1117 return entry;
1118 }
1119
1120 let is_trapdoor = is_trapdoor_block_name(&entry.name);
1121 transform_direction_string_state(&mut entry.states, "minecraft:cardinal_direction", placement);
1122 transform_direction_string_state(&mut entry.states, "cardinal_direction", placement);
1123 transform_direction_string_state(&mut entry.states, "facing", placement);
1124 transform_direction_string_state(&mut entry.states, "facing_direction", placement);
1125 transform_direction_string_state(&mut entry.states, "minecraft:block_face", placement);
1126 transform_direction_string_state(&mut entry.states, "block_face", placement);
1127 transform_direction_string_state(&mut entry.states, "torch_facing_direction", placement);
1128 transform_direction_string_state(&mut entry.states, "vine_direction", placement);
1129
1130 transform_facing_direction_state(&mut entry.states, "facing_direction", placement);
1131 transform_facing_direction_state(&mut entry.states, "minecraft:facing_direction", placement);
1132 if is_trapdoor {
1133 transform_trapdoor_direction_state(&mut entry.states, "direction", placement);
1134 transform_trapdoor_direction_state(&mut entry.states, "minecraft:direction", placement);
1135 } else {
1136 transform_cardinal_direction_state(&mut entry.states, "direction", placement);
1137 transform_cardinal_direction_state(&mut entry.states, "minecraft:direction", placement);
1138 }
1139 transform_cardinal_direction_state(&mut entry.states, "weirdo_direction", placement);
1140 transform_cardinal_direction_state(&mut entry.states, "minecraft:weirdo_direction", placement);
1141 transform_sixteen_way_direction_state(&mut entry.states, "ground_sign_direction", placement);
1142 transform_sixteen_way_direction_state(
1143 &mut entry.states,
1144 "minecraft:ground_sign_direction",
1145 placement,
1146 );
1147
1148 transform_directional_state_group(
1149 &mut entry.states,
1150 |direction| direction.state_key().to_string(),
1151 placement,
1152 );
1153 transform_directional_state_group(
1154 &mut entry.states,
1155 |direction| format!("minecraft:{}", direction.state_key()),
1156 placement,
1157 );
1158 transform_directional_state_group(
1159 &mut entry.states,
1160 |direction| format!("{}_bit", direction.state_key()),
1161 placement,
1162 );
1163 transform_directional_state_group(
1164 &mut entry.states,
1165 |direction| format!("connected_{}", direction.state_key()),
1166 placement,
1167 );
1168 transform_directional_state_group(
1169 &mut entry.states,
1170 |direction| format!("{}_connection_bit", direction.state_key()),
1171 placement,
1172 );
1173 transform_directional_state_group(
1174 &mut entry.states,
1175 |direction| format!("{}_wall_bit", direction.state_key()),
1176 placement,
1177 );
1178 transform_directional_state_group(
1179 &mut entry.states,
1180 |direction| format!("{}_connection_type", direction.state_key()),
1181 placement,
1182 );
1183 transform_directional_state_group(
1184 &mut entry.states,
1185 |direction| format!("wall_connection_type_{}", direction.state_key()),
1186 placement,
1187 );
1188
1189 transform_axis_state(&mut entry.states, "axis", placement);
1190 transform_axis_state(&mut entry.states, "minecraft:axis", placement);
1191 transform_axis_state(&mut entry.states, "pillar_axis", placement);
1192 transform_axis_state(&mut entry.states, "minecraft:pillar_axis", placement);
1193 transform_axis_state(&mut entry.states, "portal_axis", placement);
1194 transform_axis_state(&mut entry.states, "minecraft:portal_axis", placement);
1195 transform_left_right_shape_state(&mut entry.states, "shape", placement);
1196 transform_left_right_shape_state(&mut entry.states, "minecraft:shape", placement);
1197
1198 entry
1199}
1200
1201fn is_trapdoor_block_name(name: &str) -> bool {
1202 let name = name.strip_prefix("minecraft:").unwrap_or(name);
1203 name == "trapdoor" || name.ends_with("_trapdoor")
1204}
1205
1206const fn placement_changes_horizontal_state(placement: McStructurePlacement) -> bool {
1207 placement.mirror_x
1208 || placement.mirror_z
1209 || !matches!(placement.rotation, McStructureRotation::None)
1210}
1211
1212#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1213enum HorizontalDirection {
1214 North,
1215 South,
1216 East,
1217 West,
1218}
1219
1220impl HorizontalDirection {
1221 const ALL: [Self; 4] = [Self::North, Self::South, Self::East, Self::West];
1222
1223 const fn state_key(self) -> &'static str {
1224 match self {
1225 Self::North => "north",
1226 Self::South => "south",
1227 Self::East => "east",
1228 Self::West => "west",
1229 }
1230 }
1231
1232 const fn xz(self) -> (i32, i32) {
1233 match self {
1234 Self::North => (0, -1),
1235 Self::South => (0, 1),
1236 Self::East => (1, 0),
1237 Self::West => (-1, 0),
1238 }
1239 }
1240
1241 const fn from_xz(x: i32, z: i32) -> Option<Self> {
1242 match (x, z) {
1243 (0, -1) => Some(Self::North),
1244 (0, 1) => Some(Self::South),
1245 (1, 0) => Some(Self::East),
1246 (-1, 0) => Some(Self::West),
1247 _ => None,
1248 }
1249 }
1250
1251 fn transform(self, placement: McStructurePlacement) -> Self {
1252 let (mut x, mut z) = self.xz();
1253 if placement.mirror_x {
1254 x = -x;
1255 }
1256 if placement.mirror_z {
1257 z = -z;
1258 }
1259 let (x, z) = placement.rotation.rotate_chunk_delta(x, z);
1260 Self::from_xz(x, z).unwrap_or(self)
1261 }
1262
1263 const fn as_str(self) -> &'static str {
1264 self.state_key()
1265 }
1266
1267 fn from_string(value: &str) -> Option<Self> {
1268 match value {
1269 "north" => Some(Self::North),
1270 "south" => Some(Self::South),
1271 "east" => Some(Self::East),
1272 "west" => Some(Self::West),
1273 _ => None,
1274 }
1275 }
1276
1277 fn from_cardinal_int(value: i32) -> Option<Self> {
1278 match value.rem_euclid(4) {
1279 0 => Some(Self::South),
1280 1 => Some(Self::West),
1281 2 => Some(Self::North),
1282 3 => Some(Self::East),
1283 _ => None,
1284 }
1285 }
1286
1287 const fn to_cardinal_int(self) -> i32 {
1288 match self {
1289 Self::South => 0,
1290 Self::West => 1,
1291 Self::North => 2,
1292 Self::East => 3,
1293 }
1294 }
1295
1296 const fn from_facing_int(value: i32) -> Option<Self> {
1297 match value {
1298 2 => Some(Self::North),
1299 3 => Some(Self::South),
1300 4 => Some(Self::West),
1301 5 => Some(Self::East),
1302 _ => None,
1303 }
1304 }
1305
1306 const fn to_facing_int(self) -> i32 {
1307 match self {
1308 Self::North => 2,
1309 Self::South => 3,
1310 Self::West => 4,
1311 Self::East => 5,
1312 }
1313 }
1314
1315 const fn from_trapdoor_direction_int(value: i32) -> Option<Self> {
1316 match value.rem_euclid(4) {
1317 0 => Some(Self::West),
1318 1 => Some(Self::East),
1319 2 => Some(Self::North),
1320 3 => Some(Self::South),
1321 _ => None,
1322 }
1323 }
1324
1325 const fn to_trapdoor_direction_int(self) -> i32 {
1326 match self {
1327 Self::West => 0,
1328 Self::East => 1,
1329 Self::North => 2,
1330 Self::South => 3,
1331 }
1332 }
1333}
1334
1335fn transform_direction_string_state(
1336 states: &mut BTreeMap<String, NbtTag>,
1337 key: &str,
1338 placement: McStructurePlacement,
1339) {
1340 let Some(NbtTag::String(value)) = states.get_mut(key) else {
1341 return;
1342 };
1343 let Some(direction) = HorizontalDirection::from_string(value) else {
1344 return;
1345 };
1346 *value = direction.transform(placement).as_str().to_string();
1347}
1348
1349fn transform_facing_direction_state(
1350 states: &mut BTreeMap<String, NbtTag>,
1351 key: &str,
1352 placement: McStructurePlacement,
1353) {
1354 let Some(value) = states.get_mut(key) else {
1355 return;
1356 };
1357 let Some(direction) = nbt_i32_value(value).and_then(HorizontalDirection::from_facing_int)
1358 else {
1359 return;
1360 };
1361 set_nbt_i32_value(value, direction.transform(placement).to_facing_int());
1362}
1363
1364fn transform_cardinal_direction_state(
1365 states: &mut BTreeMap<String, NbtTag>,
1366 key: &str,
1367 placement: McStructurePlacement,
1368) {
1369 let Some(value) = states.get_mut(key) else {
1370 return;
1371 };
1372 let Some(direction) = nbt_i32_value(value).and_then(HorizontalDirection::from_cardinal_int)
1373 else {
1374 return;
1375 };
1376 set_nbt_i32_value(value, direction.transform(placement).to_cardinal_int());
1377}
1378
1379fn transform_trapdoor_direction_state(
1380 states: &mut BTreeMap<String, NbtTag>,
1381 key: &str,
1382 placement: McStructurePlacement,
1383) {
1384 let Some(value) = states.get_mut(key) else {
1385 return;
1386 };
1387 let Some(direction) =
1388 nbt_i32_value(value).and_then(HorizontalDirection::from_trapdoor_direction_int)
1389 else {
1390 return;
1391 };
1392 set_nbt_i32_value(
1393 value,
1394 direction.transform(placement).to_trapdoor_direction_int(),
1395 );
1396}
1397
1398fn transform_sixteen_way_direction_state(
1399 states: &mut BTreeMap<String, NbtTag>,
1400 key: &str,
1401 placement: McStructurePlacement,
1402) {
1403 let Some(value) = states.get_mut(key) else {
1404 return;
1405 };
1406 let Some(raw_step) = nbt_i32_value(value) else {
1407 return;
1408 };
1409 let mut step = raw_step.rem_euclid(16);
1410 if placement.mirror_x {
1411 step = (16 - step).rem_euclid(16);
1412 }
1413 if placement.mirror_z {
1414 step = (8 - step).rem_euclid(16);
1415 }
1416 step = match placement.rotation {
1417 McStructureRotation::None => step,
1418 McStructureRotation::Clockwise90 => step.saturating_add(4).rem_euclid(16),
1419 McStructureRotation::Rotate180 => step.saturating_add(8).rem_euclid(16),
1420 McStructureRotation::CounterClockwise90 => step.saturating_add(12).rem_euclid(16),
1421 };
1422 set_nbt_i32_value(value, step);
1423}
1424
1425fn transform_directional_state_group(
1426 states: &mut BTreeMap<String, NbtTag>,
1427 key_for: impl Fn(HorizontalDirection) -> String,
1428 placement: McStructurePlacement,
1429) {
1430 let mut values = Vec::new();
1431 for direction in HorizontalDirection::ALL {
1432 let key = key_for(direction);
1433 if let Some(value) = states.remove(&key) {
1434 values.push((direction, value));
1435 }
1436 }
1437 if values.is_empty() {
1438 return;
1439 }
1440 for (direction, value) in values {
1441 states.insert(key_for(direction.transform(placement)), value);
1442 }
1443}
1444
1445fn transform_axis_state(
1446 states: &mut BTreeMap<String, NbtTag>,
1447 key: &str,
1448 placement: McStructurePlacement,
1449) {
1450 if !matches!(
1451 placement.rotation,
1452 McStructureRotation::Clockwise90 | McStructureRotation::CounterClockwise90
1453 ) {
1454 return;
1455 }
1456 let Some(NbtTag::String(value)) = states.get_mut(key) else {
1457 return;
1458 };
1459 match value.as_str() {
1460 "x" => *value = "z".to_string(),
1461 "z" => *value = "x".to_string(),
1462 _ => {}
1463 }
1464}
1465
1466fn transform_left_right_shape_state(
1467 states: &mut BTreeMap<String, NbtTag>,
1468 key: &str,
1469 placement: McStructurePlacement,
1470) {
1471 if placement.mirror_x == placement.mirror_z {
1472 return;
1473 }
1474 let Some(NbtTag::String(value)) = states.get_mut(key) else {
1475 return;
1476 };
1477 match value.as_str() {
1478 "inner_left" => *value = "inner_right".to_string(),
1479 "inner_right" => *value = "inner_left".to_string(),
1480 "outer_left" => *value = "outer_right".to_string(),
1481 "outer_right" => *value = "outer_left".to_string(),
1482 _ => {}
1483 }
1484}
1485
1486fn set_nbt_i32_value(tag: &mut NbtTag, value: i32) {
1487 match tag {
1488 NbtTag::Byte(current) => match i8::try_from(value) {
1489 Ok(value) => *current = value,
1490 Err(_) => *tag = NbtTag::Int(value),
1491 },
1492 NbtTag::Short(current) => match i16::try_from(value) {
1493 Ok(value) => *current = value,
1494 Err(_) => *tag = NbtTag::Int(value),
1495 },
1496 NbtTag::Int(current) => *current = value,
1497 NbtTag::Long(current) => *current = i64::from(value),
1498 _ => {}
1499 }
1500}
1501
1502fn compound_position_matches(tag: &NbtTag, x: i32, y: i32, z: i32) -> bool {
1503 let NbtTag::Compound(root) = tag else {
1504 return false;
1505 };
1506 root.get("x").and_then(nbt_i32_value) == Some(x)
1507 && root.get("y").and_then(nbt_i32_value) == Some(y)
1508 && root.get("z").and_then(nbt_i32_value) == Some(z)
1509}
1510
1511fn nbt_i32_value(tag: &NbtTag) -> Option<i32> {
1512 match tag {
1513 NbtTag::Byte(value) => Some(i32::from(*value)),
1514 NbtTag::Short(value) => Some(i32::from(*value)),
1515 NbtTag::Int(value) => Some(*value),
1516 NbtTag::Long(value) => i32::try_from(*value).ok(),
1517 _ => None,
1518 }
1519}
1520
1521fn palette_entry_from_nbt(tag: &NbtTag) -> Result<McStructurePaletteEntry> {
1522 let NbtTag::Compound(root) = tag else {
1523 return Err(BedrockWorldError::Nbt(
1524 "block palette entry must be a compound".to_string(),
1525 ));
1526 };
1527 let name = compound_string(root, "name")?.to_string();
1528 let states = match root.get("states") {
1529 Some(NbtTag::Compound(values)) => values
1530 .iter()
1531 .map(|(key, value)| (key.clone(), value.clone()))
1532 .collect(),
1533 Some(_) => {
1534 return Err(BedrockWorldError::Nbt(
1535 "block palette states must be a compound".to_string(),
1536 ));
1537 }
1538 None => BTreeMap::new(),
1539 };
1540 let version = match root.get("version") {
1541 Some(value) => Some(nbt_i32(value)?),
1542 None => None,
1543 };
1544 Ok(McStructurePaletteEntry {
1545 name,
1546 states,
1547 version,
1548 })
1549}
1550
1551fn compound_child<'a>(
1552 root: &'a IndexMap<String, NbtTag>,
1553 key: &str,
1554) -> Result<&'a IndexMap<String, NbtTag>> {
1555 match root.get(key) {
1556 Some(NbtTag::Compound(value)) => Ok(value),
1557 Some(_) => Err(BedrockWorldError::Nbt(format!("{key} must be a compound"))),
1558 None => Err(BedrockWorldError::Nbt(format!("{key} is missing"))),
1559 }
1560}
1561
1562fn compound_list<'a>(root: &'a IndexMap<String, NbtTag>, key: &str) -> Result<&'a [NbtTag]> {
1563 match root.get(key) {
1564 Some(NbtTag::List(values)) => Ok(values),
1565 Some(_) => Err(BedrockWorldError::Nbt(format!("{key} must be a list"))),
1566 None => Err(BedrockWorldError::Nbt(format!("{key} is missing"))),
1567 }
1568}
1569
1570fn compound_i32(root: &IndexMap<String, NbtTag>, key: &str) -> Result<i32> {
1571 let value = root
1572 .get(key)
1573 .ok_or_else(|| BedrockWorldError::Nbt(format!("{key} is missing")))?;
1574 nbt_i32(value)
1575}
1576
1577fn compound_i32_list(
1578 root: &IndexMap<String, NbtTag>,
1579 key: &str,
1580 expected_len: usize,
1581) -> Result<Vec<i32>> {
1582 let values = compound_list(root, key)?;
1583 if values.len() != expected_len {
1584 return Err(BedrockWorldError::Validation(format!(
1585 "{key} must contain {expected_len} integers, got {}",
1586 values.len()
1587 )));
1588 }
1589 values.iter().map(nbt_i32).collect()
1590}
1591
1592fn compound_string<'a>(root: &'a IndexMap<String, NbtTag>, key: &str) -> Result<&'a str> {
1593 match root.get(key) {
1594 Some(NbtTag::String(value)) => Ok(value),
1595 Some(_) => Err(BedrockWorldError::Nbt(format!("{key} must be a string"))),
1596 None => Err(BedrockWorldError::Nbt(format!("{key} is missing"))),
1597 }
1598}
1599
1600fn nbt_i32(tag: &NbtTag) -> Result<i32> {
1601 match tag {
1602 NbtTag::Byte(value) => Ok(i32::from(*value)),
1603 NbtTag::Short(value) => Ok(i32::from(*value)),
1604 NbtTag::Int(value) => Ok(*value),
1605 NbtTag::Long(value) => i32::try_from(*value).map_err(|_| {
1606 BedrockWorldError::Validation(format!("integer value {value} does not fit i32"))
1607 }),
1608 _ => Err(BedrockWorldError::Nbt(
1609 "expected integer NBT tag".to_string(),
1610 )),
1611 }
1612}
1613
1614fn nbt_i32_list(tag: &NbtTag) -> Result<Vec<i32>> {
1615 match tag {
1616 NbtTag::List(values) => values.iter().map(nbt_i32).collect(),
1617 NbtTag::IntArray(values) => Ok(values.clone()),
1618 _ => Err(BedrockWorldError::Nbt(
1619 "expected integer list NBT tag".to_string(),
1620 )),
1621 }
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626 use super::*;
1627 use crate::Dimension;
1628
1629 #[test]
1630 fn mcstructure_roundtrip_preserves_core_fields() {
1631 let size = McStructureSize::new(2, 3, 4).expect("valid size");
1632 let mut structure = McStructureFile::new_air(size, [10, 64, -3]).expect("air structure");
1633 structure.palette.push(McStructurePaletteEntry {
1634 name: "minecraft:stone".to_string(),
1635 states: BTreeMap::new(),
1636 version: Some(1),
1637 });
1638 let index = size.index(1, 2, 3).expect("index");
1639 structure.primary_indices[index] = 1;
1640
1641 let bytes = structure.to_bytes().expect("serialize");
1642 let parsed = McStructureFile::from_bytes(&bytes).expect("parse");
1643
1644 assert_eq!(parsed.size, size);
1645 assert_eq!(parsed.world_origin, [10, 64, -3]);
1646 assert_eq!(parsed.palette.len(), 2);
1647 assert_eq!(parsed.primary_index_at(1, 2, 3).expect("primary"), 1);
1648 assert_eq!(parsed.primary_index_at(0, 0, 0).expect("air"), 0);
1649 }
1650
1651 #[test]
1652 fn mcstructure_index_order_is_x_then_y_then_z() {
1653 let size = McStructureSize::new(2, 3, 4).expect("valid size");
1654
1655 assert_eq!(size.index(0, 0, 0).expect("index"), 0);
1656 assert_eq!(size.index(0, 0, 3).expect("index"), 3);
1657 assert_eq!(size.index(0, 1, 0).expect("index"), 4);
1658 assert_eq!(size.index(1, 0, 0).expect("index"), 12);
1659 assert_eq!(size.index(1, 2, 3).expect("index"), 23);
1660 }
1661
1662 #[test]
1663 fn mcstructure_target_chunks_support_mirror_transform() {
1664 let size = McStructureSize::new(33, 4, 49).expect("valid size");
1665 let structure = McStructureFile::new_air(size, [0, 64, 0]).expect("air structure");
1666 let placement = McStructurePlacement {
1667 source_anchor: ChunkPos {
1668 x: 4,
1669 z: 10,
1670 dimension: Dimension::Overworld,
1671 },
1672 target_anchor: ChunkPos {
1673 x: -20,
1674 z: 30,
1675 dimension: Dimension::End,
1676 },
1677 origin_y: 64,
1678 rotation: McStructureRotation::None,
1679 mirror_x: true,
1680 mirror_z: true,
1681 };
1682
1683 assert_eq!(
1684 structure.target_chunks(placement).expect("target chunks"),
1685 BTreeSet::from([
1686 ChunkPos {
1687 x: -20,
1688 z: 30,
1689 dimension: Dimension::End,
1690 },
1691 ChunkPos {
1692 x: -20,
1693 z: 31,
1694 dimension: Dimension::End,
1695 },
1696 ChunkPos {
1697 x: -20,
1698 z: 32,
1699 dimension: Dimension::End,
1700 },
1701 ChunkPos {
1702 x: -20,
1703 z: 33,
1704 dimension: Dimension::End,
1705 },
1706 ChunkPos {
1707 x: -19,
1708 z: 30,
1709 dimension: Dimension::End,
1710 },
1711 ChunkPos {
1712 x: -19,
1713 z: 31,
1714 dimension: Dimension::End,
1715 },
1716 ChunkPos {
1717 x: -19,
1718 z: 32,
1719 dimension: Dimension::End,
1720 },
1721 ChunkPos {
1722 x: -19,
1723 z: 33,
1724 dimension: Dimension::End,
1725 },
1726 ChunkPos {
1727 x: -18,
1728 z: 30,
1729 dimension: Dimension::End,
1730 },
1731 ChunkPos {
1732 x: -18,
1733 z: 31,
1734 dimension: Dimension::End,
1735 },
1736 ChunkPos {
1737 x: -18,
1738 z: 32,
1739 dimension: Dimension::End,
1740 },
1741 ChunkPos {
1742 x: -18,
1743 z: 33,
1744 dimension: Dimension::End,
1745 },
1746 ])
1747 );
1748 }
1749
1750 #[test]
1751 fn mcstructure_block_placement_supports_mirror_local_coordinates() {
1752 let size = McStructureSize::new(4, 2, 4).expect("valid size");
1753 let structure = McStructureFile::new_air(size, [0, 64, 0]).expect("air structure");
1754 let placement = McStructurePlacement {
1755 source_anchor: ChunkPos {
1756 x: 0,
1757 z: 0,
1758 dimension: Dimension::Overworld,
1759 },
1760 target_anchor: ChunkPos {
1761 x: 8,
1762 z: -3,
1763 dimension: Dimension::End,
1764 },
1765 origin_y: 70,
1766 rotation: McStructureRotation::None,
1767 mirror_x: true,
1768 mirror_z: true,
1769 };
1770
1771 let block_placement = structure
1772 .structure_block_placement(
1773 &McStructureBlock {
1774 x: 1,
1775 y: 1,
1776 z: 2,
1777 primary: 0,
1778 secondary: -1,
1779 },
1780 placement,
1781 )
1782 .expect("block placement");
1783
1784 assert_eq!(block_placement.chunk, placement.target_anchor);
1785 assert_eq!(block_placement.local_x, 2);
1786 assert_eq!(block_placement.local_y, 7);
1787 assert_eq!(block_placement.local_z, 1);
1788 }
1789
1790 #[test]
1791 fn mcstructure_block_placement_transforms_horizontal_block_states() {
1792 let size = McStructureSize::new(1, 1, 1).expect("valid size");
1793 let mut structure = McStructureFile::new_air(size, [0, 64, 0]).expect("air structure");
1794 structure.palette.push(McStructurePaletteEntry {
1795 name: "minecraft:oak_trapdoor".to_string(),
1796 states: BTreeMap::from([("facing_direction".to_string(), NbtTag::Byte(4))]),
1797 version: Some(1),
1798 });
1799 structure.primary_indices[0] = 1;
1800 let placement = McStructurePlacement {
1801 source_anchor: ChunkPos {
1802 x: 0,
1803 z: 0,
1804 dimension: Dimension::Overworld,
1805 },
1806 target_anchor: ChunkPos {
1807 x: 0,
1808 z: 0,
1809 dimension: Dimension::Overworld,
1810 },
1811 origin_y: 64,
1812 rotation: McStructureRotation::None,
1813 mirror_x: true,
1814 mirror_z: false,
1815 };
1816
1817 let block_placement = structure
1818 .structure_block_placement(
1819 &McStructureBlock {
1820 x: 0,
1821 y: 0,
1822 z: 0,
1823 primary: 1,
1824 secondary: -1,
1825 },
1826 placement,
1827 )
1828 .expect("block placement");
1829
1830 assert_eq!(
1831 block_placement.primary.states.get("facing_direction"),
1832 Some(&NbtTag::Byte(5))
1833 );
1834 }
1835
1836 #[test]
1837 fn mcstructure_block_placement_transforms_trapdoor_direction_state() {
1838 let size = McStructureSize::new(1, 1, 1).expect("valid size");
1839 let mut structure = McStructureFile::new_air(size, [0, 64, 0]).expect("air structure");
1840 structure.palette.push(McStructurePaletteEntry {
1841 name: "minecraft:oak_trapdoor".to_string(),
1842 states: BTreeMap::from([("direction".to_string(), NbtTag::Byte(0))]),
1843 version: Some(1),
1844 });
1845 structure.primary_indices[0] = 1;
1846 let placement = McStructurePlacement {
1847 source_anchor: ChunkPos {
1848 x: 0,
1849 z: 0,
1850 dimension: Dimension::Overworld,
1851 },
1852 target_anchor: ChunkPos {
1853 x: 0,
1854 z: 0,
1855 dimension: Dimension::Overworld,
1856 },
1857 origin_y: 64,
1858 rotation: McStructureRotation::None,
1859 mirror_x: true,
1860 mirror_z: false,
1861 };
1862
1863 let block_placement = structure
1864 .structure_block_placement(
1865 &McStructureBlock {
1866 x: 0,
1867 y: 0,
1868 z: 0,
1869 primary: 1,
1870 secondary: -1,
1871 },
1872 placement,
1873 )
1874 .expect("block placement");
1875
1876 assert_eq!(
1877 block_placement.primary.states.get("direction"),
1878 Some(&NbtTag::Byte(1))
1879 );
1880 }
1881
1882 #[test]
1883 fn mcstructure_block_placement_transforms_connection_state_keys() {
1884 let size = McStructureSize::new(1, 1, 1).expect("valid size");
1885 let mut structure = McStructureFile::new_air(size, [0, 64, 0]).expect("air structure");
1886 structure.palette.push(McStructurePaletteEntry {
1887 name: "minecraft:glass_pane".to_string(),
1888 states: BTreeMap::from([("north".to_string(), NbtTag::Byte(1))]),
1889 version: Some(1),
1890 });
1891 structure.primary_indices[0] = 1;
1892 let placement = McStructurePlacement {
1893 source_anchor: ChunkPos {
1894 x: 0,
1895 z: 0,
1896 dimension: Dimension::Overworld,
1897 },
1898 target_anchor: ChunkPos {
1899 x: 0,
1900 z: 0,
1901 dimension: Dimension::Overworld,
1902 },
1903 origin_y: 64,
1904 rotation: McStructureRotation::Clockwise90,
1905 mirror_x: false,
1906 mirror_z: false,
1907 };
1908
1909 let block_placement = structure
1910 .structure_block_placement(
1911 &McStructureBlock {
1912 x: 0,
1913 y: 0,
1914 z: 0,
1915 primary: 1,
1916 secondary: -1,
1917 },
1918 placement,
1919 )
1920 .expect("block placement");
1921
1922 assert_eq!(block_placement.primary.states.get("north"), None);
1923 assert_eq!(
1924 block_placement.primary.states.get("east"),
1925 Some(&NbtTag::Byte(1))
1926 );
1927 }
1928}