Skip to content

Commit 2e02244

Browse files
feat: added unwrapping and wrapping for NBT lists to and from binary (#1742)
* added wrapping, unwrapping tags during serializing/deserializing NBT to/from bytes and added a serialization test * fixed wrapping check in `wrap_tag_if_needed` * added wrapped compound to test and renamed `put_component` * fixed formatting * fixed comment in test * fixed test comments
1 parent 27afc4e commit 2e02244

File tree

7 files changed

+171
-17
lines changed

7 files changed

+171
-17
lines changed

pumpkin-nbt/src/compound.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ impl NbtCompound {
130130
self.put(name, NbtTag::Double(value));
131131
}
132132

133-
pub fn put_component(&mut self, name: &str, value: Self) {
133+
pub fn put_compound(&mut self, name: &str, value: Self) {
134134
self.put(name, NbtTag::Compound(value));
135135
}
136136

pumpkin-nbt/src/lib.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,14 @@ mod test {
211211
use std::io::Cursor;
212212

213213
use crate::Error;
214+
use crate::compound::NbtCompound;
214215
use crate::deserializer::from_bytes;
215216
use crate::nbt_byte_array;
216217
use crate::nbt_int_array;
217218
use crate::nbt_long_array;
218-
use crate::serializer::to_bytes;
219219
use crate::serializer::to_bytes_named;
220+
use crate::serializer::{WriteAdaptor, to_bytes};
221+
use crate::tag::NbtTag;
220222
use crate::{deserializer::from_bytes_unnamed, serializer::to_bytes_unnamed};
221223
use serde::{Deserialize, Serialize};
222224

@@ -406,6 +408,88 @@ mod test {
406408
assert_eq!(list_compound, recreated_struct);
407409
}
408410

411+
#[test]
412+
fn wrapper_compound_lists() {
413+
let mut vec: Vec<NbtTag> = Vec::new();
414+
415+
// These tags will be wrapped during serialization.
416+
vec.push(NbtTag::Int(-1823));
417+
vec.push(NbtTag::Int(123));
418+
vec.push(NbtTag::String("Not an int".to_string()));
419+
vec.push(NbtTag::Byte(2));
420+
421+
// This compound will not, since the list is already a list of compound tags.
422+
// This compound cannot be unwrapped in any way, so it is preserved
423+
// on deserialization.
424+
vec.push(NbtTag::Compound({
425+
let mut compound = NbtCompound::new();
426+
compound.put_short("example", 1234);
427+
compound
428+
}));
429+
430+
// This wrapper compound will be wrapped because we want to preserve the
431+
// original data during deserialization.
432+
//
433+
// Suppose we had {"": `tag`}. If we didn't wrap this, on deserialization,
434+
// we would get `tag`, which doesn't match the serialized compound tag.
435+
// Therefore, we must wrap it and serialize {"": {"": `tag`}}.
436+
// Then on deserialization, we get {"": `tag`}, which matches what we wanted
437+
// to serialize in the first place.
438+
//
439+
// This compound represents {"": 1L}.
440+
vec.push(NbtTag::Compound({
441+
let mut compound = NbtCompound::new();
442+
compound.put_long("", 1);
443+
compound
444+
}));
445+
446+
let expected_bytes = [
447+
0x09, // List type
448+
0x0A, // This list is a compound tag list
449+
0x00, 0x00, 0x00, 0x06, // This list has 6 elements.
450+
// Now for parsing each compound tag:
451+
0x03, // Int type
452+
0x00, 0x00, // Empty key
453+
0xFF, 0xFF, 0xF8, 0xE1, // -1823
454+
0x00, // End
455+
0x03, // Int type
456+
0x00, 0x00, // Empty key
457+
0x00, 0x00, 0x00, 0x7B, // 123
458+
0x00, // End
459+
0x08, // String type
460+
0x00, 0x00, // Empty key
461+
0x00, 0x0A, // The string is 10 characters long.
462+
0x4E, 0x6F, 0x74, 0x20, 0x61, 0x6E, 0x20, 0x69, 0x6E, 0x74, // "Not an int"
463+
0x00, // End
464+
0x01, // Byte type
465+
0x00, 0x00, // Empty key
466+
0x02, // 2b
467+
0x00, // End
468+
// For the first (unwrapped) compound:
469+
0x02, // Short type
470+
0x00, 0x07, // The key is 7 characters long.
471+
0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // "example"
472+
0x04, 0xD2, // 1234
473+
0x00, // End
474+
// For the second (wrapped) wrapper compound:
475+
0x0A, // Compound type
476+
0x00, 0x00, // Empty key
477+
0x04, // Long type
478+
0x00, 0x00, // Empty key
479+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 1L
480+
0x00, // End
481+
0x00, // End
482+
];
483+
484+
let mut bytes = Vec::new();
485+
let mut write_adaptor = WriteAdaptor::new(&mut bytes);
486+
NbtTag::List(vec)
487+
.serialize(&mut write_adaptor)
488+
.expect("Expected serialization to succeed");
489+
490+
assert_eq!(bytes, expected_bytes);
491+
}
492+
409493
#[test]
410494
fn nbt_arrays() {
411495
#[derive(Serialize)]

pumpkin-nbt/src/nbt_compress.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ mod tests {
131131
// Create a nested compound
132132
let mut nested = NbtCompound::new();
133133
nested.put_int("nested_int", 42);
134-
compound.put_component("nested_compound", nested);
134+
compound.put_compound("nested_compound", nested);
135135

136136
// Write to GZip using streaming
137137
let mut buffer = Vec::new();

pumpkin-nbt/src/tag.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,70 @@ impl NbtTag {
5555
Ok(())
5656
}
5757

58+
/// Gets the element type of [`NbtTag::List`] the provided `Vec`
59+
/// represents. If any elements in the `Vec` are found to be of
60+
/// different types, this returns [`COMPOUND_ID`].
61+
#[must_use]
62+
fn get_list_element_type_id(vec: &Vec<Self>) -> u8 {
63+
let mut element_id = END_ID;
64+
65+
for tag in vec {
66+
let id = tag.get_type_id();
67+
if element_id == END_ID {
68+
element_id = id;
69+
} else if element_id != id {
70+
return COMPOUND_ID;
71+
}
72+
}
73+
74+
element_id
75+
}
76+
77+
/// Tries to unwrap (flatten) a wrapped `NbtTag`. If there is a wrapped tag, it is returned.
78+
/// If no unwrap is possible, this returns the given tag.
79+
fn flatten(tag: Self) -> Self {
80+
if let Self::Compound(mut compound) = tag {
81+
// Try to get the wrapped tag, stored by "".
82+
if Self::is_wrapper_compound(&compound) {
83+
compound.child_tags.remove(0).1
84+
} else {
85+
Self::Compound(compound)
86+
}
87+
} else {
88+
tag
89+
}
90+
}
91+
92+
/// Returns whether an [`NbtCompound`] is a wrapper compound.
93+
///
94+
/// A *wrapper compound* is a compound that stores exactly one
95+
/// key-value pair, an empty string key (`""`) and an `NbtTag`.
96+
fn is_wrapper_compound(compound: &NbtCompound) -> bool {
97+
compound.child_tags.len() == 1 && compound.child_tags[0].0.is_empty()
98+
}
99+
100+
/// Wraps the provided tag if needed with the provided element type of list
101+
/// the wrapped tag, if any, would be added to.
102+
fn wrap_tag_if_needed(element_type: u8, tag: Self) -> Self {
103+
if element_type == COMPOUND_ID {
104+
if let Self::Compound(compound) = &tag
105+
&& !Self::is_wrapper_compound(compound)
106+
{
107+
tag
108+
} else {
109+
Self::wrap_tag(tag)
110+
}
111+
} else {
112+
tag
113+
}
114+
}
115+
116+
fn wrap_tag(tag: Self) -> Self {
117+
let mut compound = NbtCompound::new();
118+
compound.put("", tag);
119+
Self::Compound(compound)
120+
}
121+
58122
pub fn serialize_data<W: Write>(self, w: &mut WriteAdaptor<W>) -> serializer::Result<()> {
59123
match self {
60124
Self::End => {}
@@ -82,10 +146,15 @@ impl NbtTag {
82146
return Err(Error::LargeLength(len));
83147
}
84148

85-
w.write_u8_be(list.first().unwrap_or(&Self::End).get_type_id())?;
149+
let list_element_id = Self::get_list_element_type_id(&list);
150+
151+
w.write_u8_be(list_element_id)?;
86152
w.write_i32_be(len as i32)?;
87153
for nbt_tag in list {
88-
nbt_tag.serialize_data(w)?;
154+
// Since tags in the same list tag must have the same type,
155+
// we need to handle those of different tag types by
156+
// wrapping them in `NbtCompound`s if needed.
157+
Self::wrap_tag_if_needed(list_element_id, nbt_tag).serialize_data(w)?;
89158
}
90159
}
91160
Self::Compound(compound) => {
@@ -229,7 +298,8 @@ impl NbtTag {
229298
for _ in 0..len {
230299
let tag = Self::deserialize_data(reader, tag_type_id)?;
231300
assert_eq!(tag.get_type_id(), tag_type_id);
232-
list.push(tag);
301+
// Try unwrapping the tag.
302+
list.push(Self::flatten(tag));
233303
}
234304
Ok(Self::List(list))
235305
}

pumpkin-world/src/block/entities/mob_spawner.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ impl BlockEntity for MobSpawnerBlockEntity {
173173
let mut entity_nbt = NbtCompound::new();
174174
entity_nbt.put_string("id", format!("minecraft:{}", entity_type.resource_name));
175175

176-
nbt.put_component("entity", entity_nbt);
176+
nbt.put_compound("entity", entity_nbt);
177177
}
178178
})
179179
}
@@ -186,9 +186,9 @@ impl BlockEntity for MobSpawnerBlockEntity {
186186
let mut entity_nbt = NbtCompound::new();
187187
entity_nbt.put_string("id", format!("minecraft:{}", entity_type.resource_name));
188188

189-
spawn_entry.put_component("entity", entity_nbt);
189+
spawn_entry.put_compound("entity", entity_nbt);
190190

191-
final_nbt.put_component("SpawnData", spawn_entry);
191+
final_nbt.put_compound("SpawnData", spawn_entry);
192192
}
193193
Some(final_nbt)
194194
}

pumpkin-world/src/item/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ impl ItemStack {
436436
}
437437

438438
// Store custom data like enchantments, display name, etc. would go here
439-
compound.put_component("components", tag);
439+
compound.put_compound("components", tag);
440440
}
441441

442442
#[must_use]

pumpkin/src/entity/player.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,27 +3019,27 @@ impl NBTStorage for PlayerInventory {
30193019
drop(stack);
30203020
match slot {
30213021
EquipmentSlot::OffHand(_) => {
3022-
equipment_compound.put_component("offhand", item_compound);
3022+
equipment_compound.put_compound("offhand", item_compound);
30233023
}
30243024
EquipmentSlot::Feet(_) => {
3025-
equipment_compound.put_component("feet", item_compound);
3025+
equipment_compound.put_compound("feet", item_compound);
30263026
}
30273027
EquipmentSlot::Legs(_) => {
3028-
equipment_compound.put_component("legs", item_compound);
3028+
equipment_compound.put_compound("legs", item_compound);
30293029
}
30303030
EquipmentSlot::Chest(_) => {
3031-
equipment_compound.put_component("chest", item_compound);
3031+
equipment_compound.put_compound("chest", item_compound);
30323032
}
30333033
EquipmentSlot::Head(_) => {
3034-
equipment_compound.put_component("head", item_compound);
3034+
equipment_compound.put_compound("head", item_compound);
30353035
}
30363036
_ => {
30373037
warn!("Invalid equipment slot for a player");
30383038
}
30393039
}
30403040
}
30413041
}
3042-
nbt.put_component("equipment", equipment_compound);
3042+
nbt.put_compound("equipment", equipment_compound);
30433043
nbt.put("Inventory", NbtTag::List(items));
30443044
})
30453045
}
@@ -3302,7 +3302,7 @@ impl NBTStorage for Abilities {
33023302
component.put_bool("mayBuild", self.allow_modify_world);
33033303
component.put_float("flySpeed", self.fly_speed);
33043304
component.put_float("walkSpeed", self.walk_speed);
3305-
nbt.put_component("abilities", component);
3305+
nbt.put_compound("abilities", component);
33063306
})
33073307
}
33083308

0 commit comments

Comments
 (0)