Pumpkin/pumpkin-world/src/generation/structure/template/structure_template.rs
Alexander Medvedev 7350fba3b0 chore: chunk loading/saving move from serde to manual
should be faster now
2026-08-05 12:23:44 +02:00

400 lines
13 KiB
Rust

//! Structure template loading from NBT files.
//!
//! This module handles parsing vanilla Minecraft structure template files (.nbt)
//! into a runtime representation that can be placed in the world.
use std::io::Cursor;
use pumpkin_nbt::{compound::NbtCompound, nbt_compress::read_gzip_compound_tag, tag::NbtTag};
use pumpkin_util::math::vector3::Vector3;
use thiserror::Error;
/// Errors that can occur when loading a structure template.
#[derive(Debug, Error)]
pub enum TemplateError {
#[error("Failed to decompress NBT: {0}")]
NbtError(#[from] pumpkin_nbt::Error),
#[error("Missing required field: {0}")]
MissingField(&'static str),
#[error("Invalid field type for {0}")]
InvalidFieldType(&'static str),
#[error("Invalid palette index: {0}")]
InvalidPaletteIndex(u32),
}
/// A loaded structure template from an NBT file.
///
/// Structure templates contain a palette of block states and a list of blocks
/// referencing that palette. This allows for efficient storage and the ability
/// to transform (rotate/mirror) the template when placing.
#[derive(Debug, Clone)]
pub struct StructureTemplate {
/// The dimensions of the template (width, height, depth).
pub size: Vector3<i32>,
/// The palette of block states used in this template.
pub palette: Vec<PaletteEntry>,
/// The blocks in this template, referencing palette indices.
pub blocks: Vec<TemplateBlock>,
/// Entities to spawn when placing this template (future use).
pub entities: Vec<TemplateEntity>,
}
/// A single entry in the template's block palette.
///
/// Each palette entry defines a block type with optional properties.
#[derive(Debug, Clone)]
pub struct PaletteEntry {
/// The block name (e.g., "`minecraft:stone_bricks`").
pub name: String,
/// Block state properties (e.g., [("facing", "north"), ("lit", "false")]).
pub properties: Vec<(String, String)>,
}
impl PaletteEntry {
/// Creates a new palette entry with no properties.
#[must_use]
pub const fn new(name: String) -> Self {
Self {
name,
properties: Vec::new(),
}
}
/// Creates a new palette entry with the given properties.
#[must_use]
pub const fn with_properties(name: String, properties: Vec<(String, String)>) -> Self {
Self { name, properties }
}
/// Parses a block state string (e.g. "`minecraft:oak_log`[axis=x]") into a palette entry.
#[must_use]
pub fn from_string(s: &str) -> Self {
s.find('[').map_or_else(
|| Self::new(s.to_string()),
|bracket_pos| {
let name = s[..bracket_pos].to_string();
let end_bracket = s.rfind(']').unwrap_or(s.len());
let props_str = &s[bracket_pos + 1..end_bracket];
let properties = props_str
.split(',')
.filter_map(|p| {
let mut parts = p.split('=');
let key = parts.next()?.trim().to_string();
let value = parts.next()?.trim().to_string();
Some((key, value))
})
.collect();
Self { name, properties }
},
)
}
}
/// A single block placement in the template.
#[derive(Debug, Clone)]
pub struct TemplateBlock {
/// Position relative to the template origin.
pub pos: Vector3<i32>,
/// Index into the template's palette.
pub state: u32,
/// Optional NBT data for block entities (chests, signs, etc.).
pub nbt: Option<NbtCompound>,
}
/// An entity to spawn when placing the template.
#[derive(Debug, Clone)]
pub struct TemplateEntity {
/// Position relative to the template origin.
pub pos: Vector3<f64>,
/// Block position (snapped to grid).
pub block_pos: Vector3<i32>,
/// Entity NBT data.
pub nbt: NbtCompound,
}
impl StructureTemplate {
/// Loads a structure template from gzipped NBT bytes.
///
/// # Errors
///
/// Returns an error if the NBT data is invalid or missing required fields.
pub fn from_nbt_bytes(bytes: &[u8]) -> Result<Self, TemplateError> {
let compound = read_gzip_compound_tag(Cursor::new(bytes))?;
Self::from_nbt_compound(&compound)
}
/// Loads a structure template from a parsed NBT compound.
///
/// # Errors
///
/// Returns an error if required fields are missing or have invalid types.
pub fn from_nbt_compound(compound: &NbtCompound) -> Result<Self, TemplateError> {
// Parse size
let size = Self::parse_size(compound)?;
// Parse palette
let palette = Self::parse_palette(compound)?;
// Parse blocks
let blocks = Self::parse_blocks(compound, palette.len())?;
// Parse entities (optional)
let entities = Self::parse_entities(compound).unwrap_or_default();
Ok(Self {
size,
palette,
blocks,
entities,
})
}
fn parse_size(compound: &NbtCompound) -> Result<Vector3<i32>, TemplateError> {
let size_list = compound
.get_list("size")
.ok_or(TemplateError::MissingField("size"))?;
if size_list.len() != 3 {
return Err(TemplateError::InvalidFieldType("size"));
}
let x = Self::extract_int(&size_list[0]).ok_or(TemplateError::InvalidFieldType("size"))?;
let y = Self::extract_int(&size_list[1]).ok_or(TemplateError::InvalidFieldType("size"))?;
let z = Self::extract_int(&size_list[2]).ok_or(TemplateError::InvalidFieldType("size"))?;
Ok(Vector3::new(x, y, z))
}
fn parse_palette(compound: &NbtCompound) -> Result<Vec<PaletteEntry>, TemplateError> {
let palette_list = if let Some(list) = compound.get_list("palette") {
list
} else if let Some(palettes) = compound.get_list("palettes") {
if let Some(NbtTag::List(list)) = palettes.first() {
list
} else {
return Err(TemplateError::MissingField("palette"));
}
} else {
return Err(TemplateError::MissingField("palette"));
};
let mut palette = Vec::with_capacity(palette_list.len());
for tag in palette_list {
let NbtTag::Compound(entry_compound) = tag else {
return Err(TemplateError::InvalidFieldType("palette entry"));
};
let name = entry_compound
.get_string("Name")
.ok_or(TemplateError::MissingField("palette.Name"))?
.to_string();
let properties: Vec<(String, String)> = entry_compound
.get_compound("Properties")
.map_or_else(Vec::new, |props_compound| {
props_compound
.child_tags
.iter()
.filter_map(|(key, value)| {
if let NbtTag::String(v) = value {
Some((key.to_string(), v.to_string()))
} else {
None
}
})
.collect()
});
palette.push(PaletteEntry { name, properties });
}
Ok(palette)
}
fn parse_blocks(
compound: &NbtCompound,
palette_size: usize,
) -> Result<Vec<TemplateBlock>, TemplateError> {
let blocks_list = compound
.get_list("blocks")
.ok_or(TemplateError::MissingField("blocks"))?;
let mut blocks = Vec::with_capacity(blocks_list.len());
for tag in blocks_list {
let NbtTag::Compound(block_compound) = tag else {
return Err(TemplateError::InvalidFieldType("blocks entry"));
};
// Parse position
let pos_list = block_compound
.get_list("pos")
.ok_or(TemplateError::MissingField("blocks.pos"))?;
if pos_list.len() != 3 {
return Err(TemplateError::InvalidFieldType("blocks.pos"));
}
let x =
Self::extract_int(&pos_list[0]).ok_or(TemplateError::InvalidFieldType("pos"))?;
let y =
Self::extract_int(&pos_list[1]).ok_or(TemplateError::InvalidFieldType("pos"))?;
let z =
Self::extract_int(&pos_list[2]).ok_or(TemplateError::InvalidFieldType("pos"))?;
// Parse state (palette index)
let state = block_compound
.get_int("state")
.ok_or(TemplateError::MissingField("blocks.state"))? as u32;
if state as usize >= palette_size {
return Err(TemplateError::InvalidPaletteIndex(state));
}
// Parse optional NBT
let nbt = block_compound.get_compound("nbt").cloned();
blocks.push(TemplateBlock {
pos: Vector3::new(x, y, z),
state,
nbt,
});
}
Ok(blocks)
}
fn parse_entities(compound: &NbtCompound) -> Result<Vec<TemplateEntity>, TemplateError> {
let Some(entities_list) = compound.get_list("entities") else {
return Ok(Vec::new());
};
let mut entities = Vec::with_capacity(entities_list.len());
for tag in entities_list {
let NbtTag::Compound(entity_compound) = tag else {
return Err(TemplateError::InvalidFieldType("entities entry"));
};
// Parse position (double coordinates)
let pos_list = entity_compound
.get_list("pos")
.ok_or(TemplateError::MissingField("entities.pos"))?;
if pos_list.len() != 3 {
return Err(TemplateError::InvalidFieldType("entities.pos"));
}
let x = Self::extract_double(&pos_list[0])
.ok_or(TemplateError::InvalidFieldType("entities.pos"))?;
let y = Self::extract_double(&pos_list[1])
.ok_or(TemplateError::InvalidFieldType("entities.pos"))?;
let z = Self::extract_double(&pos_list[2])
.ok_or(TemplateError::InvalidFieldType("entities.pos"))?;
// Parse block position
let block_pos_list = entity_compound
.get_list("blockPos")
.ok_or(TemplateError::MissingField("entities.blockPos"))?;
if block_pos_list.len() != 3 {
return Err(TemplateError::InvalidFieldType("entities.blockPos"));
}
let bx = Self::extract_int(&block_pos_list[0])
.ok_or(TemplateError::InvalidFieldType("entities.blockPos"))?;
let by = Self::extract_int(&block_pos_list[1])
.ok_or(TemplateError::InvalidFieldType("entities.blockPos"))?;
let bz = Self::extract_int(&block_pos_list[2])
.ok_or(TemplateError::InvalidFieldType("entities.blockPos"))?;
// Parse NBT
let nbt = entity_compound
.get_compound("nbt")
.cloned()
.ok_or(TemplateError::MissingField("entities.nbt"))?;
entities.push(TemplateEntity {
pos: Vector3::new(x, y, z),
block_pos: Vector3::new(bx, by, bz),
nbt,
});
}
Ok(entities)
}
/// Helper to extract an i32 from various NBT numeric types.
fn extract_int(tag: &NbtTag) -> Option<i32> {
match tag {
NbtTag::Byte(v) => Some(i32::from(*v)),
NbtTag::Short(v) => Some(i32::from(*v)),
NbtTag::Int(v) => Some(*v),
NbtTag::Long(v) => Some(*v as i32),
_ => None,
}
}
/// Helper to extract an f64 from various NBT numeric types.
fn extract_double(tag: &NbtTag) -> Option<f64> {
match tag {
NbtTag::Float(v) => Some(f64::from(*v)),
NbtTag::Double(v) => Some(*v),
NbtTag::Int(v) => Some(f64::from(*v)),
NbtTag::Long(v) => Some(*v as f64),
_ => None,
}
}
/// Returns the total number of blocks in this template.
#[must_use]
pub const fn block_count(&self) -> usize {
self.blocks.len()
}
/// Returns whether this template has any block entity data.
#[must_use]
pub fn has_block_entities(&self) -> bool {
self.blocks.iter().any(|b| b.nbt.is_some())
}
/// Returns whether this template has any entities.
#[must_use]
pub const fn has_entities(&self) -> bool {
!self.entities.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn palette_entry_creation() {
let entry = PaletteEntry::new("minecraft:stone".to_string());
assert_eq!(entry.name, "minecraft:stone");
assert!(entry.properties.is_empty());
let entry_with_props = PaletteEntry::with_properties(
"minecraft:oak_stairs".to_string(),
vec![
("facing".to_string(), "north".to_string()),
("half".to_string(), "bottom".to_string()),
],
);
assert_eq!(entry_with_props.properties.len(), 2);
}
}