use crate::item::ItemStack;
use crate::utils::{NbtMap, NbtValue};
use quartz_nbt::NbtCompound;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BlockEntity {
pub nbt: Arc<NbtMap>,
pub id: String,
pub position: (i32, i32, i32),
}
impl BlockEntity {
pub fn new(id: String, position: (i32, i32, i32)) -> Self {
BlockEntity {
nbt: Arc::new(NbtMap::new()),
id,
position,
}
}
#[inline]
pub fn nbt_mut(&mut self) -> &mut NbtMap {
Arc::make_mut(&mut self.nbt)
}
#[inline]
pub fn set_nbt(&mut self, nbt: NbtMap) {
self.nbt = Arc::new(nbt);
}
pub fn with_nbt_data(mut self, key: String, value: NbtValue) -> Self {
self.nbt_mut().insert(key, value);
self
}
pub fn to_hashmap(&self) -> HashMap<String, NbtValue> {
let mut map = HashMap::new();
map.insert("Id".to_string(), NbtValue::String(self.id.clone()));
map.insert(
"Pos".to_string(),
NbtValue::IntArray(vec![self.position.0, self.position.1, self.position.2]),
);
for (key, value) in self.nbt.iter() {
map.insert(key.clone(), value.clone());
}
map
}
pub fn add_item_stack(&mut self, item: ItemStack) {
let mut items = self
.nbt
.get("Items")
.map(|items| {
if let NbtValue::List(items) = items {
items.clone()
} else {
vec![]
}
})
.unwrap_or_default();
items.push(item.to_nbt());
self.nbt_mut()
.insert("Items".to_string(), NbtValue::List(items));
}
pub fn create_chest(position: (i32, i32, i32), items: Vec<ItemStack>) -> BlockEntity {
let mut chest = BlockEntity::new("minecraft:chest".to_string(), position);
for item_stack in items {
chest.add_item_stack(item_stack);
}
chest
}
pub fn from_nbt(nbt: &NbtCompound) -> Self {
let nbt_map = NbtMap::from_quartz_nbt(nbt);
let id = nbt_map
.get("Id")
.or_else(|| nbt_map.get("id"))
.and_then(|v| v.as_string())
.cloned()
.unwrap_or_else(|| "unknown".to_string());
let position = nbt_map
.get("Pos")
.and_then(|v| v.as_int_array())
.map(|v| (v[0], v[1], v[2]))
.or_else(|| {
match (
nbt_map.get("x").and_then(|v| v.as_i32()),
nbt_map.get("y").and_then(|v| v.as_i32()),
nbt_map.get("z").and_then(|v| v.as_i32()),
) {
(Some(x), Some(y), Some(z)) => Some((x, y, z)),
_ => None,
}
})
.unwrap_or((0, 0, 0));
BlockEntity {
nbt: Arc::new(nbt_map),
id,
position,
}
}
pub fn to_nbt(&self) -> NbtCompound {
let mut nbt = NbtCompound::new();
nbt.insert("Id", NbtValue::String(self.id.clone()).to_quartz_nbt());
nbt.insert(
"Pos",
NbtValue::IntArray(vec![self.position.0, self.position.1, self.position.2])
.to_quartz_nbt(),
);
for (key, value) in self.nbt.iter() {
nbt.insert(key, value.to_quartz_nbt());
}
nbt
}
pub fn to_nbt_v3(&self, data_version: Option<i32>) -> NbtCompound {
const COMPONENTS_VERSION: i32 = 3837; let inject_components = data_version.is_none_or(|dv| dv >= COMPONENTS_VERSION);
let mut nbt = NbtCompound::new();
nbt.insert("Id", NbtValue::String(self.id.clone()).to_quartz_nbt());
nbt.insert(
"Pos",
NbtValue::IntArray(vec![self.position.0, self.position.1, self.position.2])
.to_quartz_nbt(),
);
let has_nbt_data = self.nbt.iter().next().is_some();
if has_nbt_data {
let mut data_compound = NbtCompound::new();
let is_container = self.id.contains("barrel")
|| self.id.contains("chest")
|| self.id.contains("hopper")
|| self.id.contains("dropper")
|| self.id.contains("dispenser")
|| self.id.contains("jukebox");
if is_container && inject_components {
data_compound.insert(
"components",
NbtValue::Compound(NbtMap::new()).to_quartz_nbt(),
);
data_compound.insert("id", NbtValue::String(self.id.clone()).to_quartz_nbt());
}
for (key, value) in self.nbt.iter() {
data_compound.insert(key, value.to_quartz_nbt());
}
nbt.insert("Data", quartz_nbt::NbtTag::Compound(data_compound));
}
nbt
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_entity_creation() {
let block_entity = BlockEntity::new("minecraft:chest".to_string(), (1, 2, 3));
assert_eq!(block_entity.id, "minecraft:chest");
assert_eq!(block_entity.position, (1, 2, 3));
}
#[test]
fn test_block_entity_with_nbt_data() {
let block_entity = BlockEntity::new("minecraft:chest".to_string(), (1, 2, 3))
.with_nbt_data(
"CustomName".to_string(),
NbtValue::String("Test".to_string()),
);
assert_eq!(
block_entity.nbt.get("CustomName"),
Some(&NbtValue::String("Test".to_string()))
);
}
#[test]
fn from_nbt_reads_litematica_lowercase_id_and_xyz() {
let mut compound = NbtCompound::new();
compound.insert(
"id",
quartz_nbt::NbtTag::String("minecraft:dispenser".to_string()),
);
compound.insert("x", quartz_nbt::NbtTag::Int(3));
compound.insert("y", quartz_nbt::NbtTag::Int(4));
compound.insert("z", quartz_nbt::NbtTag::Int(5));
let be = BlockEntity::from_nbt(&compound);
assert_eq!(be.id, "minecraft:dispenser");
assert_eq!(be.position, (3, 4, 5));
}
#[test]
fn from_nbt_still_reads_capitalized_id_and_pos() {
let mut compound = NbtCompound::new();
compound.insert(
"Id",
quartz_nbt::NbtTag::String("minecraft:chest".to_string()),
);
compound.insert("Pos", quartz_nbt::NbtTag::IntArray(vec![7, 8, 9]));
let be = BlockEntity::from_nbt(&compound);
assert_eq!(be.id, "minecraft:chest");
assert_eq!(be.position, (7, 8, 9));
}
}