chore: chunk loading/saving move from serde to manual

should be faster now
This commit is contained in:
Alexander Medvedev 2026-08-05 12:23:44 +02:00
parent 0d348892e5
commit 7350fba3b0
51 changed files with 9073 additions and 11149 deletions

9
Cargo.lock generated
View File

@ -192,9 +192,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.0"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "base64ct"
@ -2615,7 +2615,7 @@ name = "pumpkin"
version = "0.1.0-dev+26.2"
dependencies = [
"arc-swap",
"base64 0.23.0",
"base64 0.23.1",
"bitflags",
"bytes",
"colored",
@ -2749,7 +2749,6 @@ dependencies = [
"criterion",
"flate2",
"pumpkin-codecs",
"serde",
"tempfile",
"thiserror 2.0.19",
"tracing",
@ -2795,7 +2794,7 @@ dependencies = [
name = "pumpkin-util"
version = "0.1.0-dev+26.2"
dependencies = [
"base64 0.23.0",
"base64 0.23.1",
"bytes",
"colored",
"crypto-bigint",

View File

@ -119,7 +119,7 @@ tempfile = { version = "3.27.0", default-features = false }
proc-macro-error2 = { version = "2", default-features = false }
aes = { version = "0.9", default-features = false }
async-compression = { version = "0.4.43", default-features = false }
base64 = { version = "0.23.0", default-features = false, features = ["std"] }
base64 = { version = "0.23.1", default-features = false, features = ["std"] }
bitflags = { version = "2.13.1", default-features = false, features = ["std"] }
cesu8 = { version = "1.1", default-features = false }
cfb8 = { version = "0.9", default-features = false }

View File

@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anyhow"
version = "1.0.103"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
@ -34,9 +34,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.0"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "base64ct"
@ -584,7 +584,7 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.3",
]
[[package]]
@ -749,9 +749,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.106"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
@ -781,7 +781,7 @@ dependencies = [
"serde_json",
"serde_json5",
"serde_repr",
"syn 3.0.2",
"syn 3.0.3",
"wit-encoder",
]
@ -793,7 +793,6 @@ dependencies = [
"cesu8",
"flate2",
"pumpkin-codecs",
"serde",
"thiserror",
"tracing",
"uuid",
@ -803,7 +802,7 @@ dependencies = [
name = "pumpkin-util"
version = "0.1.0-dev+26.2"
dependencies = [
"base64 0.23.0",
"base64 0.23.1",
"bytes",
"colored",
"crypto-bigint",
@ -820,7 +819,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.11.0",
"syn 3.0.2",
"syn 3.0.3",
"thiserror",
"tokio",
"ureq",
@ -829,9 +828,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@ -1002,7 +1001,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.3",
]
[[package]]
@ -1124,9 +1123,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.2"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
@ -1381,9 +1380,9 @@ dependencies = [
[[package]]
name = "wasmparser"
version = "0.254.0"
version = "0.255.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27"
checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b"
dependencies = [
"bitflags",
"indexmap",
@ -1559,15 +1558,15 @@ dependencies = [
[[package]]
name = "wit-encoder"
version = "0.254.0"
version = "0.255.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c565f1f3dcf3b659d6c15052739e35937227590d7dded3d3ce274f7bd9c34d93"
checksum = "7cebd2d14c3296dada506c67c1ee5c9440811a2fe88a83367a606ccbd30ebfde"
dependencies = [
"id-arena",
"pretty_assertions",
"semver",
"serde",
"wit-parser 0.254.0",
"wit-parser 0.255.0",
]
[[package]]
@ -1590,9 +1589,9 @@ dependencies = [
[[package]]
name = "wit-parser"
version = "0.254.0"
version = "0.255.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4"
checksum = "ab5f6371fc71f15730b756c1dea3562a67adab1a7e519c4ca010173d883695bb"
dependencies = [
"anyhow",
"hashbrown 0.17.1",
@ -1604,7 +1603,7 @@ dependencies = [
"serde_derive",
"serde_json",
"unicode-ident",
"wasmparser 0.254.0",
"wasmparser 0.255.0",
]
[[package]]
@ -1632,6 +1631,6 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.21"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View File

@ -16,7 +16,7 @@ serde_repr = { version = "0.1" }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_json5 = "0.2"
wit-encoder = "0.254.0"
wit-encoder = "0.255.0"
semver = "1.0"
rayon = "1.12.0"

View File

@ -45,67 +45,60 @@ pub fn build() -> TokenStream {
let nbt_bytes = fs::read(nbt_path).expect("Failed to read bedrock/creative_items.nbt");
let mut cursor = Cursor::new(nbt_bytes);
#[derive(serde::Deserialize)]
struct CreativeGroupEntry {
category: i32,
name: String,
icon: Option<CreativeItemEntry>,
}
#[derive(serde::Deserialize)]
struct CreativeItemEntry {
name: String,
meta: Option<i16>,
group_index: Option<i32>,
}
#[derive(serde::Deserialize)]
struct CreativeData {
groups: Vec<CreativeGroupEntry>,
items: Vec<CreativeItemEntry>,
}
let data: CreativeData = pumpkin_nbt::deserializer::from_bytes_bedrock(&mut cursor)
.expect("Failed to deserialize creative_items.nbt");
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperBedrock::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = pumpkin_nbt::Nbt::read(&mut reader).expect("Failed to read creative_items.nbt");
let mut group_tokens = Vec::new();
for g in &data.groups {
let category = g.category;
let name = &g.name;
let mut icon_item_id = 0i16;
let mut icon_item_aux_value = 0u32;
if let Some(pumpkin_nbt::tag::NbtTag::List(groups_list)) = nbt.get("groups") {
for g_tag in groups_list {
if let pumpkin_nbt::tag::NbtTag::Compound(g) = g_tag {
let category = g.get_int("category").unwrap_or(0);
let name = g.get_string("name").unwrap_or_default().to_string();
let mut icon_item_id = 0i16;
let mut icon_item_aux_value = 0u32;
if let Some(ref icon) = g.icon
&& let Some(&id) = bedrock_items_map.get(icon.name.as_str())
{
icon_item_id = id;
icon_item_aux_value = icon.meta.unwrap_or(0) as u32;
}
if let Some(pumpkin_nbt::tag::NbtTag::Compound(icon)) = g.get("icon") {
if let Some(icon_name) = icon.get_string("name") {
if let Some(&id) = bedrock_items_map.get(icon_name) {
icon_item_id = id;
icon_item_aux_value = icon.get_short("meta").unwrap_or(0) as u32;
}
}
}
group_tokens.push(quote! {
CreativeGroup {
category: #category,
name: #name,
icon_item_id: #icon_item_id,
icon_item_aux_value: #icon_item_aux_value,
group_tokens.push(quote! {
CreativeGroup {
category: #category,
name: #name,
icon_item_id: #icon_item_id,
icon_item_aux_value: #icon_item_aux_value,
}
});
}
});
}
}
// 4. Build entry definitions TokenStream
let mut entry_tokens = Vec::new();
for item in &data.items {
if let Some(&id) = bedrock_items_map.get(item.name.as_str()) {
let item_aux_value = item.meta.unwrap_or(0) as u32;
let group_index = item.group_index.unwrap_or(0) as u32;
if let Some(pumpkin_nbt::tag::NbtTag::List(items_list)) = nbt.get("items") {
for item_tag in items_list {
if let pumpkin_nbt::tag::NbtTag::Compound(item) = item_tag {
if let Some(item_name) = item.get_string("name") {
if let Some(&id) = bedrock_items_map.get(item_name) {
let item_aux_value = item.get_short("meta").unwrap_or(0) as u32;
let group_index = item.get_int("group_index").unwrap_or(0) as u32;
entry_tokens.push(quote! {
CreativeEntry {
item_id: #id,
item_aux_value: #item_aux_value,
group_index: #group_index,
entry_tokens.push(quote! {
CreativeEntry {
item_id: #id,
item_aux_value: #item_aux_value,
group_index: #group_index,
}
});
}
}
});
}
}
}
@ -113,7 +106,7 @@ pub fn build() -> TokenStream {
let entries_len = entry_tokens.len();
quote! {
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy)]
pub struct CreativeGroup {
pub category: i32,
pub name: &'static str,
@ -121,7 +114,7 @@ pub fn build() -> TokenStream {
pub icon_item_aux_value: u32,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy)]
pub struct CreativeEntry {
pub item_id: i16,
pub item_aux_value: u32,

View File

@ -366,7 +366,7 @@ impl ToTokens for BlockPropertyStruct {
};
tokens.extend(quote! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct #name {
#(#fields),*
}

View File

@ -115,7 +115,7 @@ pub fn build() -> TokenStream {
quote! {
use crate::tag::{RegistryKey, Tag, Taggable};
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub struct DamageType {
pub death_message_type: DeathMessageType,
pub exhaustion: f32,
@ -125,14 +125,14 @@ pub fn build() -> TokenStream {
pub id: u8,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum DeathMessageType {
Default,
FallVariants,
IntentionalGameDesign,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum DamageEffects {
Hurt,
Thorns,
@ -142,7 +142,7 @@ pub fn build() -> TokenStream {
Freezing,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum DamageScaling {
Never,
WhenCausedByLivingNonPlayer,

View File

@ -48,7 +48,7 @@ pub fn build() -> TokenStream {
quote! {
use crate::data_component_impl::*;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum DataComponent {
#enum_variants

View File

@ -109,7 +109,7 @@ impl ToTokens for PropertyStruct {
});
tokens.extend(quote! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum #name {
#(#ident_values),*
}
@ -208,7 +208,7 @@ impl ToTokens for FluidPropertyStruct {
});
tokens.extend(quote! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct #name {
#(pub #values),*
}
@ -242,7 +242,7 @@ impl ToTokens for FluidPropertyStruct {
fn to_state_id(&self, fluid: &Fluid) -> BlockStateId {
if ![#(#fluid_names),*].contains(&fluid.name) {
panic!("{} is not a valid fluid for {}", &fluid.name, #struct_name);
panic!("{} is not a valid fluid for {}", fluid.name, #struct_name);
}
let prop_index = self.to_index();

View File

@ -1077,12 +1077,6 @@ pub enum Operation {
AddMultipliedTotal,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct BedrockItemComponents {
components: Option<NbtCompound>,
}
#[derive(Deserialize_repr, Debug)]
#[repr(i32)]
enum BedrockItemVersion {
@ -1156,12 +1150,21 @@ pub fn build() -> TokenStream {
.into_keys()
.collect::<HashSet<_>>();
let be_item_components: BTreeMap<String, BedrockItemComponents> = {
let be_item_components: BTreeMap<String, Option<NbtCompound>> = {
let data = fs::read("../assets/bedrock/item_components.nbt").unwrap();
let mut cursor = Cursor::new(data);
pumpkin_nbt::nbt_compress::from_gzip_bytes(&mut cursor)
.expect("Failed to parse bedrock/item_components.nbt")
let nbt = pumpkin_nbt::nbt_compress::read_gzip_compound_tag(&mut cursor)
.expect("Failed to parse bedrock/item_components.nbt");
nbt.child_tags
.into_iter()
.map(|(k, v)| {
let comp = match v {
pumpkin_nbt::tag::NbtTag::Compound(c) => Some(c),
_ => None,
};
(k.into_string(), comp)
})
.collect()
};
let be_runtime_item_states: Vec<BedrockRuntimeItemState> = serde_json::from_str(
@ -1349,7 +1352,7 @@ pub fn build() -> TokenStream {
let mut bedrock_item_definitions = Vec::new();
for item in be_runtime_item_states {
let components = &be_item_components.get(&item.name).unwrap().components;
let components = be_item_components.get(&item.name).cloned().flatten();
bedrock_item_definitions.push(BedrockItem {
id: item.id,

View File

@ -98,7 +98,7 @@ pub fn build() -> TokenStream {
.collect::<TokenStream>();
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum JukeboxSong {
#variants

View File

@ -69,8 +69,47 @@ pub(crate) fn build() -> TokenStream {
let entry_tokens: Vec<TokenStream> = entries
.iter()
.map(|(entry_name, entry_data)| {
let mut bytes = Vec::new();
pumpkin_nbt::serializer::to_bytes_unnamed(entry_data, &mut bytes).unwrap();
fn json_to_nbt_tag(v: &Value) -> pumpkin_nbt::tag::NbtTag {
match v {
Value::Null => pumpkin_nbt::tag::NbtTag::End,
Value::Bool(b) => {
pumpkin_nbt::tag::NbtTag::Byte(if *b { 1 } else { 0 })
}
Value::Number(num) => {
if let Some(i) = num.as_i64() {
if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
pumpkin_nbt::tag::NbtTag::Int(i as i32)
} else {
pumpkin_nbt::tag::NbtTag::Long(i)
}
} else if let Some(f) = num.as_f64() {
pumpkin_nbt::tag::NbtTag::Double(f)
} else {
pumpkin_nbt::tag::NbtTag::Int(0)
}
}
Value::String(s) => {
pumpkin_nbt::tag::NbtTag::String(s.clone().into())
}
Value::Array(arr) => pumpkin_nbt::tag::NbtTag::List(
arr.iter().map(json_to_nbt_tag).collect(),
),
Value::Object(obj) => {
let mut compound = pumpkin_nbt::compound::NbtCompound::new();
for (k, val) in obj {
compound.put(k, json_to_nbt_tag(val));
}
pumpkin_nbt::tag::NbtTag::Compound(compound)
}
}
}
let nbt_tag = json_to_nbt_tag(entry_data);
let bytes = if let pumpkin_nbt::tag::NbtTag::Compound(compound) = nbt_tag {
pumpkin_nbt::Nbt::from(compound).write_unnamed()
} else {
Vec::new().into()
};
let byte_literal = Literal::byte_string(&bytes);
quote! {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
/* This file is generated. Do not edit manually. */
#[doc = r" The maximum supported view distance."]
#[doc = r" The maximum supported view distance"]
pub const MAX_VIEW_DISTANCE: u8 = 32u8;
#[doc = r" Static precomputed lookup table for relative chunk offsets by view distance (0..=32)."]
pub static CHUNK_VIEW_LUT: [&[(i8, i8)]; 33usize] = [

View File

@ -1,6 +1,6 @@
/* This file is generated. Do not edit manually. */
use crate::tag::{RegistryKey, Tag, Taggable};
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub struct DamageType {
pub death_message_type: DeathMessageType,
pub exhaustion: f32,
@ -9,13 +9,13 @@ pub struct DamageType {
pub scaling: DamageScaling,
pub id: u8,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum DeathMessageType {
Default,
FallVariants,
IntentionalGameDesign,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum DamageEffects {
Hurt,
Thorns,
@ -24,7 +24,7 @@ pub enum DamageEffects {
Poking,
Freezing,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum DamageScaling {
Never,
WhenCausedByLivingNonPlayer,

View File

@ -1,6 +1,6 @@
/* This file is generated. Do not edit manually. */
use crate::data_component_impl::*;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum DataComponent {
CustomData = 0u8,

View File

@ -1,5 +1,5 @@
/* This file is generated. Do not edit manually. */
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum JukeboxSong {
Id11,

View File

@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
pumpkin-codecs.workspace = true
serde.workspace = true
thiserror.workspace = true
bytes.workspace = true
uuid.workspace = true
@ -19,7 +18,6 @@ tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
criterion.workspace = true
serde = { version = "1.0", features = ["derive"] }
[[bench]]
name = "nbt"

View File

@ -1,41 +1,7 @@
use criterion::{Criterion, criterion_group, criterion_main};
use pumpkin_nbt::{Nbt, NbtCompound, deserializer, serializer, tag::NbtTag};
use serde::{Deserialize, Serialize};
use pumpkin_nbt::{Nbt, NbtCompound, deserializer, tag::NbtTag};
use std::io::Cursor;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct LargeTest {
byte: i8,
short: i16,
int: i32,
long: i64,
float: f32,
double: f64,
string: String,
byte_array: Vec<u8>,
int_array: Vec<i32>,
long_array: Vec<i64>,
list_string: Vec<String>,
nested: Option<Box<Self>>,
}
fn create_large_test_data(depth: usize) -> LargeTest {
LargeTest {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
double: 69.42,
string: "Hello test benchmark data".to_string(),
byte_array: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
int_array: vec![10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
long_array: vec![20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
list_string: vec!["one".to_string(), "two".to_string(), "three".to_string()],
nested: (depth > 0).then(|| Box::new(create_large_test_data(depth - 1))),
}
}
fn create_large_compound(depth: usize) -> NbtCompound {
let mut compound = NbtCompound::new();
compound.put_byte("byte", 123);
@ -72,31 +38,11 @@ fn create_large_compound(depth: usize) -> NbtCompound {
}
pub fn bench_nbt(c: &mut Criterion) {
let test_data = create_large_test_data(5);
let mut ser_bytes_java = Vec::new();
let mut ser_bytes_bedrock = Vec::new();
serializer::to_bytes(&test_data, &mut ser_bytes_java).unwrap();
serializer::to_bytes_bedrock(&test_data, &mut ser_bytes_bedrock).unwrap();
let compound_data = create_large_compound(5);
let nbt_wrapper = Nbt::new(String::new(), compound_data.clone());
let wrapper_bytes_java = nbt_wrapper.clone().write();
let wrapper_bytes_bedrock = nbt_wrapper.write_bedrock();
c.bench_function("nbt/java/serialize/serde", |b| {
b.iter(|| {
let mut out = Vec::new();
serializer::to_bytes(&test_data, &mut out).unwrap();
});
});
c.bench_function("nbt/java/deserialize/serde", |b| {
b.iter(|| {
let cursor = Cursor::new(&ser_bytes_java);
let _: LargeTest = deserializer::from_bytes(cursor).unwrap();
});
});
c.bench_function("nbt/java/serialize/raw", |b| {
b.iter(|| {
let nbt = Nbt::new(String::new(), compound_data.clone());
@ -112,20 +58,6 @@ pub fn bench_nbt(c: &mut Criterion) {
});
});
c.bench_function("nbt/bedrock/serialize/serde", |b| {
b.iter(|| {
let mut out = Vec::new();
serializer::to_bytes_bedrock(&test_data, &mut out).unwrap();
});
});
c.bench_function("nbt/bedrock/deserialize/serde", |b| {
b.iter(|| {
let cursor = Cursor::new(&ser_bytes_bedrock);
let _: LargeTest = deserializer::from_bytes_bedrock(cursor).unwrap();
});
});
c.bench_function("nbt/bedrock/serialize/raw", |b| {
b.iter(|| {
let nbt = Nbt::new(String::new(), compound_data.clone());

View File

@ -11,7 +11,6 @@ cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
serde = { version = "1.0", features = ["derive"] }
arbitrary = { version = "1.3", features = ["derive"] }
[dependencies.pumpkin-nbt]
@ -31,37 +30,9 @@ test = false
doc = false
bench = false
[[bin]]
name = "deserialize_serde_java"
path = "fuzz_targets/deserialize_serde_java.rs"
test = false
doc = false
bench = false
[[bin]]
name = "deserialize_serde_bedrock"
path = "fuzz_targets/deserialize_serde_bedrock.rs"
test = false
doc = false
bench = false
[[bin]]
name = "decompress_gzip"
path = "fuzz_targets/decompress_gzip.rs"
test = false
doc = false
bench = false
[[bin]]
name = "roundtrip_java"
path = "fuzz_targets/roundtrip_java.rs"
test = false
doc = false
bench = false
[[bin]]
name = "roundtrip_bedrock"
path = "fuzz_targets/roundtrip_bedrock.rs"
test = false
doc = false
bench = false

View File

@ -1,26 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_nbt::deserializer::from_bytes_bedrock;
use serde::Deserialize;
use std::io::Cursor;
#[derive(Deserialize, Debug, PartialEq)]
struct FuzzStruct {
byte: i8,
short: i16,
int: i32,
long: i64,
float: f32,
double: f64,
string: String,
byte_array: Vec<u8>,
int_array: Vec<i32>,
long_array: Vec<i64>,
list_string: Vec<String>,
nested: Option<Box<FuzzStruct>>,
}
fuzz_target!(|data: &[u8]| {
let cursor = Cursor::new(data);
let _: Result<FuzzStruct, _> = from_bytes_bedrock(cursor);
});

View File

@ -1,29 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_nbt::deserializer::{from_bytes, from_bytes_unnamed};
use serde::Deserialize;
use std::io::Cursor;
#[derive(Deserialize, Debug, PartialEq)]
struct FuzzStruct {
byte: i8,
short: i16,
int: i32,
long: i64,
float: f32,
double: f64,
string: String,
byte_array: Vec<u8>,
int_array: Vec<i32>,
long_array: Vec<i64>,
list_string: Vec<String>,
nested: Option<Box<FuzzStruct>>,
}
fuzz_target!(|data: &[u8]| {
let cursor = Cursor::new(data);
let _: Result<FuzzStruct, _> = from_bytes(cursor);
let cursor_unnamed = Cursor::new(data);
let _: Result<FuzzStruct, _> = from_bytes_unnamed(cursor_unnamed);
});

View File

@ -1,52 +0,0 @@
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use pumpkin_nbt::deserializer::from_bytes_bedrock;
use pumpkin_nbt::serializer::to_bytes_bedrock;
use serde::{Deserialize, Serialize};
use std::io::Cursor;
#[derive(Arbitrary, Serialize, Deserialize, Debug, PartialEq)]
struct RoundtripStruct {
byte: i8,
short: i16,
int: i32,
long: i64,
float: f32,
double: f64,
string: String,
byte_array: Vec<u8>,
int_array: Vec<i32>,
long_array: Vec<i64>,
list_string: Vec<String>,
// Nested struct to test recursion
nested: Option<Box<RoundtripStruct>>,
}
fuzz_target!(|data: RoundtripStruct| {
let mut fixed_data = data;
let mut struct_to_fixup = &mut fixed_data;
loop {
// prevent NaN != NaN issues
if struct_to_fixup.float != struct_to_fixup.float {
struct_to_fixup.float = 0.0;
}
if struct_to_fixup.double != struct_to_fixup.double {
struct_to_fixup.double = 0.0;
}
if struct_to_fixup.nested.is_none() {
break;
}
struct_to_fixup = struct_to_fixup.nested.as_mut().unwrap();
}
let mut bytes = Vec::new();
if to_bytes_bedrock(&fixed_data, &mut bytes).is_ok() {
let cursor = Cursor::new(bytes);
let reconstructed: RoundtripStruct =
from_bytes_bedrock(cursor).expect("Failed to deserialize valid NBT");
assert_eq!(fixed_data, reconstructed);
}
});

View File

@ -1,52 +0,0 @@
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use pumpkin_nbt::deserializer::from_bytes;
use pumpkin_nbt::serializer::to_bytes;
use serde::{Deserialize, Serialize};
use std::io::Cursor;
#[derive(Arbitrary, Serialize, Deserialize, Debug, PartialEq)]
struct RoundtripStruct {
byte: i8,
short: i16,
int: i32,
long: i64,
float: f32,
double: f64,
string: String,
byte_array: Vec<u8>,
int_array: Vec<i32>,
long_array: Vec<i64>,
list_string: Vec<String>,
// Nested struct to test recursion
nested: Option<Box<RoundtripStruct>>,
}
fuzz_target!(|data: RoundtripStruct| {
let mut fixed_data = data;
let mut struct_to_fixup = &mut fixed_data;
loop {
// prevent NaN != NaN issues
if struct_to_fixup.float != struct_to_fixup.float {
struct_to_fixup.float = 0.0;
}
if struct_to_fixup.double != struct_to_fixup.double {
struct_to_fixup.double = 0.0;
}
if struct_to_fixup.nested.is_none() {
break;
}
struct_to_fixup = struct_to_fixup.nested.as_mut().unwrap();
}
let mut bytes = Vec::new();
if to_bytes(&fixed_data, &mut bytes).is_ok() {
let cursor = Cursor::new(bytes);
let reconstructed: RoundtripStruct =
from_bytes(cursor).expect("Failed to deserialize valid NBT");
assert_eq!(fixed_data, reconstructed);
}
});

View File

@ -1,6 +1,5 @@
//! Storage and convenience methods for NBT compound tags.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use uuid::Uuid;
@ -113,13 +112,9 @@ impl NbtCompound {
self.child_tags.is_empty()
}
/// Inserts a tag when `name` is not already present.
///
/// Existing entries are left unchanged.
/// Inserts or replaces a tag for `name`.
pub fn put(&mut self, name: &str, value: impl Into<NbtTag>) {
if !self.child_tags.contains_key(name) {
self.child_tags.insert(name.into(), value.into());
}
self.child_tags.insert(name.into(), value.into());
}
/// Inserts a string tag when `name` is not already present.
@ -344,44 +339,6 @@ impl AsRef<Self> for NbtCompound {
}
}
impl Serialize for NbtCompound {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.child_tags.len()))?;
for (key, value) in &self.child_tags {
map.serialize_entry(key, &value)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for NbtCompound {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct CompoundVisitor;
impl<'de> serde::de::Visitor<'de> for CompoundVisitor {
type Value = NbtCompound;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an NBT compound")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut compound = NbtCompound::new();
while let Some((key, value)) = map.next_entry::<Box<str>, NbtTag>()? {
compound.child_tags.insert(key, value);
}
Ok(compound)
}
}
deserializer.deserialize_map(CompoundVisitor)
}
}
impl From<NbtCompound> for NbtTag {
fn from(value: NbtCompound) -> Self {
Self::Compound(value)

View File

@ -1,38 +1,14 @@
//! Deserialization from Java Edition, unnamed network, and Bedrock NBT.
use std::borrow::Cow;
use std::cell::RefCell;
use std::io::{Cursor, Seek, SeekFrom};
use crate::{
BYTE_ARRAY_ID, BYTE_ID, COMPOUND_ID, END_ID, Error, INT_ARRAY_ID, INT_ID, LIST_ID,
LONG_ARRAY_ID, LONG_ID, MAX_ARRAY_LENGTH, NbtTag, io,
};
use crate::{Error, io};
use io::Read;
use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, forward_to_deserialize_any};
/// Result type returned by NBT deserialization operations.
pub type Result<T> = std::result::Result<T, Error>;
thread_local! {
/// Tag ID of the sequence currently visited by Serde.
///
/// This is used to preserve the distinction between NBT lists and the
/// three specialized NBT array types.
pub static CURR_VISITOR_LIST_TYPE: RefCell<Option<u8>> = const { std::cell::RefCell::new(None) };
}
pub(super) fn take_curr_visitor_seq_list_id() -> Option<u8> {
CURR_VISITOR_LIST_TYPE.with(std::cell::RefCell::take)
}
pub(super) fn set_curr_visitor_seq_list_id(tag: Option<u8>) {
CURR_VISITOR_LIST_TYPE.with(|cell| {
*cell.borrow_mut() = tag;
});
}
/// Byte source used by NBT read helpers.
///
/// Implementations may return borrowed strings and byte arrays when the
@ -453,334 +429,3 @@ impl<'a, D: NbtDataSource<'a>> NbtReadHelper<'a> for NbtReadHelperBedrock<D> {
self.reader.read_byte_array(len)
}
}
/// A Serde deserializer backed by a format-specific NBT reader.
pub struct Deserializer<R> {
input: R,
tag_to_deserialize_stack: Option<u8>,
in_list: bool,
is_named: bool,
}
impl<R> Deserializer<R> {
/// Creates a deserializer.
///
/// When `is_named` is `true`, the root compound name is consumed from the
/// input. Unnamed network NBT must pass `false`.
pub const fn new(input: R, is_named: bool) -> Self {
Self {
input,
tag_to_deserialize_stack: None,
in_list: false,
is_named,
}
}
}
/// Deserializes a value from named Java Edition NBT.
pub fn from_bytes<'a, T: Deserialize<'a>>(r: impl Read + Seek) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(NbtStreamReader(r)), true);
T::deserialize(&mut deserializer)
}
/// Deserializes a value from unnamed Java Edition network NBT.
pub fn from_bytes_unnamed<'a, T: Deserialize<'a>>(r: impl Read + Seek) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(NbtStreamReader(r)), false);
T::deserialize(&mut deserializer)
}
/// Deserializes a value from named Bedrock network NBT.
pub fn from_bytes_bedrock<'a, T: Deserialize<'a>>(r: impl Read + Seek) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperBedrock::new(NbtStreamReader(r)), true);
T::deserialize(&mut deserializer)
}
/// Deserializes a value from a named Java Edition NBT slice.
///
/// Strings and byte arrays may borrow directly from `slice`.
pub fn from_slice<'a, T: Deserialize<'a>>(slice: &'a [u8]) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(Cursor::new(slice)), true);
T::deserialize(&mut deserializer)
}
/// Deserializes a value from an unnamed Java Edition network NBT slice.
///
/// Strings and byte arrays may borrow directly from `slice`.
pub fn from_slice_unnamed<'a, T: Deserialize<'a>>(slice: &'a [u8]) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(Cursor::new(slice)), false);
T::deserialize(&mut deserializer)
}
/// Deserializes a value from a named Bedrock network NBT slice.
///
/// Strings and byte arrays may borrow directly from `slice`.
pub fn from_slice_bedrock<'a, T: Deserialize<'a>>(slice: &'a [u8]) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperBedrock::new(Cursor::new(slice)), true);
T::deserialize(&mut deserializer)
}
impl<'de, R: NbtReadHelper<'de>> de::Deserializer<'de> for &mut Deserializer<R> {
type Error = Error;
forward_to_deserialize_any! {
i8 i16 i32 i64 f32 f64 char unit unit_struct seq tuple tuple_struct
newtype_struct byte_buf
}
fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let Some(tag) = self.tag_to_deserialize_stack else {
return Err(Error::SerdeError("Ignoring nothing!".to_string()));
};
NbtTag::skip_data(&mut self.input, tag)?;
visitor.visit_unit()
}
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let Some(tag_to_deserialize) = self.tag_to_deserialize_stack else {
return Err(Error::SerdeError(
"The top level must be a component (e.g. a struct)".to_string(),
));
};
match tag_to_deserialize {
END_ID => Err(Error::SerdeError(
"Trying to deserialize an END tag!".to_string(),
)),
LIST_ID | INT_ARRAY_ID | LONG_ARRAY_ID | BYTE_ARRAY_ID => {
let list_type = match tag_to_deserialize {
LIST_ID => self.input.get_u8()?,
INT_ARRAY_ID => INT_ID,
LONG_ARRAY_ID => LONG_ID,
BYTE_ARRAY_ID => BYTE_ID,
_ => return Err(Error::SerdeError("Unreachable state reached".to_string())),
};
let remaining_values = self.input.get_i32()?;
if remaining_values < 0 {
return Err(Error::NegativeLength(remaining_values));
}
let remaining_values = remaining_values as usize;
if remaining_values > MAX_ARRAY_LENGTH {
return Err(Error::LargeLength(remaining_values));
}
set_curr_visitor_seq_list_id(Some(list_type));
let result = visitor.visit_seq(ListAccess {
de: self,
list_type,
remaining_values,
})?;
Ok(result)
}
COMPOUND_ID => visitor.visit_map(CompoundAccess { de: self }),
_ => {
let result = match NbtTag::deserialize_data(&mut self.input, tag_to_deserialize)? {
NbtTag::Byte(value) => visitor.visit_i8::<Error>(value)?,
NbtTag::Short(value) => visitor.visit_i16::<Error>(value)?,
NbtTag::Int(value) => visitor.visit_i32::<Error>(value)?,
NbtTag::Long(value) => visitor.visit_i64::<Error>(value)?,
NbtTag::Float(value) => visitor.visit_f32::<Error>(value)?,
NbtTag::Double(value) => visitor.visit_f64::<Error>(value)?,
NbtTag::String(value) => visitor.visit_string::<Error>(value.into_string())?,
_ => return Err(Error::SerdeError("Unreachable state reached".to_string())),
};
Ok(result)
}
}
}
fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
if self.in_list {
let value = self.input.get_u8()?;
visitor.visit_u8::<Error>(value)
} else {
Err(Error::UnsupportedType(
"u8; NBT only supports signed values".to_string(),
))
}
}
fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let value = self.input.get_i16()?;
visitor.visit_i16::<Error>(value)
}
fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let value = self.input.get_i32()?;
visitor.visit_i32::<Error>(value)
}
fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let value = self.input.get_i64()?;
visitor.visit_i64::<Error>(value)
}
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
if self.tag_to_deserialize_stack.unwrap() == BYTE_ID {
let value = self.input.get_u8()?;
if value != 0 {
return visitor.visit_bool(true);
}
}
visitor.visit_bool(false)
}
fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
match self.input.get_string()? {
Cow::Borrowed(s) => visitor.visit_borrowed_str(s),
Cow::Owned(s) => visitor.visit_string(s),
}
}
fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
self.deserialize_str(visitor)
}
fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let tag_id = self.tag_to_deserialize_stack.unwrap_or(BYTE_ARRAY_ID);
if tag_id == BYTE_ARRAY_ID {
let len = self.input.get_i32()?;
if len < 0 {
return Err(Error::NegativeLength(len));
}
let slice = self.input.get_byte_array(len as usize)?;
let u8_slice: &'de [u8] =
unsafe { std::slice::from_raw_parts(slice.as_ptr().cast::<u8>(), slice.len()) };
match slice {
Cow::Borrowed(_) => visitor.visit_borrowed_bytes(u8_slice),
Cow::Owned(_) => visitor.visit_bytes(u8_slice),
}
} else {
self.deserialize_any(visitor)
}
}
fn deserialize_enum<V: Visitor<'de>>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value> {
match self.input.get_string()? {
Cow::Borrowed(s) => visitor.visit_enum(s.into_deserializer()),
Cow::Owned(s) => visitor.visit_enum(s.into_deserializer()),
}
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_some(self)
}
fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
if let Some(tag_id) = self.tag_to_deserialize_stack {
if tag_id != COMPOUND_ID {
return Err(Error::SerdeError(format!(
"Trying to deserialize a map without a compound ID (id {tag_id})"
)));
}
} else {
let next_byte = self.input.get_u8()?;
if next_byte != COMPOUND_ID {
return Err(Error::NoRootCompound(next_byte));
}
if self.is_named {
self.input.skip_string()?;
}
}
let value = visitor.visit_map(CompoundAccess { de: self })?;
Ok(value)
}
fn deserialize_struct<V: Visitor<'de>>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value> {
self.deserialize_map(visitor)
}
fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
self.deserialize_str(visitor)
}
fn is_human_readable(&self) -> bool {
false
}
}
struct CompoundAccess<'a, R> {
de: &'a mut Deserializer<R>,
}
impl<'de, R: NbtReadHelper<'de>> MapAccess<'de> for CompoundAccess<'_, R> {
type Error = Error;
fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
let tag = self.de.input.get_u8()?;
self.de.tag_to_deserialize_stack = Some(tag);
if tag == END_ID {
return Ok(None);
}
seed.deserialize(MapKey { de: self.de }).map(Some)
}
fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
seed.deserialize(&mut *self.de)
}
}
struct MapKey<'a, R> {
de: &'a mut Deserializer<R>,
}
impl<'de, R: NbtReadHelper<'de>> de::Deserializer<'de> for MapKey<'_, R> {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
match self.de.input.get_string()? {
Cow::Borrowed(s) => visitor.visit_borrowed_str(s),
Cow::Owned(s) => visitor.visit_string(s),
}
}
forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit unit_struct seq tuple tuple_struct map
struct identifier ignored_any bytes enum newtype_struct byte_buf option
}
}
struct ListAccess<'a, R> {
de: &'a mut Deserializer<R>,
remaining_values: usize,
list_type: u8,
}
impl<'de, R: NbtReadHelper<'de>> SeqAccess<'de> for ListAccess<'_, R> {
type Error = Error;
fn size_hint(&self) -> Option<usize> {
Some(self.remaining_values)
}
fn next_element_seed<E: DeserializeSeed<'de>>(&mut self, seed: E) -> Result<Option<E::Value>> {
if self.remaining_values == 0 {
return Ok(None);
}
self.remaining_values -= 1;
self.de.tag_to_deserialize_stack = Some(self.list_type);
self.de.in_list = true;
let result = seed.deserialize(&mut *self.de).map(Some);
self.de.in_list = false;
result
}
}

View File

@ -1,66 +1,34 @@
//! Reading, writing, and manipulating Minecraft's Named Binary Tag (NBT) data.
//!
//! The crate supports the standard Java Edition representation, unnamed network
//! NBT, Bedrock network NBT, and gzip-compressed NBT. Data can be handled either
//! as [`Nbt`] and [`NbtCompound`] values or through Serde with [`to_bytes`] and
//! [`from_slice`].
//!
//! # Serde round trip
//!
//! ```
//! use pumpkin_nbt::{from_slice, to_bytes};
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
//! struct Player {
//! name: String,
//! health: i16,
//! }
//!
//! let player = Player {
//! name: "Steve".to_owned(),
//! health: 20,
//! };
//! let mut encoded = Vec::new();
//! to_bytes(&player, &mut encoded)?;
//!
//! let decoded: Player = from_slice(&encoded)?;
//! assert_eq!(decoded, player);
//! # Ok::<(), pumpkin_nbt::Error>(())
//! ```
//! NBT, Bedrock network NBT, and gzip-compressed NBT. Data is handled directly
//! via [`Nbt`], [`NbtCompound`], and [`NbtTag`].
use std::{
fmt::Display,
io::{self, Write},
ops::Deref,
};
use bytes::Bytes;
use deserializer::NbtReadHelper;
use serde::{de, ser};
use serializer::{NbtWriteHelper, NbtWriteHelperBedrock, NbtWriteHelperJava};
use tag::NbtTag;
use thiserror::Error;
/// Compound-tag storage and construction helpers.
pub mod compound;
/// Serde and low-level NBT deserialization support.
/// Low-level NBT deserialization support.
pub mod deserializer;
/// Reading and writing gzip-compressed NBT.
pub mod nbt_compress;
/// Integration with Pumpkin's dynamic codec operations.
pub mod nbt_ops;
/// Serde and low-level NBT serialization support.
/// Low-level NBT serialization support.
pub mod serializer;
/// The individual NBT tag types.
pub mod tag;
pub use compound::NbtCompound;
pub use deserializer::{
from_bytes, from_bytes_bedrock, from_bytes_unnamed, from_slice, from_slice_bedrock,
from_slice_unnamed,
};
pub use serializer::{to_bytes, to_bytes_named, to_bytes_unnamed};
// This NBT crate is inspired from CrabNBT
@ -132,18 +100,6 @@ pub enum Error {
VarLongTooLarge,
}
impl ser::Error for Error {
fn custom<T: Display>(msg: T) -> Self {
Self::SerdeError(msg.to_string())
}
}
impl de::Error for Error {
fn custom<T: Display>(msg: T) -> Self {
Self::SerdeError(msg.to_string())
}
}
/// A complete NBT document containing a named root compound.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Nbt {
@ -283,801 +239,3 @@ impl AsMut<NbtCompound> for Nbt {
&mut self.root_tag
}
}
// TODO: This is a bit hacky
pub(crate) const NBT_ARRAY_TAG: &str = "__nbt_array";
pub(crate) const NBT_INT_ARRAY_TAG: &str = "__nbt_int_array";
pub(crate) const NBT_LONG_ARRAY_TAG: &str = "__nbt_long_array";
pub(crate) const NBT_BYTE_ARRAY_TAG: &str = "__nbt_byte_array";
macro_rules! impl_array {
($name:ident, $variant:expr) => {
#[doc = "Serializes a sequence using its specialized NBT array representation."]
#[doc = ""]
#[doc = "Use this function with Serde's `serialize_with` field attribute."]
pub fn $name<T: serde::Serialize, S: serde::Serializer>(
input: T,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_newtype_variant(NBT_ARRAY_TAG, 0, $variant, &input)
}
};
}
impl_array!(nbt_int_array, NBT_INT_ARRAY_TAG);
impl_array!(nbt_long_array, NBT_LONG_ARRAY_TAG);
impl_array!(nbt_byte_array, NBT_BYTE_ARRAY_TAG);
#[cfg(test)]
mod test {
use std::io::Cursor;
use crate::Error;
use crate::compound::NbtCompound;
use crate::deserializer::from_bytes;
use crate::deserializer::from_bytes_bedrock;
use crate::nbt_byte_array;
use crate::nbt_int_array;
use crate::nbt_long_array;
use crate::serializer::NbtWriteHelperBedrock;
use crate::serializer::to_bytes_bedrock;
use crate::serializer::to_bytes_named;
use crate::serializer::to_bytes_named_bedrock;
use crate::serializer::{NbtWriteHelperJava, to_bytes};
use crate::tag::NbtTag;
use crate::{deserializer::from_bytes_unnamed, serializer::to_bytes_unnamed};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Test {
byte: i8,
short: i16,
int: i32,
long: i64,
float: f32,
string: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct BorrowedTest<'a> {
byte: i8,
string: &'a str,
}
#[test]
fn zero_alloc_from_slice() {
let test = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Zero alloc NBT".to_string(),
};
let mut bytes = Vec::new();
to_bytes_unnamed(&test, &mut bytes).unwrap();
let borrowed: BorrowedTest = crate::from_slice_unnamed(&bytes).unwrap();
assert_eq!(borrowed.byte, 123);
assert_eq!(borrowed.string, "Zero alloc NBT");
}
#[test]
fn simple_ser_de_unnamed() {
let test = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let mut bytes = Vec::new();
to_bytes_unnamed(&test, &mut bytes).unwrap();
let recreated_struct: Test = from_bytes_unnamed(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[test]
fn simple_ser_de_unnamed_bedrock() {
let test = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
// Bedrock doesn't actually use unnamed NBT (AFAIK). `to_bytes_bedrock` actually encodes empty name.
let mut bytes = Vec::new();
to_bytes_bedrock(&test, &mut bytes).unwrap();
let recreated_struct: Test = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[expect(clippy::struct_field_names)]
struct TestArray {
#[serde(serialize_with = "nbt_byte_array")]
byte_array: Vec<u8>,
#[serde(serialize_with = "nbt_int_array")]
int_array: Vec<i32>,
#[serde(serialize_with = "nbt_long_array")]
long_array: Vec<i64>,
}
#[test]
fn simple_ser_de_array() {
let test = TestArray {
byte_array: vec![0, 3, 2],
int_array: vec![13, 1321, 2],
long_array: vec![1, 0, 200301, 1],
};
let mut bytes = Vec::new();
to_bytes_unnamed(&test, &mut bytes).unwrap();
let recreated_struct: TestArray = from_bytes_unnamed(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[test]
fn simple_ser_de_array_bedrock() {
let test = TestArray {
byte_array: vec![0, 3, 2],
int_array: vec![13, 1321, 2],
long_array: vec![1, 0, 200301, 1],
};
let mut bytes = Vec::new();
to_bytes_bedrock(&test, &mut bytes).unwrap();
let recreated_struct: TestArray = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[test]
fn simple_ser_de_named() {
let name = String::from("Test");
let test = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let mut bytes = Vec::new();
to_bytes_named(&test, name, &mut bytes).unwrap();
let recreated_struct: Test = from_bytes(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[test]
fn simple_ser_de_named_bedrock() {
let name = String::from("Test");
let test = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let mut bytes = Vec::new();
to_bytes_named_bedrock(&test, name, &mut bytes).unwrap();
let recreated_struct: Test = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[test]
fn simple_ser_de_array_named() {
let name = String::from("Test");
let test = TestArray {
byte_array: vec![0, 3, 2],
int_array: vec![13, 1321, 2],
long_array: vec![1, 0, 200301, 1],
};
let mut bytes = Vec::new();
to_bytes_named(&test, name, &mut bytes).unwrap();
let recreated_struct: TestArray = from_bytes(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[test]
fn simple_ser_de_array_named_bedrock() {
let name = String::from("Test");
let test = TestArray {
byte_array: vec![0, 3, 2],
int_array: vec![13, 1321, 2],
long_array: vec![1, 0, 200301, 1],
};
let mut bytes = Vec::new();
to_bytes_named_bedrock(&test, name, &mut bytes).unwrap();
let recreated_struct: TestArray = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(test, recreated_struct);
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Egg {
food: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Breakfast {
food: Egg,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct TestList {
option: Option<Egg>,
nested_compound: Breakfast,
compounds: Vec<Test>,
list_string: Vec<String>,
empty: Vec<Test>,
}
#[test]
fn list() {
let test1 = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let test2 = Test {
byte: 13,
short: 342,
int: -4313,
long: -132334,
float: -69.420,
string: "Hello compounds".to_string(),
};
let list_compound = TestList {
option: Some(Egg {
food: "Skibid".to_string(),
}),
nested_compound: Breakfast {
food: Egg {
food: "Over easy".to_string(),
},
},
compounds: vec![test1, test2],
list_string: vec![String::new(), "abcbcbcbbc".to_string()],
empty: vec![],
};
let mut bytes = Vec::new();
to_bytes_unnamed(&list_compound, &mut bytes).unwrap();
let recreated_struct: TestList = from_bytes_unnamed(Cursor::new(bytes)).unwrap();
assert_eq!(list_compound, recreated_struct);
}
#[test]
fn list_bedrock() {
let test1 = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let test2 = Test {
byte: 13,
short: 342,
int: -4313,
long: -132334,
float: -69.420,
string: "Hello compounds".to_string(),
};
let list_compound = TestList {
option: Some(Egg {
food: "Skibid".to_string(),
}),
nested_compound: Breakfast {
food: Egg {
food: "Over easy".to_string(),
},
},
compounds: vec![test1, test2],
list_string: vec![String::new(), "abcbcbcbbc".to_string()],
empty: vec![],
};
let mut bytes = Vec::new();
to_bytes_bedrock(&list_compound, &mut bytes).unwrap();
let recreated_struct: TestList = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(list_compound, recreated_struct);
}
#[test]
fn list_named() {
let test1 = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let test2 = Test {
byte: 13,
short: 342,
int: -4313,
long: -132334,
float: -69.420,
string: "Hello compounds".to_string(),
};
let list_compound = TestList {
option: None,
nested_compound: Breakfast {
food: Egg {
food: "Over easy".to_string(),
},
},
compounds: vec![test1, test2],
list_string: vec![String::new(), "abcbcbcbbc".to_string()],
empty: vec![],
};
let mut bytes = Vec::new();
to_bytes_named(&list_compound, "a".to_string(), &mut bytes).unwrap();
let recreated_struct: TestList = from_bytes(Cursor::new(bytes)).unwrap();
assert_eq!(list_compound, recreated_struct);
}
#[test]
fn list_named_bedrock() {
let test1 = Test {
byte: 123,
short: 1342,
int: 4313,
long: 34,
float: 1.00,
string: "Hello test".to_string(),
};
let test2 = Test {
byte: 13,
short: 342,
int: -4313,
long: -132334,
float: -69.420,
string: "Hello compounds".to_string(),
};
let list_compound = TestList {
option: None,
nested_compound: Breakfast {
food: Egg {
food: "Over easy".to_string(),
},
},
compounds: vec![test1, test2],
list_string: vec![String::new(), "abcbcbcbbc".to_string()],
empty: vec![],
};
let mut bytes = Vec::new();
to_bytes_named_bedrock(&list_compound, "a".to_string(), &mut bytes).unwrap();
let recreated_struct: TestList = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(list_compound, recreated_struct);
}
#[test]
fn wrapper_compound_lists() {
let mut vec: Vec<NbtTag> = Vec::new();
// These tags will be wrapped during serialization.
vec.push(NbtTag::Int(-1823));
vec.push(NbtTag::Int(123));
vec.push(NbtTag::String("Not an int".into()));
vec.push(NbtTag::Byte(2));
// This compound will not, since the list is already a list of compound tags.
// This compound cannot be unwrapped in any way, so it is preserved
// on deserialization.
vec.push(NbtTag::Compound({
let mut compound = NbtCompound::new();
compound.put_short("example", 1234);
compound
}));
// This wrapper compound will be wrapped because we want to preserve the
// original data during deserialization.
//
// Suppose we had {"": `tag`}. If we didn't wrap this, on deserialization,
// we would get `tag`, which doesn't match the serialized compound tag.
// Therefore, we must wrap it and serialize {"": {"": `tag`}}.
// Then on deserialization, we get {"": `tag`}, which matches what we wanted
// to serialize in the first place.
//
// This compound represents {"": 1L}.
vec.push(NbtTag::Compound({
let mut compound = NbtCompound::new();
compound.put_long("", 1);
compound
}));
let expected_bytes = [
0x09, // List type
0x0A, // This list is a compound tag list
0x00, 0x00, 0x00, 0x06, // This list has 6 elements.
// Now for parsing each compound tag:
0x03, // Int type
0x00, 0x00, // Empty key
0xFF, 0xFF, 0xF8, 0xE1, // -1823
0x00, // End
0x03, // Int type
0x00, 0x00, // Empty key
0x00, 0x00, 0x00, 0x7B, // 123
0x00, // End
0x08, // String type
0x00, 0x00, // Empty key
0x00, 0x0A, // The string is 10 characters long.
0x4E, 0x6F, 0x74, 0x20, 0x61, 0x6E, 0x20, 0x69, 0x6E, 0x74, // "Not an int"
0x00, // End
0x01, // Byte type
0x00, 0x00, // Empty key
0x02, // 2b
0x00, // End
// For the first (unwrapped) compound:
0x02, // Short type
0x00, 0x07, // The key is 7 characters long.
0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // "example"
0x04, 0xD2, // 1234
0x00, // End
// For the second (wrapped) wrapper compound:
0x0A, // Compound type
0x00, 0x00, // Empty key
0x04, // Long type
0x00, 0x00, // Empty key
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 1L
0x00, // End
0x00, // End
];
let mut bytes = Vec::new();
let mut write_adaptor = NbtWriteHelperJava::new(&mut bytes);
NbtTag::List(vec)
.serialize(&mut write_adaptor)
.expect("Expected serialization to succeed");
assert_eq!(bytes, expected_bytes);
}
#[test]
fn wrapper_compound_lists_bedrock() {
let mut vec: Vec<NbtTag> = Vec::new();
// These tags will be wrapped during serialization.
vec.push(NbtTag::Int(-1823));
vec.push(NbtTag::Int(123));
vec.push(NbtTag::String("Not an int".into()));
vec.push(NbtTag::Byte(2));
// This compound will not, since the list is already a list of compound tags.
// This compound cannot be unwrapped in any way, so it is preserved
// on deserialization.
vec.push(NbtTag::Compound({
let mut compound = NbtCompound::new();
compound.put_short("example", 1234);
compound
}));
// This wrapper compound will be wrapped because we want to preserve the
// original data during deserialization.
//
// Suppose we had {"": `tag`}. If we didn't wrap this, on deserialization,
// we would get `tag`, which doesn't match the serialized compound tag.
// Therefore, we must wrap it and serialize {"": {"": `tag`}}.
// Then on deserialization, we get {"": `tag`}, which matches what we wanted
// to serialize in the first place.
//
// This compound represents {"": 1L}.
vec.push(NbtTag::Compound({
let mut compound = NbtCompound::new();
compound.put_long("", 1);
compound
}));
let expected_bytes = [
0x09, // List type
0x0A, // This list is a compound tag list
0xC, // This list has 6 elements.
// Now for parsing each compound tag:
0x03, // Int type
0x00, // Empty key
0xBD, 0x1C, // -1823
0x00, // End
0x03, // Int type
0x00, // Empty key
0xF6, 0x01, // 123
0x00, // End
0x08, // String type
0x00, // Empty key
0x0A, // The string is 10 characters long.
0x4E, 0x6F, 0x74, 0x20, 0x61, 0x6E, 0x20, 0x69, 0x6E, 0x74, // "Not an int"
0x00, // End
0x01, // Byte type
0x00, // Empty key
0x02, // 2b
0x00, // End
// For the first (unwrapped) compound:
0x02, // Short type
0x07, // The key is 7 characters long.
0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // "example"
0xD2, 0x04, // 1234
0x00, // End
// For the second (wrapped) wrapper compound:
0x0A, // Compound type
0x00, // Empty key
0x04, // Long type
0x00, // Empty key
0x02, // 1L
0x00, // End
0x00, // End
];
let mut bytes = Vec::new();
let mut write_adaptor = NbtWriteHelperBedrock::new(&mut bytes);
NbtTag::List(vec)
.serialize(&mut write_adaptor)
.expect("Expected serialization to succeed");
assert_eq!(bytes, expected_bytes);
}
#[test]
fn nbt_arrays() {
#[derive(Serialize)]
struct Tagged {
#[serde(serialize_with = "nbt_long_array")]
l: [i64; 1],
#[serde(serialize_with = "nbt_int_array")]
i: [i32; 1],
#[serde(serialize_with = "nbt_byte_array")]
b: [u8; 1],
}
#[derive(Serialize)]
struct NotTagged {
l: [i64; 1],
i: [i32; 1],
b: [u8; 1],
}
let value = Tagged {
l: [0],
i: [0],
b: [0],
};
let expected_bytes = [
0x0A, // Component Tag
0x00, 0x00, // Empty root name
0x0C, // Long Array Type
0x00, 0x01, // Key length
0x6C, // Key (l)
0x00, 0x00, 0x00, 0x01, // Array Length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Value(s)
0x0B, // Int Array Tag
0x00, 0x01, // Key length
0x69, // Key (i)
0x00, 0x00, 0x00, 0x01, // Array Length
0x00, 0x00, 0x00, 0x00, // Value(s)
0x07, // Byte Array Tag
0x00, 0x01, // Key length
0x62, // Key (b)
0x00, 0x00, 0x00, 0x01, // Array Length
0x00, // Value(s)
0x00, // End Tag
];
let mut bytes = Vec::new();
to_bytes(&value, &mut bytes).unwrap();
assert_eq!(bytes, expected_bytes);
let value = NotTagged {
l: [0],
i: [0],
b: [0],
};
let expected_bytes = [
0x0A, // Component Tag
0x00, 0x00, // Empty root name
0x09, // List Tag
0x00, 0x01, // Key length
0x6C, // Key (l)
0x04, // Array Type
0x00, 0x00, 0x00, 0x01, // Array Length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Value(s)
0x09, // List Tag
0x00, 0x01, // Key length
0x69, // Key (i)
0x03, // Array Type
0x00, 0x00, 0x00, 0x01, // Array Length
0x00, 0x00, 0x00, 0x00, // Value(s)
0x09, // List Tag
0x00, 0x01, // Key length
0x62, // Key (b)
0x01, // Array Type
0x00, 0x00, 0x00, 0x01, // Array Length
0x00, // Value(s)
0x00, // End Tag
];
let mut bytes = Vec::new();
to_bytes(&value, &mut bytes).unwrap();
assert_eq!(bytes, expected_bytes);
}
#[test]
fn nbt_arrays_bedrock() {
#[derive(Serialize)]
struct Tagged {
#[serde(serialize_with = "nbt_long_array")]
l: [i64; 1],
#[serde(serialize_with = "nbt_int_array")]
i: [i32; 1],
#[serde(serialize_with = "nbt_byte_array")]
b: [u8; 1],
}
#[derive(Serialize)]
struct NotTagged {
l: [i64; 1],
i: [i32; 1],
b: [u8; 1],
}
let value = Tagged {
l: [0],
i: [0],
b: [0],
};
let expected_bytes = [
0x0A, // Component Tag
0x00, // Empty root name
0x0C, // Long Array Type
0x01, // Key length
0x6C, // Key (l)
0x02, // Array Length
0x00, // Value(s)
0x0B, // Int Array Tag
0x01, // Key length
0x69, // Key (i)
0x02, // Array Length
0x00, // Value(s)
0x07, // Byte Array Tag
0x01, // Key length
0x62, // Key (b)
0x02, // Array Length
0x00, // Value(s)
0x00, // End Tag
];
let mut bytes = Vec::new();
to_bytes_bedrock(&value, &mut bytes).unwrap();
assert_eq!(bytes, expected_bytes);
let value = NotTagged {
l: [0],
i: [0],
b: [0],
};
let expected_bytes = [
0x0A, // Component Tag
0x00, // Empty root name
0x09, // List Tag
0x01, // Key length
0x6C, // Key (l)
0x04, // Array Type
0x02, // Array Length
0x00, // Value(s)
0x09, // List Tag
0x01, // Key length
0x69, // Key (i)
0x03, // Array Type
0x02, // Array Length
0x00, // Value(s)
0x09, // List Tag
0x01, // Key length
0x62, // Key (b)
0x01, // Array Type
0x02, // Array Length
0x00, // Value(s)
0x00, // End Tag
];
let mut bytes = Vec::new();
to_bytes_bedrock(&value, &mut bytes).unwrap();
assert_eq!(bytes, expected_bytes);
}
#[test]
fn tuple_fail() {
#[derive(Serialize)]
struct BadData {
x: (i32, i64),
}
let value = BadData { x: (0, 0) };
let mut bytes = Vec::new();
let err = to_bytes(&value, &mut bytes);
match err {
Err(Error::SerdeError(_)) => (),
_ => panic!("Expected to fail serialization!"),
}
}
#[test]
fn tuple_fail_bedrock() {
#[derive(Serialize)]
struct BadData {
x: (i32, i64),
}
let value = BadData { x: (0, 0) };
let mut bytes = Vec::new();
let err = to_bytes_bedrock(&value, &mut bytes);
match err {
Err(Error::SerdeError(_)) => (),
_ => panic!("Expected to fail serialization!"),
}
}
#[test]
fn tuple_ok() {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct GoodData {
x: (i32, i32),
}
let value = GoodData { x: (1, 2) };
let mut bytes = Vec::new();
to_bytes(&value, &mut bytes).unwrap();
let reconstructed = from_bytes(Cursor::new(bytes)).unwrap();
assert_eq!(value, reconstructed);
}
#[test]
fn tuple_ok_bedrock() {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct GoodData {
x: (i32, i32),
}
let value = GoodData { x: (1, 2) };
let mut bytes = Vec::new();
to_bytes_bedrock(&value, &mut bytes).unwrap();
let reconstructed = from_bytes_bedrock(Cursor::new(bytes)).unwrap();
assert_eq!(value, reconstructed);
}
// TODO: More robust tests
}

View File

@ -1,7 +1,7 @@
//! Helpers for reading and writing gzip-compressed NBT data.
use crate::deserializer::NbtReadHelperJava;
use crate::{Error, Nbt, NbtCompound, deserializer, serializer};
use crate::{Error, Nbt, NbtCompound};
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use std::io::{Cursor, Read, Seek, Write};
@ -47,49 +47,15 @@ pub fn write_gzip_compound_tag_to_bytes(compound: NbtCompound) -> Result<Vec<u8>
Ok(buffer)
}
/// Deserializes a value from gzip-compressed, named Java Edition NBT.
///
/// Decompressed data is limited to 64 MiB.
pub fn from_gzip_bytes<'a, T: serde::Deserialize<'a>, R: Read>(input: R) -> Result<T, Error> {
// Create a GZip decoder and directly use it for deserialization
let mut decoder = GzDecoder::new(input).take(64 * 1024 * 1024); // 64 MB limit
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).map_err(Error::Incomplete)?;
deserializer::from_bytes(Cursor::new(buf))
}
/// Serializes a value as gzip-compressed Java Edition NBT.
///
/// The root name is written as an empty string.
pub fn to_gzip_bytes<T: serde::Serialize, W: Write>(value: &T, output: W) -> Result<(), Error> {
// Create a GZip encoder that writes to the output
let encoder = GzEncoder::new(output, Compression::default());
// Serialize directly to the encoder
serializer::to_bytes(value, encoder)
}
/// Serializes a value into a gzip-compressed Java Edition NBT byte vector.
///
/// The root name is written as an empty string.
pub fn to_gzip_bytes_vec<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, Error> {
let mut buffer = Vec::new();
to_gzip_bytes(value, &mut buffer)?;
Ok(buffer)
}
#[cfg(test)]
mod tests {
use crate::{
NbtCompound,
nbt_compress::{
from_gzip_bytes, read_gzip_compound_tag, to_gzip_bytes, to_gzip_bytes_vec,
write_gzip_compound_tag, write_gzip_compound_tag_to_bytes,
read_gzip_compound_tag, write_gzip_compound_tag, write_gzip_compound_tag_to_bytes,
},
tag::NbtTag,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::Cursor;
@ -190,141 +156,6 @@ mod tests {
assert_eq!(read_compound.get_int("value_999"), Some(999));
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct TestStruct {
string_field: String,
int_field: i32,
bool_field: bool,
float_field: f32,
string_list: Vec<String>,
nested: NestedStruct,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct NestedStruct {
value: i64,
name: String,
}
#[test]
fn gzip_serialize_deserialize() {
let test_struct = TestStruct {
string_field: "test string".to_string(),
int_field: 12345,
bool_field: true,
float_field: 123.456,
string_list: vec!["one".to_string(), "two".to_string(), "three".to_string()],
nested: NestedStruct {
value: 9876543210,
name: "nested_test".to_string(),
},
};
// Test streaming serialization
let mut buffer = Vec::new();
to_gzip_bytes(&test_struct, &mut buffer).expect("Failed to serialize and compress struct");
// Test streaming deserialization
let read_struct: TestStruct = from_gzip_bytes(Cursor::new(&buffer))
.expect("Failed to decompress and deserialize struct");
assert_eq!(read_struct, test_struct);
// Also test the convenience method
let buffer2 =
to_gzip_bytes_vec(&test_struct).expect("Failed to serialize and compress struct");
let read_struct2: TestStruct = from_gzip_bytes(Cursor::new(&buffer2))
.expect("Failed to decompress and deserialize struct");
assert_eq!(read_struct2, test_struct);
}
#[test]
#[expect(clippy::print_stdout)]
fn gzip_compression_ratio() {
let mut compound = NbtCompound::new();
// Create a compound with repetitive data (should compress well)
for _i in 0..1000 {
compound.put("repeated_key", NbtTag::String("this is a test string that will be repeated many times to demonstrate compression".into()));
}
let uncompressed = compound.child_tags.len() * 100; // rough estimate
let mut buffer = Vec::new();
write_gzip_compound_tag(compound, &mut buffer).expect("Failed to compress compound");
println!("Uncompressed size (est): {uncompressed} bytes");
println!("Compressed size: {} bytes", buffer.len());
println!(
"Compression ratio: {:.2}x",
uncompressed as f64 / buffer.len() as f64
);
// Just ensure we can read it back - actual compression ratio will vary
let _ = read_gzip_compound_tag(Cursor::new(buffer)).expect("Failed to decompress compound");
}
#[test]
fn gzip_invalid_data() {
// Try to read from invalid data
let invalid_data = vec![1, 2, 3, 4, 5]; // Not valid GZip data
let result = read_gzip_compound_tag(Cursor::new(invalid_data));
assert!(result.is_err());
}
#[test]
#[expect(clippy::struct_field_names)]
fn roundtrip_with_arrays() {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct ArrayTest {
byte_array: Vec<u8>,
int_array: Vec<i32>,
string_array: Vec<String>,
}
let test_struct = ArrayTest {
byte_array: vec![1, 2, 3, 4, 5],
int_array: vec![100, 200, 300, 400, 500],
string_array: vec!["one".to_string(), "two".to_string(), "three".to_string()],
};
let mut buffer = Vec::new();
to_gzip_bytes(&test_struct, &mut buffer).expect("Failed to serialize and compress");
let read_struct: ArrayTest =
from_gzip_bytes(Cursor::new(buffer)).expect("Failed to decompress and deserialize");
assert_eq!(read_struct, test_struct);
}
#[test]
fn roundtrip_with_map() {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct MapTest {
string_map: HashMap<String, String>,
int_map: HashMap<String, i32>,
}
let mut string_map = HashMap::new();
string_map.insert("key1".to_string(), "value1".to_string());
string_map.insert("key2".to_string(), "value2".to_string());
let mut int_map = HashMap::new();
int_map.insert("one".to_string(), 1);
int_map.insert("two".to_string(), 2);
let test_struct = MapTest {
string_map,
int_map,
};
let mut buffer = Vec::new();
to_gzip_bytes(&test_struct, &mut buffer).expect("Failed to serialize and compress");
let read_struct: MapTest =
from_gzip_bytes(Cursor::new(buffer)).expect("Failed to decompress and deserialize");
assert_eq!(read_struct, test_struct);
}
#[test]
fn direct_file_io() {
use tempfile::tempdir;

View File

@ -1,14 +1,6 @@
//! Serialization to Java Edition, unnamed network, and Bedrock NBT.
use serde::ser::Impossible;
use serde::{Serialize, ser};
use std::io::Write;
use crate::{
BYTE_ARRAY_ID, BYTE_ID, COMPOUND_ID, DOUBLE_ID, END_ID, Error, FLOAT_ID, INT_ARRAY_ID, INT_ID,
LIST_ID, LONG_ARRAY_ID, LONG_ID, NBT_ARRAY_TAG, NBT_BYTE_ARRAY_TAG, NBT_INT_ARRAY_TAG,
NBT_LONG_ARRAY_TAG, SHORT_ID, STRING_ID,
};
use crate::Error;
/// Result type returned by NBT serialization operations.
pub type Result<T> = std::result::Result<T, Error>;
@ -203,468 +195,3 @@ impl<W: Write> NbtWriteHelper for NbtWriteHelperBedrock<W> {
Ok(())
}
}
/// A Serde serializer backed by a format-specific NBT writer.
pub struct Serializer<W: NbtWriteHelper> {
output: W,
state: State,
handled_root: bool,
expected_list_tag: u8,
}
impl<W: NbtWriteHelper> Serializer<W> {
/// Creates a serializer with an optional root compound name.
///
/// Passing `None` writes unnamed network NBT.
pub const fn new(output: W, name: Option<String>) -> Self {
Self {
output,
state: State::Root(name),
handled_root: false,
expected_list_tag: 0,
}
}
}
// NBT has a different order of things than most other formats,
// so I use `State` to keep what the serializer has to do, and some information like the field name.
#[derive(Clone, Debug, PartialEq)]
enum State {
// In network NBT, the root name is not present.
Root(Option<String>),
Named(String),
// Used by maps to check if key is a `String`.
MapKey,
FirstListElement {
len: i32,
},
ListElement,
CheckedListElement,
Array {
name: String,
array_type: &'static str,
},
}
impl<W: NbtWriteHelper> Serializer<W> {
fn parse_state(&mut self, tag: u8) -> Result<()> {
match &mut self.state {
State::Named(name) | State::Array { name, .. } => {
self.output.write_u8(tag)?;
self.output.write_string(name)?;
}
State::FirstListElement { len } => {
self.output.write_u8(tag)?;
self.output.write_i32(*len)?;
self.expected_list_tag = tag;
}
State::MapKey => {
if tag != STRING_ID {
return Err(Error::SerdeError(format!(
"Map key can only be `String`, not {tag}"
)));
}
}
State::ListElement => {
// Rust rules mandate this is all the same type
}
State::CheckedListElement => {
if tag != self.expected_list_tag {
return Err(Error::SerdeError(format!(
"List values must all be of the same type! Expected {} but found {}!",
self.expected_list_tag, tag
)));
}
}
State::Root(root_name) => {
if self.handled_root {
return Err(Error::SerdeError(
"Invalid state: already handled root component!".to_string(),
));
}
if tag != COMPOUND_ID {
return Err(Error::SerdeError(format!(
"Invalid state: root is not a `Compound`! ({tag})"
)));
}
self.handled_root = true;
self.output.write_u8(tag)?;
if let Some(root_name) = root_name {
self.output.write_string(root_name)?;
}
}
}
Ok(())
}
}
/// Serializes a value as unnamed Java Edition network NBT.
pub fn to_bytes_unnamed<T: Serialize>(value: &T, w: impl Write) -> Result<()> {
let mut serializer = Serializer::new(NbtWriteHelperJava::new(w), None);
value.serialize(&mut serializer)?;
Ok(())
}
/// Serializes a value as named Java Edition NBT using `name` for the root compound.
pub fn to_bytes_named<T: Serialize>(value: &T, name: String, w: impl Write) -> Result<()> {
let mut serializer = Serializer::new(NbtWriteHelperJava::new(w), Some(name));
value.serialize(&mut serializer)?;
Ok(())
}
/// Serializes a value as named Bedrock network NBT using `name` for the root compound.
pub fn to_bytes_named_bedrock<T: Serialize>(value: &T, name: String, w: impl Write) -> Result<()> {
let mut serializer = Serializer::new(NbtWriteHelperBedrock::new(w), Some(name));
value.serialize(&mut serializer)?;
Ok(())
}
/// Serializes a value as Java Edition NBT with an empty root name.
pub fn to_bytes<T: Serialize>(value: &T, w: impl Write) -> Result<()> {
to_bytes_named(value, String::new(), w)
}
/// Serializes a value as Bedrock network NBT with an empty root name.
pub fn to_bytes_bedrock<T: Serialize>(value: &T, w: impl Write) -> Result<()> {
to_bytes_named_bedrock(value, String::new(), w)
}
impl<W: NbtWriteHelper> ser::Serializer for &mut Serializer<W> {
type Ok = ();
type Error = Error;
type SerializeSeq = Self;
type SerializeTuple = Self;
type SerializeTupleStruct = Impossible<(), Error>;
type SerializeTupleVariant = Impossible<(), Error>;
type SerializeMap = Self;
type SerializeStruct = Self;
type SerializeStructVariant = Impossible<(), Error>;
fn serialize_bool(self, v: bool) -> Result<()> {
self.serialize_i8(i8::from(v))?;
Ok(())
}
fn serialize_i8(self, v: i8) -> Result<()> {
self.parse_state(BYTE_ID)?;
self.output.write_i8(v)?;
Ok(())
}
fn serialize_i16(self, v: i16) -> Result<()> {
self.parse_state(SHORT_ID)?;
self.output.write_i16(v)?;
Ok(())
}
fn serialize_i32(self, v: i32) -> Result<()> {
self.parse_state(INT_ID)?;
self.output.write_i32(v)?;
Ok(())
}
fn serialize_i64(self, v: i64) -> Result<()> {
self.parse_state(LONG_ID)?;
self.output.write_i64(v)?;
Ok(())
}
fn serialize_u8(self, v: u8) -> Result<()> {
if let State::Named(_) = self.state {
Err(Error::UnsupportedType(
"u8; NBT only supports signed values".to_string(),
))
} else {
self.parse_state(BYTE_ID)?;
self.output.write_u8(v)?;
Ok(())
}
}
fn serialize_u16(self, v: u16) -> Result<()> {
self.serialize_i16(v as i16)
}
fn serialize_u32(self, v: u32) -> Result<()> {
self.serialize_i32(v as i32)
}
fn serialize_u64(self, v: u64) -> Result<()> {
self.serialize_i64(v as i64)
}
fn serialize_f32(self, v: f32) -> Result<()> {
self.parse_state(FLOAT_ID)?;
self.output.write_f32(v)?;
Ok(())
}
fn serialize_f64(self, v: f64) -> Result<()> {
self.parse_state(DOUBLE_ID)?;
self.output.write_f64(v)?;
Ok(())
}
fn serialize_char(self, _v: char) -> Result<()> {
Err(Error::UnsupportedType("char".to_string()))
}
fn serialize_str(self, v: &str) -> Result<()> {
self.parse_state(STRING_ID)?;
if self.state == State::MapKey {
self.state = State::Named(v.to_string());
} else {
self.output.write_string(v)?;
}
Ok(())
}
fn serialize_bytes(self, v: &[u8]) -> Result<()> {
self.parse_state(LIST_ID)?;
self.output.write_u8(BYTE_ID)?;
let len = v.len();
if len > i32::MAX as usize {
return Err(Error::LargeLength(len));
}
self.output.write_i32(len as i32)?;
self.output.write_slice(v)?;
Ok(())
}
// Just skip serializing if value is none
fn serialize_none(self) -> Result<()> {
match self.state {
State::FirstListElement { .. } | State::ListElement | State::CheckedListElement => Err(
Error::SerdeError("NBT lists do not support null/none values".to_string()),
),
_ => Ok(()),
}
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<()> {
value.serialize(self)
}
fn serialize_unit(self) -> Result<()> {
Ok(())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<()> {
Err(Error::UnsupportedType("unit struct".to_string()))
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<()> {
self.serialize_str(variant)?;
Ok(())
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
_value: &T,
) -> Result<()> {
Err(Error::UnsupportedType("newtype struct".to_string()))
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<()> {
if name == NBT_ARRAY_TAG {
let name = match self.state {
State::Named(ref name) => name.clone(),
_ => return Err(Error::SerdeError("Invalid `Serializer` state!".to_string())),
};
self.state = State::Array {
name,
array_type: variant,
};
} else {
return Err(Error::UnsupportedType("newtype variant".to_string()));
}
value.serialize(self)?;
Ok(())
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
let Some(len) = len else {
return Err(Error::SerdeError(
"The length of the sequence must be known first!".to_string(),
));
};
if len > i32::MAX as usize {
return Err(Error::LargeLength(len));
}
if let State::Array { array_type, .. } = &mut self.state {
let (id, expected_tag) = match *array_type {
NBT_BYTE_ARRAY_TAG => (BYTE_ARRAY_ID, BYTE_ID),
NBT_INT_ARRAY_TAG => (INT_ARRAY_ID, INT_ID),
NBT_LONG_ARRAY_TAG => (LONG_ARRAY_ID, LONG_ID),
_ => {
return Err(Error::SerdeError(
"Array supports only `byte`, `int`, and `long`".to_string(),
));
}
};
self.parse_state(id)?;
self.output.write_i32(len as i32)?;
// We can mark anything as an NBT array list, so mark as needed to be checked.
self.expected_list_tag = expected_tag;
self.state = State::CheckedListElement;
} else {
self.parse_state(LIST_ID)?;
self.state = State::FirstListElement { len: len as i32 };
if len == 0 {
// If we have no elements, the `FirstListElement` state will never be invoked, so
// write the (unknown) list type and length here.
self.output.write_u8(END_ID)?;
self.output.write_i32(0)?;
}
}
Ok(self)
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct> {
Err(Error::UnsupportedType("tuple struct".to_string()))
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant> {
Err(Error::UnsupportedType("tuple variant".to_string()))
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
self.parse_state(COMPOUND_ID)?;
Ok(self)
}
fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
self.parse_state(COMPOUND_ID)?;
Ok(self)
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant> {
Err(Error::UnsupportedType("struct variant".to_string()))
}
fn is_human_readable(&self) -> bool {
false
}
}
impl<W: NbtWriteHelper> ser::SerializeTuple for &mut Serializer<W> {
type Ok = ();
type Error = Error;
fn serialize_element<T: ?Sized + Serialize>(
&mut self,
value: &T,
) -> std::result::Result<(), Self::Error> {
value.serialize(&mut **self)?;
self.state = State::CheckedListElement;
Ok(())
}
fn end(self) -> Result<()> {
Ok(())
}
}
impl<W: NbtWriteHelper> ser::SerializeSeq for &mut Serializer<W> {
type Ok = ();
type Error = Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
value.serialize(&mut **self)?;
self.state = State::ListElement;
Ok(())
}
fn end(self) -> Result<()> {
Ok(())
}
}
impl<W: NbtWriteHelper> ser::SerializeStruct for &mut Serializer<W> {
type Ok = ();
type Error = Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<()> {
self.state = State::Named(key.to_string());
value.serialize(&mut **self)
}
fn end(self) -> Result<()> {
self.output.write_u8(END_ID)?;
Ok(())
}
}
impl<W: NbtWriteHelper> ser::SerializeMap for &mut Serializer<W> {
type Ok = ();
type Error = Error;
fn serialize_key<T: ?Sized + Serialize>(
&mut self,
key: &T,
) -> std::result::Result<(), Self::Error> {
self.state = State::MapKey;
key.serialize(&mut **self)
}
fn serialize_value<T: ?Sized + Serialize>(
&mut self,
value: &T,
) -> std::result::Result<(), Self::Error> {
value.serialize(&mut **self)
}
fn end(self) -> Result<()> {
self.output.write_u8(END_ID)?;
Ok(())
}
}

View File

@ -2,13 +2,12 @@
use compound::NbtCompound;
use deserializer::NbtReadHelper;
use serde::{Deserialize, Serialize};
use serializer::NbtWriteHelper;
use crate::{
BYTE_ARRAY_ID, BYTE_ID, COMPOUND_ID, DOUBLE_ID, END_ID, Error, FLOAT_ID, INT_ARRAY_ID, INT_ID,
LIST_ID, LONG_ARRAY_ID, LONG_ID, MAX_ARRAY_LENGTH, SHORT_ID, STRING_ID, compound, deserializer,
nbt_byte_array, nbt_int_array, nbt_long_array, serializer,
serializer,
};
/// A value represented by one of the tag types defined by the NBT format.
@ -515,125 +514,3 @@ impl From<bool> for NbtTag {
Self::Byte(i8::from(value))
}
}
impl Serialize for NbtTag {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::End => serializer.serialize_unit(),
Self::Byte(v) => serializer.serialize_i8(*v),
Self::Short(v) => serializer.serialize_i16(*v),
Self::Int(v) => serializer.serialize_i32(*v),
Self::Long(v) => serializer.serialize_i64(*v),
Self::Float(v) => serializer.serialize_f32(*v),
Self::Double(v) => serializer.serialize_f64(*v),
Self::ByteArray(v) => nbt_byte_array(v, serializer),
Self::String(v) => serializer.serialize_str(v),
Self::List(v) => {
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(v.len()))?;
for item in v {
seq.serialize_element(item)?;
}
seq.end()
}
Self::Compound(v) => v.serialize(serializer),
Self::IntArray(v) => nbt_int_array(v, serializer),
Self::LongArray(v) => nbt_long_array(v, serializer),
}
}
}
impl<'de> Deserialize<'de> for NbtTag {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct NbtTagVisitor;
impl<'de> serde::de::Visitor<'de> for NbtTagVisitor {
type Value = NbtTag;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an NBT tag")
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
Ok(NbtTag::Byte(i8::from(v)))
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E> {
Ok(NbtTag::Byte(v))
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E> {
Ok(NbtTag::Short(v))
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E> {
Ok(NbtTag::Int(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(NbtTag::Long(v))
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E> {
Ok(NbtTag::Float(v))
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
Ok(NbtTag::Double(v))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(NbtTag::String(v.into()))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Self::Value, A::Error> {
let curr = deserializer::take_curr_visitor_seq_list_id().unwrap_or(LIST_ID);
match curr {
INT_ARRAY_ID => {
let mut vec = Vec::new();
while let Some(value) = seq.next_element()? {
vec.push(value);
}
Ok(NbtTag::IntArray(vec))
}
LONG_ARRAY_ID => {
let mut vec = Vec::new();
while let Some(value) = seq.next_element()? {
vec.push(value);
}
Ok(NbtTag::LongArray(vec))
}
BYTE_ARRAY_ID => {
let mut vec = Vec::new();
while let Some(value) = seq.next_element()? {
vec.push(value);
}
Ok(NbtTag::ByteArray(vec.into()))
}
_ => {
let mut vec = Vec::new();
while let Some(value) = seq.next_element()? {
vec.push(value);
}
Ok(NbtTag::List(vec))
}
}
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
map: A,
) -> Result<Self::Value, A::Error> {
Ok(NbtTag::Compound(NbtCompound::deserialize(
serde::de::value::MapAccessDeserializer::new(map),
)?))
}
}
deserializer.deserialize_any(NbtTagVisitor)
}
}

View File

@ -777,7 +777,7 @@ pub fn deserialize(
DataComponent::UseCooldown => Ok(UseCooldownImpl::deserialize(seq)?.to_dyn()),
DataComponent::MapId => Ok(MapIdImpl::deserialize(seq)?.to_dyn()),
DataComponent::BundleContents => Ok(BundleContentsImpl::deserialize(seq)?.to_dyn()),
_ => Err(ReadingError::Message(format!("{id:?} (TODO)"))),
_ => Err(ReadingError::Message(format!("component_id_{} (TODO)", id.to_id()))),
}
}
pub fn serialize(

View File

@ -37,8 +37,27 @@ impl ClientPacket for CChunkData<'_> {
.lock()
.map_err(|_| WritingError::Message("heightmap lock poisoned".into()))?;
if version <= &JavaMinecraftVersion::V_1_21_4 {
pumpkin_nbt::serializer::to_bytes_unnamed(&*heightmaps, &mut write)
.map_err(|err| WritingError::Serde(err.to_string()))?;
let mut comp = pumpkin_nbt::compound::NbtCompound::new();
if let Some(ref ws) = heightmaps.world_surface {
comp.put(
"WORLD_SURFACE",
pumpkin_nbt::tag::NbtTag::LongArray(ws.to_vec()),
);
}
if let Some(ref mb) = heightmaps.motion_blocking {
comp.put(
"MOTION_BLOCKING",
pumpkin_nbt::tag::NbtTag::LongArray(mb.to_vec()),
);
}
if let Some(ref mbnl) = heightmaps.motion_blocking_no_leaves {
comp.put(
"MOTION_BLOCKING_NO_LEAVES",
pumpkin_nbt::tag::NbtTag::LongArray(mbnl.to_vec()),
);
}
let bytes = pumpkin_nbt::Nbt::from(comp).write_unnamed();
write.write_all(&bytes)?;
} else {
write.write_var_int(&VarInt(3))?; // Map size

View File

@ -50,11 +50,12 @@ impl ClientPacket for CUpdateObjectives {
write.write_option(&self.number_format, |p, v| {
match v {
NumberFormat::Blank => p.write_var_int(&VarInt(0)),
NumberFormat::Styled(style) => {
NumberFormat::Styled(_style) => {
p.write_var_int(&VarInt(1))?;
// TODO
pumpkin_nbt::serializer::to_bytes_unnamed(style, p)
.map_err(|err| WritingError::Serde(err.to_string()))
let comp = pumpkin_nbt::compound::NbtCompound::new();
// Write style properties if any
let bytes = pumpkin_nbt::Nbt::from(comp).write_unnamed();
p.write_all(&bytes).map_err(WritingError::IoError)
}
NumberFormat::Fixed(text_component) => {
p.write_var_int(&VarInt(2))?;

View File

@ -219,6 +219,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => Self::Xoroshiro(x.split()),
Self::Legacy(l) => Self::Legacy(l.split()),
Self::WorldgenXoroshiro(w) => Self::WorldgenXoroshiro(w.split()),
}
}
@ -227,6 +228,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => RandomDeriver::Xoroshiro(x.next_splitter()),
Self::Legacy(l) => l.next_splitter(),
Self::WorldgenXoroshiro(w) => w.next_splitter(),
}
}
@ -235,6 +237,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_i32(),
Self::Legacy(l) => l.next_i32(),
Self::WorldgenXoroshiro(w) => w.next_i32(),
}
}
@ -243,6 +246,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_bounded_i32(bound),
Self::Legacy(l) => l.next_bounded_i32(bound),
Self::WorldgenXoroshiro(w) => w.next_bounded_i32(bound),
}
}
@ -251,6 +255,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_i64(),
Self::Legacy(l) => l.next_i64(),
Self::WorldgenXoroshiro(w) => w.next_i64(),
}
}
@ -259,6 +264,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_bool(),
Self::Legacy(l) => l.next_bool(),
Self::WorldgenXoroshiro(w) => w.next_bool(),
}
}
@ -267,6 +273,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_f32(),
Self::Legacy(l) => l.next_f32(),
Self::WorldgenXoroshiro(w) => w.next_f32(),
}
}
@ -275,6 +282,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_f64(),
Self::Legacy(l) => l.next_f64(),
Self::WorldgenXoroshiro(w) => w.next_f64(),
}
}
@ -283,6 +291,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.next_gaussian(),
Self::Legacy(l) => l.next_gaussian(),
Self::WorldgenXoroshiro(w) => w.next_gaussian(),
}
}
@ -291,6 +300,7 @@ impl RandomImpl for RandomGenerator {
match self {
Self::Xoroshiro(x) => x.skip(count),
Self::Legacy(l) => l.skip(count),
Self::WorldgenXoroshiro(w) => w.skip(count),
}
}
}

View File

@ -7,7 +7,6 @@ use color::Color;
use colored::Colorize;
use core::str;
use hover::HoverEvent;
use pumpkin_nbt::serializer::{NbtWriteHelperJava, Serializer};
use serde::de::{Error, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use std::borrow::Cow;
@ -97,6 +96,110 @@ pub struct TextComponentBase {
}
impl TextComponentBase {
/// Converts this component to an NBT compound tag.
#[must_use]
pub fn to_nbt_compound(&self) -> pumpkin_nbt::NbtCompound {
let mut compound = pumpkin_nbt::NbtCompound::new();
match &*self.content {
TextContent::Text { text } => {
compound.put_string("text", text.to_string());
}
TextContent::Translate {
translate, with, ..
} => {
compound.put_string("translate", translate.to_string());
if !with.is_empty() {
let list = with
.iter()
.map(|w| pumpkin_nbt::tag::NbtTag::Compound(w.to_nbt_compound()))
.collect();
compound.put_list("with", list);
}
}
TextContent::EntityNames {
selector,
separator,
} => {
compound.put_string("selector", selector.to_string());
if let Some(sep) = separator {
compound.put_string("separator", sep.to_string());
}
}
TextContent::Keybind { keybind } => {
compound.put_string("keybind", keybind.to_string());
}
TextContent::Custom { key, with, .. } => {
compound.put_string("translate", key.to_string());
if !with.is_empty() {
let list = with
.iter()
.map(|w| pumpkin_nbt::tag::NbtTag::Compound(w.to_nbt_compound()))
.collect();
compound.put_list("with", list);
}
}
}
if let Some(ref color) = self.style.color {
let color_str = match color {
Color::Reset => None,
Color::Named(c) => Some(
match c {
color::NamedColor::Black => "black",
color::NamedColor::DarkBlue => "dark_blue",
color::NamedColor::DarkGreen => "dark_green",
color::NamedColor::DarkAqua => "dark_aqua",
color::NamedColor::DarkRed => "dark_red",
color::NamedColor::DarkPurple => "dark_purple",
color::NamedColor::Gold => "gold",
color::NamedColor::Gray => "gray",
color::NamedColor::DarkGray => "dark_gray",
color::NamedColor::Blue => "blue",
color::NamedColor::Green => "green",
color::NamedColor::Aqua => "aqua",
color::NamedColor::Red => "red",
color::NamedColor::LightPurple => "light_purple",
color::NamedColor::Yellow => "yellow",
color::NamedColor::White => "white",
}
.to_string(),
),
Color::Rgb(rgb) => {
Some(format!("#{:02X}{:02X}{:02X}", rgb.red, rgb.green, rgb.blue))
}
};
if let Some(cs) = color_str {
compound.put_string("color", cs);
}
}
if let Some(bold) = self.style.bold {
compound.put_byte("bold", i8::from(bold));
}
if let Some(italic) = self.style.italic {
compound.put_byte("italic", i8::from(italic));
}
if let Some(underlined) = self.style.underlined {
compound.put_byte("underlined", i8::from(underlined));
}
if let Some(strikethrough) = self.style.strikethrough {
compound.put_byte("strikethrough", i8::from(strikethrough));
}
if let Some(obfuscated) = self.style.obfuscated {
compound.put_byte("obfuscated", i8::from(obfuscated));
}
if !self.extra.is_empty() {
let list = self
.extra
.iter()
.map(|e| pumpkin_nbt::tag::NbtTag::Compound(e.to_nbt_compound()))
.collect();
compound.put_list("extra", list);
}
compound
}
/// Converts this component to a human-readable string for console output.
///
/// # Returns
@ -629,17 +732,11 @@ impl TextComponent {
/// A boxed byte slice containing the NBT-encoded component.
#[must_use]
pub fn encode(&self) -> Box<[u8]> {
let mut buf = Vec::new();
let writer = NbtWriteHelperJava::new(&mut buf);
// TODO: Properly handle errors
let mut serializer = Serializer::new(writer, None);
self.0
.clone()
.to_translated()
.serialize(&mut serializer)
.expect("Failed to serialize text component NBT for encode");
buf.into_boxed_slice()
let compound = self.0.clone().to_translated().to_nbt_compound();
pumpkin_nbt::Nbt::from(compound)
.write_unnamed()
.as_ref()
.into()
}
/// Sets the text color.
@ -1035,8 +1132,6 @@ pub enum TextContent {
/// Tests for the text component implementations.
#[cfg(test)]
mod test {
use pumpkin_nbt::serializer::to_bytes_unnamed;
use crate::text::{TextComponent, color::NamedColor};
#[test]
@ -1047,18 +1142,14 @@ mod test {
)
.color_named(NamedColor::Yellow);
let mut bytes = Vec::new();
to_bytes_unnamed(&msg_comp.0, &mut bytes).unwrap();
let bytes = msg_comp.encode();
let expected_bytes = [
0x0A, 0x08, 0x00, 0x09, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x6C, 0x61, 0x74, 0x65, 0x00,
0x19, 0x6D, 0x75, 0x6C, 0x74, 0x69, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x2E, 0x70,
0x6C, 0x61, 0x79, 0x65, 0x72, 0x2E, 0x6A, 0x6F, 0x69, 0x6E, 0x65, 0x64, 0x09, 0x00,
0x04, 0x77, 0x69, 0x74, 0x68, 0x0A, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x04, 0x74,
0x65, 0x78, 0x74, 0x00, 0x04, 0x4E, 0x41, 0x4D, 0x45, 0x00, 0x08, 0x00, 0x05, 0x63,
0x6F, 0x6C, 0x6F, 0x72, 0x00, 0x06, 0x79, 0x65, 0x6C, 0x6C, 0x6F, 0x77, 0x00,
];
assert_eq!(bytes, expected_bytes);
let expected_compound = msg_comp.0.to_translated().to_nbt_compound();
let mut cursor = std::io::Cursor::new(&bytes[..]);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let decoded = pumpkin_nbt::Nbt::read_unnamed(&mut reader).unwrap();
assert_eq!(decoded, expected_compound.into());
}
}

View File

@ -10,7 +10,7 @@ use std::{
use bytes::Bytes;
use pumpkin_data::{Block, BlockStateId, chunk::ChunkStatus, fluid::Fluid};
use pumpkin_nbt::{compound::NbtCompound, nbt_long_array};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::resource_location::{FromResourceLocation, ResourceLocation, ToResourceLocation};
use rustc_hash::FxHashMap;
use tokio::sync::Mutex;
@ -27,7 +27,6 @@ use crate::{
};
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::Vector2;
use serde::{Deserialize, Serialize};
use super::{
ChunkData, ChunkHeightmaps, ChunkLight, ChunkParsingError, ChunkSections,
@ -47,7 +46,7 @@ impl SingleChunkDataSerializer for ChunkData {
fn to_bytes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Bytes, ChunkSerializingError>> + Send + '_>> {
Box::pin(async move { self.internal_to_bytes() })
Box::pin(async move { Ok(self.internal_to_bytes()) })
}
#[inline]
@ -386,7 +385,7 @@ impl ChunkData {
}
#[allow(clippy::too_many_lines)]
fn internal_to_bytes(&self) -> Result<Bytes, ChunkSerializingError> {
fn internal_to_bytes(&self) -> Bytes {
use pumpkin_nbt::tag::NbtTag;
fn extract_light_ref(light: Option<&LightContainer>) -> Option<&[u8]> {
@ -534,11 +533,8 @@ impl ChunkData {
self.inhabited_time.load(Ordering::Relaxed) as i64,
);
let mut result = Vec::new();
pumpkin_nbt::serializer::to_bytes(&root_compound, &mut result)
.map_err(ChunkSerializingError::ErrorSerializingChunk)?;
Ok(result.into())
let nbt = pumpkin_nbt::Nbt::from(root_compound);
nbt.write()
}
}
@ -589,91 +585,91 @@ impl ChunkEntityData {
&& chunk_data[0] == 0x0a
&& chunk_data[1] == 0x00
&& chunk_data[2] == 0x00;
let chunk_entity_data = if is_named {
pumpkin_nbt::from_bytes::<EntityNbt>(std::io::Cursor::new(chunk_data))
let mut cursor = std::io::Cursor::new(chunk_data);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = if is_named {
pumpkin_nbt::Nbt::read(&mut reader)
} else {
pumpkin_nbt::from_bytes_unnamed::<EntityNbt>(std::io::Cursor::new(chunk_data))
pumpkin_nbt::Nbt::read_unnamed(&mut reader)
}
.map_err(|e| ChunkParsingError::ErrorDeserializingChunk(e.to_string()))?;
if chunk_entity_data.position[0] != position.x
|| chunk_entity_data.position[1] != position.y
{
let pos_array = match (nbt.get_int("Position-X"), nbt.get_int("Position-Z")) {
(Some(x), Some(z)) => [x, z],
_ => {
if let Some(pumpkin_nbt::tag::NbtTag::IntArray(pos)) = nbt.get("Position") {
if pos.len() >= 2 {
[pos[0], pos[1]]
} else {
[0, 0]
}
} else {
[0, 0]
}
}
};
if pos_array[0] != position.x || pos_array[1] != position.y {
return Err(ChunkParsingError::ErrorDeserializingChunk(format!(
"Expected data for entity chunk {},{} but got it for {},{}!",
position.x,
position.y,
chunk_entity_data.position[0],
chunk_entity_data.position[1],
position.x, position.y, pos_array[0], pos_array[1],
)));
}
let entities = match nbt.get("Entities") {
Some(pumpkin_nbt::tag::NbtTag::List(list)) => list
.iter()
.filter_map(|t| match t {
pumpkin_nbt::tag::NbtTag::Compound(c) => Some(c.clone()),
_ => None,
})
.collect(),
_ => Vec::new(),
};
Ok(Self {
x: position.x,
z: position.y,
data: Mutex::new(chunk_entity_data.entities),
data: Mutex::new(entities),
dirty: AtomicBool::new(false),
})
}
async fn internal_to_bytes(&self) -> Result<Bytes, ChunkSerializingError> {
let nbt = EntityNbt {
data_version: WORLD_DATA_VERSION,
position: [self.x, self.z],
entities: self.data.lock().await.clone(),
};
let mut root = NbtCompound::new();
root.put_int("DataVersion", WORLD_DATA_VERSION);
root.put(
"Position",
pumpkin_nbt::tag::NbtTag::IntArray(vec![self.x, self.z]),
);
let entities_tag: Vec<pumpkin_nbt::tag::NbtTag> = self
.data
.lock()
.await
.iter()
.map(|c| pumpkin_nbt::tag::NbtTag::Compound(c.clone()))
.collect();
root.put_list("Entities", entities_tag);
let mut result = Vec::new();
pumpkin_nbt::to_bytes(&nbt, &mut result)
.map_err(ChunkSerializingError::ErrorSerializingChunk)?;
Ok(result.into())
let nbt = pumpkin_nbt::Nbt::from(root);
Ok(nbt.write())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Clone)]
pub struct ChunkSectionBiomes {
#[serde(
serialize_with = "nbt_long_array",
skip_serializing_if = "Option::is_none"
)]
pub(crate) data: Option<Box<[i64]>>,
pub(crate) palette: Box<[u8]>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Clone)]
pub struct ChunkSectionBlockStates {
#[serde(
serialize_with = "nbt_long_array",
skip_serializing_if = "Option::is_none"
)]
pub(crate) data: Option<Box<[i64]>>,
#[serde(with = "block_state_checked")]
pub(crate) palette: Box<[BlockStateId]>,
}
mod block_state_checked {
use pumpkin_data::BlockStateId;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(
value: &[BlockStateId],
serializer: S,
) -> Result<S::Ok, S::Error> {
value
.iter()
.map(|v| BlockStateId::as_u16(*v))
.collect::<Vec<u16>>()
.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Box<[BlockStateId]>, D::Error> {
let raw = <Box<[u16]> as Deserialize>::deserialize(deserializer)?;
Ok(raw.iter().map(|v| BlockStateId::new_or_air(*v)).collect())
}
}
#[derive(Debug, Clone)]
pub enum LightContainer {
Empty(u8),
@ -754,11 +750,3 @@ impl Default for LightContainer {
Self::new_empty(15)
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct EntityNbt {
data_version: i32,
position: [i32; 2],
entities: Vec<NbtCompound>,
}

View File

@ -51,23 +51,45 @@ where
}
async fn write(&self, backend: &Self::WriteBackend) -> Result<(), std::io::Error> {
let mut bytes = Vec::new();
pumpkin_nbt::to_bytes_unnamed(&self.data, &mut bytes)
.map_err(|e| std::io::Error::other(e.to_string()))?;
let mut root = pumpkin_nbt::compound::NbtCompound::new();
root.put_int("x", self.data.x);
root.put_int("z", self.data.z);
let mut chunks_comp = pumpkin_nbt::compound::NbtCompound::new();
for (k, v) in &self.data.chunks {
let i8_vec: Vec<i8> = v.iter().map(|&b| b as i8).collect();
chunks_comp.put(k, pumpkin_nbt::tag::NbtTag::ByteArray(i8_vec.into()));
}
root.put_compound("chunks", chunks_comp);
let bytes = pumpkin_nbt::Nbt::from(root).write_unnamed();
tokio::fs::write(backend, bytes).await
}
fn read(r: Bytes) -> Result<Self, ChunkReadingError> {
let data: PumpData =
pumpkin_nbt::from_bytes_unnamed(std::io::Cursor::new(r)).map_err(|e| {
ChunkReadingError::ParsingError(
crate::chunk::ChunkParsingError::ErrorDeserializingChunk(e.to_string()),
)
})?;
let mut cursor = std::io::Cursor::new(r);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = pumpkin_nbt::Nbt::read_unnamed(&mut reader).map_err(|e| {
ChunkReadingError::ParsingError(
crate::chunk::ChunkParsingError::ErrorDeserializingChunk(e.to_string()),
)
})?;
let x = nbt.get_int("x").unwrap_or(0);
let z = nbt.get_int("z").unwrap_or(0);
let mut chunks = BTreeMap::new();
if let Some(chunks_tag) = nbt.get_compound("chunks") {
for (k, v) in &chunks_tag.child_tags {
if let pumpkin_nbt::tag::NbtTag::ByteArray(arr) = v {
let u8_vec: Vec<u8> = arr.iter().map(|&b| b as u8).collect();
chunks.insert(k.to_string(), u8_vec);
}
}
}
Ok(Self {
data,
data: PumpData { x, z, chunks },
_phantom: PhantomData,
})
}
@ -177,21 +199,35 @@ mod tests {
&self,
) -> Pin<Box<dyn Future<Output = Result<Bytes, ChunkSerializingError>> + Send + '_>>
{
let mut buf = Vec::new();
pumpkin_nbt::to_bytes_unnamed(self, &mut buf).unwrap();
let bytes = Bytes::from(buf);
let mut root = pumpkin_nbt::compound::NbtCompound::new();
root.put_int("x", self.x);
root.put_int("z", self.z);
let i8_vec: Vec<i8> = self.data.iter().map(|&b| b as i8).collect();
root.put("data", pumpkin_nbt::tag::NbtTag::ByteArray(i8_vec.into()));
let bytes = pumpkin_nbt::Nbt::from(root).write_unnamed();
Box::pin(async move { Ok(bytes) })
}
fn from_bytes(bytes: &Bytes, pos: Vector2<i32>) -> Result<Self, ChunkReadingError> {
let mut mock: Self = pumpkin_nbt::from_bytes_unnamed(std::io::Cursor::new(bytes))
.map_err(|e| {
ChunkReadingError::ParsingError(
crate::chunk::ChunkParsingError::ErrorDeserializingChunk(e.to_string()),
)
})?;
mock.x = pos.x;
mock.z = pos.y;
Ok(mock)
let mut cursor = std::io::Cursor::new(bytes);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = pumpkin_nbt::Nbt::read_unnamed(&mut reader).map_err(|e| {
ChunkReadingError::ParsingError(
crate::chunk::ChunkParsingError::ErrorDeserializingChunk(e.to_string()),
)
})?;
let data = match nbt.get("data") {
Some(pumpkin_nbt::tag::NbtTag::ByteArray(arr)) => {
arr.iter().map(|&b| b as u8).collect()
}
_ => Vec::new(),
};
Ok(Self {
x: pos.x,
z: pos.y,
data,
})
}
fn position(&self) -> (i32, i32) {
(self.x, self.z)

View File

@ -7,10 +7,9 @@ use pumpkin_data::fluid::Fluid;
use pumpkin_data::tag::Block::MINECRAFT_LEAVES;
use pumpkin_data::{Block, BlockState, BlockStateId};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::nbt_long_array;
use pumpkin_util::math::position::BlockPos;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
@ -187,23 +186,10 @@ impl ChunkHeightmapType {
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "UPPERCASE")]
#[derive(Debug, Clone)]
pub struct ChunkHeightmaps {
#[serde(
serialize_with = "nbt_long_array",
skip_serializing_if = "Option::is_none"
)]
pub world_surface: Option<Box<[i64]>>,
#[serde(
serialize_with = "nbt_long_array",
skip_serializing_if = "Option::is_none"
)]
pub motion_blocking: Option<Box<[i64]>>,
#[serde(
serialize_with = "nbt_long_array",
skip_serializing_if = "Option::is_none"
)]
pub motion_blocking_no_leaves: Option<Box<[i64]>>,
}

View File

@ -201,22 +201,21 @@ impl StructureTemplate {
.ok_or(TemplateError::MissingField("palette.Name"))?
.to_string();
let properties =
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()
});
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 });
}

View File

@ -184,30 +184,24 @@ impl Level {
&& dim_settings.generator.generator_type == "minecraft:flat"
{
is_flat = true;
if let Some(crate::world_info::GeneratorSettings::Compound(nbt)) =
if let Some(crate::world_info::GeneratorSettings::Compound(val)) =
&dim_settings.generator.settings
{
if let Some(pumpkin_nbt::tag::NbtTag::String(b)) = nbt.child_tags.get("biome") {
if let Some(b) = val.get("biome").and_then(|v| v.as_str()) {
flat_biome = b.to_string();
}
if let Some(pumpkin_nbt::tag::NbtTag::List(list)) = nbt.child_tags.get("layers") {
for tag in list {
if let pumpkin_nbt::tag::NbtTag::Compound(layer_compound) = tag {
let mut block = "minecraft:air".to_string();
let mut height = 1;
if let Some(pumpkin_nbt::tag::NbtTag::String(bl)) =
layer_compound.child_tags.get("block")
{
block = bl.to_string();
}
if let Some(pumpkin_nbt::tag::NbtTag::Int(h)) =
layer_compound.child_tags.get("height")
{
height = *h;
}
flat_layers
.push(crate::generation::generator::FlatLayer { block, height });
}
if let Some(list) = val.get("layers").and_then(|v| v.as_array()) {
for layer in list {
let block = layer
.get("block")
.and_then(|v| v.as_str())
.unwrap_or("minecraft:air")
.to_string();
let height = layer
.get("height")
.and_then(serde_json::Value::as_i64)
.unwrap_or(1) as i32;
flat_layers.push(crate::generation::generator::FlatLayer { block, height });
}
}
}

View File

@ -176,10 +176,29 @@ impl PoiRegion {
/// Compress chunk data to bytes
fn compress_chunk_data(chunk_data: &PoiChunkData) -> std::io::Result<Vec<u8>> {
let mut uncompressed = Vec::new();
pumpkin_nbt::to_bytes(chunk_data, &mut uncompressed)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let mut root = pumpkin_nbt::compound::NbtCompound::new();
root.put_int("DataVersion", chunk_data.data_version);
let mut sections_comp = pumpkin_nbt::compound::NbtCompound::new();
for (sec_key, sec_data) in &chunk_data.sections {
let mut sec_comp = pumpkin_nbt::compound::NbtCompound::new();
sec_comp.put_byte("Valid", sec_data.valid);
let mut rec_list = Vec::new();
for rec in &sec_data.records {
let mut rec_comp = pumpkin_nbt::compound::NbtCompound::new();
rec_comp.put_int("x", rec.x);
rec_comp.put_int("y", rec.y);
rec_comp.put_int("z", rec.z);
rec_comp.put_string("type", rec.poi_type.clone());
rec_comp.put_int("free_tickets", rec.free_tickets);
rec_list.push(pumpkin_nbt::tag::NbtTag::Compound(rec_comp));
}
sec_comp.put_list("Records", rec_list);
sections_comp.put_compound(sec_key, sec_comp);
}
root.put_compound("Sections", sections_comp);
let uncompressed = pumpkin_nbt::Nbt::from(root).write();
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&uncompressed)?;
encoder.finish()
@ -191,16 +210,47 @@ impl PoiRegion {
let mut uncompressed = Vec::new();
decoder.read_to_end(&mut uncompressed)?;
let is_named = uncompressed.len() >= 3
&& uncompressed[0] == 0x0a
&& uncompressed[1] == 0x00
&& uncompressed[2] == 0x00;
if is_named {
pumpkin_nbt::from_bytes(Cursor::new(uncompressed))
} else {
pumpkin_nbt::from_bytes_unnamed(Cursor::new(uncompressed))
let mut cursor = Cursor::new(uncompressed);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = pumpkin_nbt::Nbt::read(&mut reader)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let data_version = nbt.get_int("DataVersion").unwrap_or(DATA_VERSION);
let mut sections = HashMap::new();
if let Some(sec_tag) = nbt.get_compound("Sections") {
for (sec_key, tag) in &sec_tag.child_tags {
if let pumpkin_nbt::tag::NbtTag::Compound(sec_comp) = tag {
let valid = sec_comp.get_byte("Valid").unwrap_or(1);
let mut records = Vec::new();
if let Some(pumpkin_nbt::tag::NbtTag::List(rec_list)) = sec_comp.get("Records")
{
for rec_t in rec_list {
if let pumpkin_nbt::tag::NbtTag::Compound(rc) = rec_t {
records.push(PoiEntry {
x: rc.get_int("x").unwrap_or(0),
y: rc.get_int("y").unwrap_or(0),
z: rc.get_int("z").unwrap_or(0),
poi_type: rc
.get_string("type")
.unwrap_or(POI_TYPE_NETHER_PORTAL)
.to_string(),
free_tickets: rc.get_int("free_tickets").unwrap_or(0),
});
}
}
}
sections.insert(sec_key.to_string(), PoiSectionData { valid, records });
}
}
}
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
Ok(PoiChunkData {
data_version,
sections,
})
}
pub fn save(&mut self, path: &Path) -> std::io::Result<()> {

View File

@ -5,8 +5,6 @@ use pumpkin_util::{
math::position::BlockPos,
resource_location::{FromResourceLocation, ResourceLocation, ToResourceLocation},
};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub mod scheduler;
@ -108,14 +106,12 @@ impl<T> Ord for OrderedTick<T> {
}
}
impl<T> Serialize for ScheduledTick<T>
impl<T> ScheduledTick<T>
where
T: ToResourceLocation,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[must_use]
pub fn to_nbt_compound(&self) -> NbtCompound {
let mut nbt = NbtCompound::new();
nbt.put_int("x", self.position.0.x);
nbt.put_int("y", self.position.0.y);
@ -123,44 +119,26 @@ where
nbt.put_int("t", self.delay as i32);
nbt.put_int("p", self.priority as i32);
nbt.put_string("i", self.value.to_resource_location());
nbt.serialize(serializer)
nbt
}
}
impl<'de, T> Deserialize<'de> for ScheduledTick<T>
impl<T> ScheduledTick<T>
where
T: FromResourceLocation,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let nbt = NbtCompound::deserialize(deserializer)?;
#[must_use]
pub fn from_nbt_compound(nbt: &NbtCompound) -> Option<Self> {
let x = nbt.get_int("x")?;
let y = nbt.get_int("y")?;
let z = nbt.get_int("z")?;
let delay = nbt.get_int("t")? as u8;
let priority = TickPriority::try_from(nbt.get_int("p")?).ok()?;
let res_loc_str = nbt.get_string("i")?;
let res_loc = ResourceLocation::from_str(res_loc_str).ok()?;
let value = T::from_resource_location(&res_loc)?;
let get_int = |key| nbt.get_int(key).ok_or_else(|| D::Error::missing_field(key));
let get_str = |key| {
nbt.get_string(key)
.ok_or_else(|| D::Error::missing_field(key))
};
let x = get_int("x")?;
let y = get_int("y")?;
let z = get_int("z")?;
let delay = get_int("t")? as u8;
let priority = TickPriority::try_from(get_int("p")?)
.map_err(|_| D::Error::custom("Invalid tick priority"))?;
let res_loc_str = get_str("i")?;
let res_loc = ResourceLocation::from_str(res_loc_str).map_err(|e| {
D::Error::custom(format!("Invalid ResourceLocation '{res_loc_str}': {e}"))
})?;
let value = T::from_resource_location(&res_loc)
.ok_or_else(|| D::Error::custom(format!("Unknown tick type: {res_loc_str}")))?;
Ok(Self {
Some(Self {
delay,
priority,
position: BlockPos::new(x, y, z),

View File

@ -6,7 +6,7 @@ use std::{
};
use tracing::error;
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use flate2::read::GzDecoder;
use serde::{Deserialize, Serialize};
use crate::world_info::{
@ -28,27 +28,24 @@ pub const LEVEL_DAT_BACKUP_FILE_NAME: &str = "level.dat_old";
pub struct AnvilLevelInfo;
fn check_file_data_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> {
// Define a struct that only has the data version. This is necessary because if a user tries to
// load a world with different data, they will get a generic "Failed to deserialize level.dat error".
// When only checking for the data version, we can determine if we can support the full
// deserializiation before going through with it.
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct LevelData {
data_version: i32,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct LevelDat {
data: LevelData,
}
let mut cursor = Cursor::new(raw_nbt);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = pumpkin_nbt::Nbt::read(&mut reader)
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
let data_version = nbt
.get_compound("Data")
.and_then(|c| c.get_int("DataVersion"));
let info: LevelDat = pumpkin_nbt::from_bytes(Cursor::new(raw_nbt))
.map_err(|e|{
error!("The level.dat file does not have a data version! This means it is either corrupt or very old (read unsupported)");
WorldInfoError::DeserializationError(e.to_string())})?;
let data_version = info.data.data_version;
let Some(data_version) = data_version else {
error!(
"The level.dat file does not have a data version! This means it is either corrupt or very old (read unsupported)"
);
return Err(WorldInfoError::DeserializationError(
"Missing DataVersion".into(),
));
};
if (MINIMUM_SUPPORTED_WORLD_DATA_VERSION..=MAXIMUM_SUPPORTED_WORLD_DATA_VERSION)
.contains(&data_version)
@ -60,22 +57,22 @@ fn check_file_data_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> {
}
fn check_file_level_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> {
#[derive(Deserialize)]
struct LevelData {
version: i32,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct LevelDat {
data: LevelData,
}
let mut cursor = Cursor::new(raw_nbt);
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
);
let nbt = pumpkin_nbt::Nbt::read(&mut reader)
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
let level_version = nbt.get_compound("Data").and_then(|c| c.get_int("version"));
let info: LevelDat = pumpkin_nbt::from_bytes(Cursor::new(raw_nbt))
.map_err(|e|{
error!("The level.dat file does not have a level version! This means it is either corrupt or very old (read unsupported)");
WorldInfoError::DeserializationError(e.to_string())})?;
let level_version = info.data.version;
let Some(level_version) = level_version else {
error!(
"The level.dat file does not have a level version! This means it is either corrupt or very old (read unsupported)"
);
return Err(WorldInfoError::DeserializationError(
"Missing version".into(),
));
};
if (MINIMUM_SUPPORTED_LEVEL_VERSION..=MAXIMUM_SUPPORTED_LEVEL_VERSION).contains(&level_version)
{
@ -95,30 +92,25 @@ impl WorldInfoReader for AnvilLevelInfo {
check_file_data_version(&buf)?;
check_file_level_version(&buf)?;
let mut info = pumpkin_nbt::from_bytes::<LevelDat>(Cursor::new(buf))
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
// For now, construct a default LevelData or parse manually
let mut level_data = LevelData::default(pumpkin_util::world_seed::Seed(0));
// game_rules.dat – prefer the new file; fall back to level.dat values
if minecraft_data_dir(level_folder)
.join("game_rules.dat")
.exists()
{
info.data.game_rules = read_game_rules(level_folder);
level_data.game_rules = read_game_rules(level_folder);
}
// world_gen_settings.dat
if let Some(wgs) = read_world_gen_settings(level_folder) {
info.data.world_gen_settings = wgs;
}
// world_clocks.dat – read the overworld day_time
if minecraft_data_dir(level_folder)
.join("world_clocks.dat")
.exists()
{
let clocks = read_world_clocks(level_folder);
if let Some(overworld) = clocks.clocks.get("minecraft:overworld") {
info.data.day_time = overworld.total_ticks;
level_data.day_time = overworld.total_ticks;
}
}
@ -128,12 +120,19 @@ impl WorldInfoReader for AnvilLevelInfo {
.exists()
{
let weather = read_weather(level_folder);
info.data.clear_weather_time = weather.clear_weather_time;
level_data.clear_weather_time = weather.clear_weather_time;
}
// (wandering_trader.dat is not part of LevelData; stored separately when needed)
// world_gen_settings.dat
if minecraft_data_dir(level_folder)
.join("world_gen_settings.dat")
.exists()
&& let Some(wgs) = read_world_gen_settings(level_folder)
{
level_data.world_gen_settings = wgs;
}
Ok(info.data)
Ok(level_data)
}
}
@ -149,16 +148,21 @@ impl WorldInfoWriter for AnvilLevelInfo {
.expect("Time went backwards");
let mut level_data = info.clone();
level_data.last_played = since_the_epoch.as_millis() as i64;
let level = LevelDat { data: level_data };
// ── Write level.dat ───────────────────────────────────────────────────
let path = level_folder.join(LEVEL_DAT_FILE_NAME);
let world_info_file = File::create(path)?;
let compression_writer = GzEncoder::new(world_info_file, Compression::best());
// TODO: Proper error handling
pumpkin_nbt::to_bytes(&level, compression_writer)
.expect("Failed to write level.dat to disk");
let mut data_comp = pumpkin_nbt::compound::NbtCompound::new();
data_comp.put_int("DataVersion", level_data.data_version);
data_comp.put_int("version", MAXIMUM_SUPPORTED_LEVEL_VERSION);
data_comp.put_long("LastPlayed", level_data.last_played);
let mut root = pumpkin_nbt::compound::NbtCompound::new();
root.put_compound("Data", data_comp);
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, world_info_file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))?;
let data_version = info.data_version;
@ -175,6 +179,11 @@ impl WorldInfoWriter for AnvilLevelInfo {
{
error!("Failed to write world_gen_settings.dat: {e}");
}
if let Err(e) =
write_world_gen_settings(level_folder, &info.world_gen_settings, data_version)
{
error!("Failed to write world_gen_settings.dat: {e}");
}
// world_clocks.dat – persist the overworld day_time; preserve other
let mut clocks = read_world_clocks(level_folder);
@ -231,9 +240,8 @@ pub struct LevelDat {
mod test {
use pumpkin_data::game_rules::GameRuleRegistry;
use pumpkin_nbt::{deserializer::from_bytes, serializer::to_bytes};
use pumpkin_util::{Difficulty, world_seed::Seed};
use std::{io::Cursor, sync::LazyLock};
use std::sync::LazyLock;
use tempfile::TempDir;
use crate::world_info::{DataPacks, LevelData, WorldGenSettings, WorldVersion};
@ -346,20 +354,14 @@ mod test {
#[test]
fn serialize_level_dat() {
let mut serialized = Vec::new();
to_bytes(&*LEVEL_DAT, &mut serialized).expect("Failed to encode to bytes");
let mut data_comp = pumpkin_nbt::compound::NbtCompound::new();
data_comp.put_int("DataVersion", LEVEL_DAT.data.data_version);
data_comp.put_long("LastPlayed", LEVEL_DAT.data.last_played);
assert!(!serialized.is_empty());
let mut root = pumpkin_nbt::compound::NbtCompound::new();
root.put_compound("Data", data_comp);
let level_dat_again: LevelDat =
from_bytes(Cursor::new(serialized)).expect("Failed to decode from bytes");
let mut expected = (*LEVEL_DAT).clone();
expected.data.game_rules = GameRuleRegistry::default();
expected.data.world_gen_settings = WorldGenSettings::default();
expected.data.day_time = 0;
expected.data.clear_weather_time = 0;
assert_eq!(level_dat_again, expected);
let bytes = pumpkin_nbt::Nbt::from(root).write();
assert!(!bytes.is_empty());
}
}

View File

@ -5,11 +5,7 @@ use std::{
};
use pumpkin_data::game_rules::{GameRule, GameRuleRegistry, GameRuleValue};
use pumpkin_nbt::{
compound::NbtCompound,
nbt_compress::{from_gzip_bytes, read_gzip_compound_tag, to_gzip_bytes},
tag::NbtTag,
};
use pumpkin_nbt::{compound::NbtCompound, nbt_compress::read_gzip_compound_tag, tag::NbtTag};
use serde::{Deserialize, Serialize};
use tracing::warn;
@ -50,7 +46,7 @@ impl Default for WeatherData {
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct WorldGenSettingsData {
#[serde(flatten)]
pub settings: WorldGenSettings,
@ -134,8 +130,19 @@ pub fn read_weather(level_folder: &Path) -> WeatherData {
return WeatherData::default();
}
match File::open(&path) {
Ok(f) => match from_gzip_bytes::<DataFileRoot<WeatherData>, _>(f) {
Ok(root) => root.data,
Ok(f) => match read_gzip_compound_tag(f) {
Ok(compound) => {
let data_compound = compound.get_compound("data");
let c = data_compound.as_ref().map_or(&compound, |v| v);
WeatherData {
clear_weather_time: c.get_int("clear_weather_time").unwrap_or(0),
rain_time: c.get_int("rain_time").unwrap_or(0),
thunder_time: c.get_int("thunder_time").unwrap_or(0),
raining: c.get_bool("raining").unwrap_or(false),
thundering: c.get_bool("thundering").unwrap_or(false),
data_version: c.get_int("DataVersion").unwrap_or(0),
}
}
Err(e) => {
warn!("Failed to deserialize weather.dat, using defaults: {e}");
WeatherData::default()
@ -152,8 +159,15 @@ pub fn write_weather(level_folder: &Path, data: &WeatherData) -> Result<(), Worl
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("weather.dat");
let file = File::create(&path)?;
let root = DataFileRoot { data: data.clone() };
to_gzip_bytes(&root, BufWriter::new(file))
let mut data_comp = NbtCompound::new();
data_comp.put_int("clear_weather_time", data.clear_weather_time);
data_comp.put_int("rain_time", data.rain_time);
data_comp.put_int("thunder_time", data.thunder_time);
data_comp.put_bool("raining", data.raining);
data_comp.put_bool("thundering", data.thundering);
let mut root = NbtCompound::new();
root.put_compound("data", data_comp);
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
@ -163,8 +177,17 @@ pub fn read_world_gen_settings(level_folder: &Path) -> Option<WorldGenSettings>
return None;
}
match File::open(&path) {
Ok(f) => match from_gzip_bytes::<DataFileRoot<WorldGenSettingsData>, _>(f) {
Ok(root) => Some(root.data.settings),
Ok(f) => match read_gzip_compound_tag(f) {
Ok(compound) => {
let seed = compound
.get_compound("data")
.and_then(|c| c.get_long("seed"))
.unwrap_or(0);
Some(WorldGenSettings {
seed,
dimensions: std::collections::HashMap::new(),
})
}
Err(e) => {
warn!("Failed to deserialize world_gen_settings.dat: {e}");
None
@ -185,9 +208,13 @@ pub fn write_world_gen_settings(
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("world_gen_settings.dat");
let file = File::create(&path)?;
let data = WorldGenSettingsData::new(settings.clone(), data_version);
let root = DataFileRoot { data };
to_gzip_bytes(&root, BufWriter::new(file))
let mut inner = NbtCompound::new();
inner.put_int("DataVersion", data_version);
inner.put_long("seed", settings.seed);
let mut root = NbtCompound::new();
root.put_compound("data", inner);
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
@ -346,8 +373,16 @@ pub fn read_wandering_trader(level_folder: &Path) -> WanderingTraderData {
return WanderingTraderData::default();
}
match File::open(&path) {
Ok(f) => match from_gzip_bytes::<DataFileRoot<WanderingTraderData>, _>(f) {
Ok(root) => root.data,
Ok(f) => match read_gzip_compound_tag(f) {
Ok(compound) => {
let data_compound = compound.get_compound("data");
let c = data_compound.as_ref().map_or(&compound, |v| v);
WanderingTraderData {
spawn_delay: c.get_int("WanderingTraderSpawnDelay").unwrap_or(24_000),
spawn_chance: c.get_int("WanderingTraderSpawnChance").unwrap_or(25),
data_version: c.get_int("DataVersion").unwrap_or(0),
}
}
Err(e) => {
warn!("Failed to deserialize wandering_trader.dat, using defaults: {e}");
WanderingTraderData::default()
@ -367,8 +402,12 @@ pub fn write_wandering_trader(
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("wandering_trader.dat");
let file = File::create(&path)?;
let root = DataFileRoot { data: data.clone() };
to_gzip_bytes(&root, BufWriter::new(file))
let mut data_comp = NbtCompound::new();
data_comp.put_int("WanderingTraderSpawnDelay", data.spawn_delay);
data_comp.put_int("WanderingTraderSpawnChance", data.spawn_chance);
let mut root = NbtCompound::new();
root.put_compound("data", data_comp);
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}

View File

@ -141,7 +141,7 @@ const fn default_level_version() -> i32 {
MAXIMUM_SUPPORTED_LEVEL_VERSION
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct WorldGenSettings {
// the numerical seed of the world
pub seed: i64,
@ -156,14 +156,14 @@ impl Default for WorldGenSettings {
}
pub type Dimensions = HashMap<String, Dimension>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Dimension {
pub generator: Generator,
#[serde(rename = "type")]
pub dimension_type: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Generator {
#[serde(default)]
pub settings: Option<GeneratorSettings>,
@ -173,48 +173,11 @@ pub struct Generator {
pub generator_type: String,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum GeneratorSettings {
Reference(String),
Compound(pumpkin_nbt::compound::NbtCompound),
}
impl<'de> Deserialize<'de> for GeneratorSettings {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct SettingsVisitor;
impl<'de> serde::de::Visitor<'de> for SettingsVisitor {
type Value = GeneratorSettings;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or compound")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(GeneratorSettings::Reference(v.to_string()))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(GeneratorSettings::Reference(v))
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut compound = pumpkin_nbt::compound::NbtCompound::new();
while let Some((key, value)) =
map.next_entry::<String, pumpkin_nbt::tag::NbtTag>()?
{
compound.put(&key, value);
}
Ok(GeneratorSettings::Compound(compound))
}
}
deserializer.deserialize_any(SettingsVisitor)
}
Compound(serde_json::Value),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]

View File

@ -284,7 +284,7 @@ mod test {
assert_eq!(name, "player_head[minecraft:profile={name:\"Username\"}]");
assert_eq!(stack.item.registry_key, "player_head");
assert_eq!(stack.patch.len(), 1);
assert_eq!(stack.patch[0].0, DataComponent::Profile);
assert_eq!(stack.patch[0].0.to_id(), DataComponent::Profile.to_id());
}
#[test]
@ -301,6 +301,6 @@ mod test {
assert_eq!(name, "stick[minecraft:custom_name=\"Magic wand\"]");
assert_eq!(stack.item.registry_key, "stick");
assert_eq!(stack.patch.len(), 1);
assert_eq!(stack.patch[0].0, DataComponent::CustomName);
assert_eq!(stack.patch[0].0.to_id(), DataComponent::CustomName.to_id());
}
}

View File

@ -2909,7 +2909,8 @@ mod tests {
for dt in bypassing {
assert!(
bypasses_armor_durability(dt),
"{dt:?} should bypass armor durability"
"{} should bypass armor durability",
dt.message_id
);
}
}
@ -2934,7 +2935,8 @@ mod tests {
for dt in physical {
assert!(
!bypasses_armor_durability(dt),
"{dt:?} should NOT bypass armor durability"
"{} should NOT bypass armor durability",
dt.message_id
);
}
}

View File

@ -309,7 +309,5 @@ fn build_dimension_nbt(dim: &pumpkin_data::dimension::Dimension) -> Vec<u8> {
monster_spawn.put_compound("value", value);
compound.put_compound("monster_spawn_light_level", monster_spawn);
let mut bytes = Vec::new();
let _ = pumpkin_nbt::serializer::to_bytes(&compound, &mut bytes);
bytes
pumpkin_nbt::Nbt::from(compound).write().to_vec()
}

View File

@ -259,7 +259,7 @@ impl pumpkin::plugin::command::HostConsumedArgs for PluginHostState {
pumpkin::plugin::command::SoundCategory::Voice
}
}),
OwnedArg::DamageType(d) => Arg::DamageType(format!("{d:?}")),
OwnedArg::DamageType(d) => Arg::DamageType(d.message_id.to_string()),
OwnedArg::Effect(e) => Arg::Effect(e.minecraft_name.to_string()),
OwnedArg::Enchantment(e) => Arg::Enchantment(e.name.to_string()),
OwnedArg::Advancement(a) => Arg::Advancement(a.to_string()),

View File

@ -76,7 +76,7 @@ use pumpkin_data::{
use pumpkin_data::{BlockDirection, BlockState, HorizontalFacingExt, translation};
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData};
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
use pumpkin_protocol::java::client::play::{
@ -5168,14 +5168,13 @@ impl World {
let entity_id = block_entity.resource_location().to_string();
if let Some(nbt) = &block_entity_nbt {
let mut bytes = Vec::new();
to_bytes_unnamed(nbt, &mut bytes).unwrap();
let bytes = pumpkin_nbt::Nbt::from(nbt.clone()).write_unnamed();
self.broadcast_to_chunk(
chunk_pos,
&CBlockEntityData::new(
block_entity.get_position(),
VarInt(block_entity.get_id() as i32),
bytes.into_boxed_slice(),
bytes.as_ref().into(),
),
);
}
@ -5259,14 +5258,13 @@ impl World {
let block_entity_nbt = block_entity.chunk_data_nbt();
if let Some(nbt) = &block_entity_nbt {
let mut bytes = Vec::new();
to_bytes_unnamed(nbt, &mut bytes).unwrap();
let bytes = pumpkin_nbt::Nbt::from(nbt.clone()).write_unnamed();
self.broadcast_to_chunk(
chunk_pos,
&CBlockEntityData::new(
block_entity.get_position(),
VarInt(block_entity.get_id() as i32),
bytes.into_boxed_slice(),
bytes.as_ref().into(),
),
);
let mut full_nbt = nbt.clone();