Skip to main content

bedrock_world/
mcstructure.rs

1//! Minecraft Bedrock `.mcstructure` files and world placement helpers.
2//!
3//! Structure files are uncompressed little-endian Bedrock NBT. The block index
4//! arrays use the structure file order documented by Bedrock tooling: X outer,
5//! then Y, then Z.
6
7use 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/// 3D size of a Bedrock structure.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct McStructureSize {
28    /// Size in the X direction.
29    pub x: i32,
30    /// Size in the Y direction.
31    pub y: i32,
32    /// Size in the Z direction.
33    pub z: i32,
34}
35
36impl McStructureSize {
37    /// Creates a validated structure size.
38    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    /// Total number of block positions.
59    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    /// Index in `.mcstructure` block index order.
72    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/// A block palette entry in a `.mcstructure` file.
94#[derive(Debug, Clone, PartialEq)]
95pub struct McStructurePaletteEntry {
96    /// Bedrock block identifier.
97    pub name: String,
98    /// Bedrock block states.
99    pub states: BTreeMap<String, NbtTag>,
100    /// Optional Bedrock block version.
101    pub version: Option<i32>,
102}
103
104impl McStructurePaletteEntry {
105    /// Creates a structure palette entry from a decoded chunk block state.
106    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    /// Air palette entry.
115    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/// A single block sampled from a structure.
158#[derive(Debug, Clone, PartialEq)]
159pub struct McStructureBlock {
160    /// X coordinate relative to the structure origin.
161    pub x: i32,
162    /// Y coordinate relative to the structure origin.
163    pub y: i32,
164    /// Z coordinate relative to the structure origin.
165    pub z: i32,
166    /// Primary-layer palette index, or `-1`.
167    pub primary: i32,
168    /// Secondary-layer palette index, or `-1`.
169    pub secondary: i32,
170}
171
172/// Rotation applied while placing a structure into a world.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum McStructureRotation {
175    /// Keep the structure orientation unchanged.
176    None,
177    /// Rotate clockwise around the Y axis.
178    Clockwise90,
179    /// Rotate 180 degrees around the Y axis.
180    Rotate180,
181    /// Rotate counter-clockwise around the Y axis.
182    CounterClockwise90,
183}
184
185impl McStructureRotation {
186    #[must_use]
187    /// Rotates chunk-relative offsets.
188    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/// Chunk anchor and height used to place a structure in a world.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct McStructurePlacement {
210    /// Chunk used as the source origin when the structure was copied/imported.
211    pub source_anchor: ChunkPos,
212    /// Chunk that receives the source anchor after placement.
213    pub target_anchor: ChunkPos,
214    /// World Y coordinate used as structure relative Y zero.
215    pub origin_y: i32,
216    /// Rotation applied around the Y axis.
217    pub rotation: McStructureRotation,
218    /// Mirror source X offsets before rotation.
219    pub mirror_x: bool,
220    /// Mirror source Z offsets before rotation.
221    pub mirror_z: bool,
222}
223
224/// Progress phase emitted by structure world writes.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum McStructureWritePhase {
227    /// The write is preparing and grouping block placements.
228    Prepare,
229    /// The write is merging structure data into chunk records.
230    WriteChunks,
231}
232
233/// Progress emitted by structure world writes.
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub struct McStructureWriteProgress {
236    /// Current phase.
237    pub phase: McStructureWritePhase,
238    /// Completed units in this phase.
239    pub completed: usize,
240    /// Total units in this phase.
241    pub total: usize,
242}
243
244/// Result of writing a structure into a world.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct McStructureWriteResult {
247    /// Chunks whose terrain records were changed.
248    pub affected_chunks: BTreeSet<ChunkPos>,
249    /// Number of block positions considered during placement.
250    pub placed_blocks: usize,
251}
252
253/// In-memory representation of a Bedrock `.mcstructure` file.
254#[derive(Debug, Clone, PartialEq)]
255pub struct McStructureFile {
256    /// Structure dimensions.
257    pub size: McStructureSize,
258    /// World origin saved in the file.
259    pub world_origin: [i32; 3],
260    /// Shared block palette.
261    pub palette: Vec<McStructurePaletteEntry>,
262    /// Primary block layer indices in structure order.
263    pub primary_indices: Vec<i32>,
264    /// Secondary block layer indices in structure order.
265    pub secondary_indices: Vec<i32>,
266    /// Entity NBT entries.
267    pub entities: Vec<NbtTag>,
268    /// Raw `block_position_data` compound.
269    pub block_position_data: IndexMap<String, NbtTag>,
270}
271
272impl McStructureFile {
273    /// Creates an empty air structure with the requested size.
274    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    /// Reads a structure from uncompressed little-endian NBT bytes.
288    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
289        let root = NbtReader::new(bytes).parse_root()?;
290        Self::from_nbt(root)
291    }
292
293    /// Serializes this structure as uncompressed little-endian NBT bytes.
294    pub fn to_bytes(&self) -> Result<Vec<u8>> {
295        NbtWriter::write_root(&self.to_nbt()?)
296    }
297
298    /// Reads a `.mcstructure` file from disk.
299    pub fn read_from_path(path: &Path) -> Result<Self> {
300        read_mcstructure_file(path)
301    }
302
303    /// Writes this structure to disk as a `.mcstructure` file.
304    pub fn write_to_path(&self, path: &Path) -> Result<()> {
305        write_mcstructure_file(path, self)
306    }
307
308    /// Parses a structure from its root NBT tag.
309    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    /// Converts this structure to a root NBT tag.
385    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    /// Returns the primary-layer palette index at a relative block position.
445    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    /// Iterates every block position in structure order.
453    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    /// Returns the palette entry for a structure-layer index.
473    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    /// Returns whether a structure-layer index resolves to air.
488    pub fn index_is_air(&self, index: i32) -> bool {
489        self.palette_entry_or_air(index).is_air()
490    }
491
492    /// Computes all chunks touched by this structure placement.
493    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    /// Merges this structure's block layers into a world.
542    ///
543    /// Block palette data, secondary block layers, and block-position NBT data
544    /// are written. Entity placement is intentionally not performed because
545    /// Bedrock actor storage requires stable `UniqueID` and digest updates.
546    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    /// Builds a structure by sampling decoded blocks from a world.
769    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
856/// Reads a `.mcstructure` file from disk.
857pub fn read_mcstructure_file(path: &Path) -> Result<McStructureFile> {
858    let bytes = std::fs::read(path)?;
859    McStructureFile::from_bytes(&bytes)
860}
861
862/// Writes a `.mcstructure` file to disk.
863pub 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}