From f22d95a3a32b71909997d8af60244a0e1435213c Mon Sep 17 00:00:00 2001 From: Andrew Brower Date: Sat, 23 Nov 2024 02:13:02 -0500 Subject: [PATCH] feat(world): basic block entity parsing from anvil --- src/world/block_entity.rs | 405 ++++++++++++++++++++++++++++++++++++++ src/world/mod.rs | 13 +- 2 files changed, 413 insertions(+), 5 deletions(-) create mode 100644 src/world/block_entity.rs diff --git a/src/world/block_entity.rs b/src/world/block_entity.rs new file mode 100644 index 0000000..8b4b0f4 --- /dev/null +++ b/src/world/block_entity.rs @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2024 Andrew Brower. + * This file is part of Crawlspace. + * + * Crawlspace is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * Crawlspace is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Crawlspace. If not, see + * . + */ + +use std::collections::HashMap; + +use color_eyre::eyre::{bail, Error, Result}; +use fastnbt::Value; + +macro_rules! get_tag { + ($data:expr, $tag_kind:path, $tag_name:literal) => {{ + { + let tag = $data.get($tag_name); + + let Some(tag) = tag else { + bail!("Unable to find tag {}: {:?}", $tag_name, $data); + }; + + match tag { + $tag_kind(v) => v.to_owned(), + e => bail!( + "Improper tag kind found searching for {}: {:?}", + $tag_name, + e + ), + } + } + }}; +} + +macro_rules! get_tag_opt { + ($data:expr, $tag_kind:path, $tag_name:literal) => {{ + let tag = $data.get($tag_name); + + match tag { + Some($tag_kind(v)) => Some(v.to_owned()), + _ => None, + } + }}; +} + +pub struct BlockEntity { + pub id: String, + pub keep_packed: bool, + pub x: i32, + pub y: i32, + pub z: i32, + pub other_tags: Box, +} + +impl BlockEntity { + pub fn try_parse(value: Value) -> Result { + let Value::Compound(data) = value else { + bail!( + "try_parse was called with a value that is not a compound: {:?}", + value + ); + }; + + let id = get_tag!(data, Value::String, "id"); + trace!("Parsing {id}"); + + let other_tags: Box = match id.as_str() { + "minecraft:barrel" => Box::new(BarrelBlockEntityTags::try_parse(&data)?), + "minecraft:brewing_stand" => Box::new(BrewingStandBlockEntityTags::try_parse(&data)?), + "minecraft:campfire" => Box::new(CampfireBlockEntityTags::try_parse(&data)?), + "minecraft:chiseled_bookshelf" => { + Box::new(ChiseledBookshelfBlockEntityTags::try_parse(&data)?) + } + "minecraft:sign" => Box::new(SignBlockEntityTags::try_parse(&data)?), + "minecraft:skull" => Box::new(SkullBlockEntityTags::try_parse(&data)?), + t => unimplemented!("Got block with tag {t}, but it isn't implemented"), + }; + + Ok(Self { + id, + keep_packed: get_tag!(data, Value::String, "keepPacked") == "true", + x: get_tag!(data, Value::Int, "x"), + y: get_tag!(data, Value::Int, "y"), + z: get_tag!(data, Value::Int, "z"), + other_tags, + }) + } +} + +pub struct BlockEntityItem { + pub slot: i8, + pub id: String, + pub count: i32, +} + +pub trait BlockEntityTags { + fn try_parse(from: &HashMap) -> Result + where + Self: Sized; +} + +pub struct SignBlockEntityTags { + pub is_waxed: bool, + pub front_text: SignText, + pub back_text: SignText, +} + +pub struct SignText { + pub has_glowing_text: bool, + pub color: String, + pub messages: Vec, +} + +fn byte_to_bool(byte: i8) -> bool { + matches!(byte, 0) +} + +impl BlockEntityTags for SignBlockEntityTags { + fn try_parse(from: &HashMap) -> Result { + let front_text_tag = get_tag!(from, Value::Compound, "front_text"); + let back_text_tag = get_tag!(from, Value::Compound, "back_text"); + + let front_messages = get_tag!(front_text_tag, Value::List, "messages") + .iter() + .map(|v| match v { + Value::String(s) => s.to_owned(), + _ => "Unknown".to_owned(), + }) + .collect(); + + let back_messages = get_tag!(back_text_tag, Value::List, "messages") + .iter() + .map(|v| match v { + Value::String(s) => s.to_owned(), + _ => "Unknown".to_owned(), + }) + .collect(); + + Ok(Self { + is_waxed: byte_to_bool(get_tag!(from, Value::Byte, "is_waxed")), + front_text: SignText { + has_glowing_text: byte_to_bool(get_tag!( + front_text_tag, + Value::Byte, + "has_glowing_text" + )), + color: get_tag!(front_text_tag, Value::String, "color").to_owned(), + messages: front_messages, + }, + back_text: SignText { + has_glowing_text: byte_to_bool(get_tag!( + back_text_tag, + Value::Byte, + "has_glowing_text" + )), + color: get_tag!(back_text_tag, Value::String, "color").to_owned(), + messages: back_messages, + }, + }) + } +} + +pub struct CampfireBlockEntityTags { + cooking_times: Vec, + cooking_total_times: Vec, + items: Vec, +} + +impl BlockEntityTags for CampfireBlockEntityTags { + fn try_parse(from: &HashMap) -> Result + where + Self: Sized, + { + let cooking_times = get_tag!(from, Value::List, "CookingTimes") + .iter() + .map(|v| match v { + Value::Int(s) => s.to_owned(), + _ => 0, + }) + .collect(); + + let cooking_total_times = get_tag!(from, Value::List, "CookingTotalTimes") + .iter() + .map(|v| match v { + Value::Int(s) => s.to_owned(), + _ => 0, + }) + .collect(); + + let items = { + let item_compounds = get_tag!(from, Value::List, "Items"); + let mut items = Vec::with_capacity(item_compounds.len()); + for c in item_compounds { + match c { + Value::Compound(c) => items.push(BlockEntityItem { + slot: get_tag!(c, Value::Byte, "Slot"), + id: get_tag!(c, Value::String, "id"), + count: get_tag!(c, Value::Int, "count"), + }), + t => bail!("Wrong tag type parsing item: {:?}", t), + } + } + items + }; + + Ok(Self { + cooking_times, + cooking_total_times, + items, + }) + } +} + +pub struct ChiseledBookshelfBlockEntityTags { + items: Vec, + last_interacted_slot: i32, +} + +impl BlockEntityTags for ChiseledBookshelfBlockEntityTags { + fn try_parse(from: &HashMap) -> Result + where + Self: Sized, + { + let items = { + let item_compounds = get_tag!(from, Value::List, "Items"); + let mut items = Vec::with_capacity(item_compounds.len()); + for c in item_compounds { + match c { + Value::Compound(c) => items.push(BlockEntityItem { + slot: get_tag!(c, Value::Byte, "Slot"), + id: get_tag!(c, Value::String, "id"), + count: get_tag!(c, Value::Int, "count"), + }), + t => bail!("Wrong tag type parsing item: {:?}", t), + } + } + items + }; + + Ok(Self { + items, + last_interacted_slot: get_tag!(from, Value::Int, "last_interacted_slot"), + }) + } +} + +pub struct BarrelBlockEntityTags { + custom_name: Option, + items: Vec, +} + +impl BlockEntityTags for BarrelBlockEntityTags { + fn try_parse(from: &HashMap) -> Result + where + Self: Sized, + { + let items = { + let item_compounds = get_tag!(from, Value::List, "Items"); + let mut items = Vec::with_capacity(item_compounds.len()); + for c in item_compounds { + match c { + Value::Compound(c) => items.push(BlockEntityItem { + slot: get_tag!(c, Value::Byte, "Slot"), + id: get_tag!(c, Value::String, "id"), + count: get_tag!(c, Value::Int, "count"), + }), + t => bail!("Wrong tag type parsing item: {:?}", t), + } + } + items + }; + + Ok(Self { + custom_name: get_tag_opt!(from, Value::String, "CustomName"), + items, + }) + } +} + +pub struct BrewingStandBlockEntityTags { + brew_time: i16, + custom_name: Option, + fuel: i8, + items: Vec, + lock: String, +} + +impl BlockEntityTags for BrewingStandBlockEntityTags { + fn try_parse(from: &HashMap) -> Result + where + Self: Sized, + { + let items = { + let item_compounds = get_tag!(from, Value::List, "Items"); + let mut items = Vec::with_capacity(item_compounds.len()); + for c in item_compounds { + match c { + Value::Compound(c) => items.push(BlockEntityItem { + slot: get_tag!(c, Value::Byte, "Slot"), + id: get_tag!(c, Value::String, "id"), + count: get_tag!(c, Value::Int, "count"), + }), + t => bail!("Wrong tag type parsing item: {:?}", t), + } + } + items + }; + + Ok(Self { + brew_time: get_tag!(from, Value::Short, "BrewTime"), + custom_name: get_tag_opt!(from, Value::String, "CustomName"), + fuel: get_tag!(from, Value::Byte, "Fuel"), + items, + lock: get_tag!(from, Value::String, "Lock"), + }) + } +} + +pub struct SkullBlockEntityTags { + pub custom_name: Option, + pub note_block_sound: Option, + pub profile: SkullProfile, +} + +pub enum SkullProfile { + String(String), + Compound { + name: String, + id: Vec, + properties: Option>, + }, +} + +pub struct SkullProperty { + pub name: String, + pub value: String, + pub signature: Option, +} + +impl BlockEntityTags for SkullBlockEntityTags { + fn try_parse(from: &HashMap) -> Result + where + Self: Sized, + { + let profile = match get_tag_opt!(from, Value::String, "profile") { + Some(s) => SkullProfile::String(s), + None => match get_tag_opt!(from, Value::Compound, "profile") { + None => bail!("Profile not present for skull"), + Some(c) => { + let id = get_tag!(from, Value::List, "id") + .iter() + .map(|v| match v { + Value::Int(s) => s.to_owned(), + _ => 0, + }) + .collect(); + + let properties = { + match get_tag_opt!(from, Value::List, "properties") { + None => None, + Some(property_compounds) => { + let mut properties = Vec::with_capacity(property_compounds.len()); + for c in property_compounds { + match c { + Value::Compound(s) => properties.push(SkullProperty { + name: get_tag!(s, Value::String, "name"), + value: get_tag!(s, Value::String, "value"), + signature: get_tag_opt!(s, Value::String, "signature"), + }), + t => bail!("Wrong tag type parsing item: {:?}", t), + } + } + + Some(properties) + } + } + }; + + SkullProfile::Compound { + name: get_tag!(c, Value::String, "name"), + id, + properties, + } + } + }, + }; + + Ok(Self { + custom_name: get_tag_opt!(from, Value::String, "custom_name"), + note_block_sound: get_tag_opt!(from, Value::String, "note_block_sound"), + profile, + }) + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs index 70c3108..3dbb1c0 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -17,17 +17,15 @@ * . */ -use std::{ - collections::HashMap, - fs::File, - path::Path, -}; +use std::{collections::HashMap, fs::File, path::Path}; +use block_entity::BlockEntity; use color_eyre::eyre::Result; use fastanvil::Region; use rayon::prelude::*; use serde::Deserialize; +mod block_entity; pub mod blocks; #[derive(Clone, Debug)] @@ -48,6 +46,7 @@ pub struct Chunk { #[serde(rename = "LastUpdate")] pub _last_update: f64, pub sections: Vec
, + pub block_entities: Vec, } #[derive(Clone, Debug, Deserialize)] @@ -139,6 +138,10 @@ pub fn read_world(path: &str) -> Result { if (-10..10).contains(&parsed.x_pos) && (-10..10).contains(&parsed.z_pos) { parsed.sections.sort_by_key(|c| c.y); + parsed.block_entities.clone().into_iter().for_each(|e| { + let _ = BlockEntity::try_parse(e); + }); + debug!( "Successfully parsed chunk at {}, {}", parsed.x_pos, parsed.z_pos -- 2.51.2