From af854597610ae1eb1547e003de24adeb732146e0 Mon Sep 17 00:00:00 2001 From: mworzala Date: Fri, 12 Apr 2024 09:28:07 -0400 Subject: [PATCH] chore: more components --- .../net/minestom/server/entity/Player.java | 65 ++++----- .../minestom/server/item/ItemComponent.java | 7 +- .../server/item/ItemComponentMap.java | 12 -- .../server/item/ItemComponentPatch.java | 133 +++++++++++++++++- .../net/minestom/server/item/ItemStack.java | 5 +- .../minestom/server/item/ItemStackImpl.java | 31 +++- .../server/item/book/FilteredText.java | 60 ++++++++ .../item/component/WritableBookContent.java | 57 ++++++++ .../item/component/WrittenBookContent.java | 72 ++++++++++ .../server/utils/nbt/BinaryTagUtil.java | 5 + 10 files changed, 389 insertions(+), 58 deletions(-) create mode 100644 src/main/java/net/minestom/server/item/book/FilteredText.java create mode 100644 src/main/java/net/minestom/server/item/component/WritableBookContent.java create mode 100644 src/main/java/net/minestom/server/item/component/WrittenBookContent.java diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index ab7836b41..446de1935 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -18,6 +18,7 @@ import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent.ShowEntity; import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.title.TitlePart; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; @@ -48,9 +49,10 @@ import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.item.ItemComponent; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; -import net.minestom.server.item.metadata.WrittenBookMeta; +import net.minestom.server.item.component.WrittenBookContent; import net.minestom.server.listener.manager.PacketListenerManager; import net.minestom.server.message.ChatMessageType; import net.minestom.server.message.ChatPosition; @@ -181,7 +183,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, protected PlayerInventory inventory; private Inventory openInventory; // Used internally to allow the closing of inventory within the inventory listener - private boolean didCloseInventory; + private boolean skipClosePacket; private byte heldSlot; @@ -1000,13 +1002,13 @@ public class Player extends LivingEntity implements CommandSender, Localizable, closeInventory(); } + // TODO: when adventure updates, delete this + String title = PlainTextComponentSerializer.plainText().serialize(book.title()); + String author = PlainTextComponentSerializer.plainText().serialize(book.author()); final ItemStack writtenBook = ItemStack.builder(Material.WRITTEN_BOOK) - .meta(WrittenBookMeta.class, builder -> builder.resolved(false) - .generation(WrittenBookMeta.WrittenBookGeneration.ORIGINAL) - .author(book.author()) - .title(book.title()) - .pages(book.pages())) + .set(ItemComponent.WRITTEN_BOOK_CONTENT, new WrittenBookContent(book.pages(), title, author, 0, false)) .build(); + // Set book in offhand sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFFHAND_SLOT, writtenBook)); // Open the book @@ -1726,6 +1728,19 @@ public class Player extends LivingEntity implements CommandSender, Localizable, return openInventory; } + private void tryCloseInventory(boolean skipClosePacket) { + var closedInventory = getOpenInventory(); + if (closedInventory == null) return; + + this.skipClosePacket = skipClosePacket; + + if (closedInventory.removeViewer(this)) { + if (closedInventory == getOpenInventory()) { + this.openInventory = null; + } + } + } + /** * Opens the specified Inventory, close the previous inventory if existing. * @@ -1736,21 +1751,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable, InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this); EventDispatcher.callCancellable(inventoryOpenEvent, () -> { - Inventory openInventory = getOpenInventory(); - if (openInventory != null) { - openInventory.removeViewer(this); - } + tryCloseInventory(true); Inventory newInventory = inventoryOpenEvent.getInventory(); - if (newInventory == null) { - // just close the inventory - return; + if (newInventory.addViewer(this)) { + this.openInventory = newInventory; } - - sendPacket(new OpenWindowPacket(newInventory.getWindowId(), - newInventory.getInventoryType().getWindowType(), newInventory.getTitle())); - newInventory.addViewer(this); - this.openInventory = newInventory; }); return !inventoryOpenEvent.isCancelled(); } @@ -1799,27 +1805,16 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } /** - * Used internally to prevent an inventory click to be processed - * when the inventory listeners closed the inventory. - *

- * Should only be used within an inventory listener (event or condition). - * - * @return true if the inventory has been closed, false otherwise - */ - public boolean didCloseInventory() { - return didCloseInventory; - } - - /** - * Used internally to reset the didCloseInventory field. + * Used internally to reset the skipClosePacket field, which determines when sending the close inventory packet + * should be skipped. *

* Shouldn't be used externally without proper understanding of its consequence. * - * @param didCloseInventory the new didCloseInventory field + * @param skipClosePacket the new skipClosePacket field */ @ApiStatus.Internal - public void UNSAFE_changeDidCloseInventory(boolean didCloseInventory) { - this.didCloseInventory = didCloseInventory; + public void UNSAFE_changeSkipClosePacket(boolean skipClosePacket) { + this.skipClosePacket = skipClosePacket; } public int getNextTeleportId() { diff --git a/src/main/java/net/minestom/server/item/ItemComponent.java b/src/main/java/net/minestom/server/item/ItemComponent.java index 8f0d6b231..eef69da27 100644 --- a/src/main/java/net/minestom/server/item/ItemComponent.java +++ b/src/main/java/net/minestom/server/item/ItemComponent.java @@ -51,8 +51,8 @@ public sealed interface ItemComponent extends StaticProtocolObject permits It ItemComponent> BUNDLE_CONTENTS = declare("bundle_contents", NetworkBuffer.ITEM.list(Short.MAX_VALUE), BinaryTagSerializer.ITEM.list()); ItemComponent POTION_CONTENTS = declare("potion_contents", null, null); //todo ItemComponent SUSPICIOUS_STEW_EFFECTS = declare("suspicious_stew_effects", null, null); //todo - ItemComponent WRITABLE_BOOK_CONTENT = declare("writable_book_content", null, null); //todo - ItemComponent WRITTEN_BOOK_CONTENT = declare("written_book_content", null, null); //todo + ItemComponent WRITABLE_BOOK_CONTENT = declare("writable_book_content", WritableBookContent.NETWORK_TYPE, WritableBookContent.NBT_TYPE); + ItemComponent WRITTEN_BOOK_CONTENT = declare("written_book_content", WrittenBookContent.NETWORK_TYPE, WrittenBookContent.NBT_TYPE); ItemComponent TRIM = declare("trim", null, null); //todo ItemComponent DEBUG_STICK_STATE = declare("debug_stick_state", null, null); //todo ItemComponent ENTITY_DATA = declare("entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); @@ -76,8 +76,11 @@ public sealed interface ItemComponent extends StaticProtocolObject permits It ItemComponent CONTAINER_LOOT = declare("container_loot", null, SeededContainerLoot.NBT_TYPE); @NotNull T read(@NotNull BinaryTag tag); + @NotNull BinaryTag write(@NotNull T value); @NotNull T read(@NotNull NetworkBuffer reader); + void write(@NotNull NetworkBuffer writer, @NotNull T value); + static @Nullable ItemComponent fromNamespaceId(@NotNull String namespaceId) { return ItemComponentImpl.NAMESPACES.get(namespaceId); diff --git a/src/main/java/net/minestom/server/item/ItemComponentMap.java b/src/main/java/net/minestom/server/item/ItemComponentMap.java index 124baece9..3e3b9d011 100644 --- a/src/main/java/net/minestom/server/item/ItemComponentMap.java +++ b/src/main/java/net/minestom/server/item/ItemComponentMap.java @@ -5,10 +5,6 @@ import org.jetbrains.annotations.Nullable; public interface ItemComponentMap { - static @NotNull Builder builder() { - //todo - } - boolean has(@NotNull ItemComponent component); @Nullable T get(@NotNull ItemComponent component); @@ -18,12 +14,4 @@ public interface ItemComponentMap { return value != null ? value : defaultValue; } - interface Builder { - - @NotNull Builder set(@NotNull ItemComponent component, @NotNull T value); - - void remove(@NotNull ItemComponent component); - - @NotNull ItemComponentMap build(); - } } diff --git a/src/main/java/net/minestom/server/item/ItemComponentPatch.java b/src/main/java/net/minestom/server/item/ItemComponentPatch.java index d148f8350..85b618dfc 100644 --- a/src/main/java/net/minestom/server/item/ItemComponentPatch.java +++ b/src/main/java/net/minestom/server/item/ItemComponentPatch.java @@ -1,18 +1,141 @@ package net.minestom.server.item; +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.nbt.BinaryTagSerializer; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public sealed interface ItemComponentPatch extends ItemComponentMap permits ItemComponentPatchImpl { +import java.util.Map; - @NotNull NetworkBuffer.Type NETWORK_TYPE = null; - @NotNull BinaryTagSerializer NBT_TYPE = null; +/** + *

Holds the altered components of an itemstack.

+ * + *

The inner map contains the value for added components, null for removed components, and no entry for unmodified components.

+ * + * @param patch + */ +record ItemComponentPatch(@NotNull Int2ObjectMap patch) { + private static final char REMOVAL_PREFIX = '!'; - @NotNull ItemComponentPatch with(@NotNull ItemComponent component, T value); + public static final ItemComponentPatch EMPTY = new ItemComponentPatch(new Int2ObjectArrayMap<>(0)); - @NotNull ItemComponentPatch without(@NotNull ItemComponent component); + public static final @NotNull NetworkBuffer.Type NETWORK_TYPE = new NetworkBuffer.Type<>() { + @Override + public void write(@NotNull NetworkBuffer buffer, ItemComponentPatch value) { + int added = 0; + for (Object o : value.patch.values()) { + if (o != null) added++; + } + + buffer.write(NetworkBuffer.VAR_INT, added); + buffer.write(NetworkBuffer.VAR_INT, value.patch.size() - added); + for (Int2ObjectMap.Entry entry : value.patch.int2ObjectEntrySet()) { + if (entry.getValue() != null) { + buffer.write(NetworkBuffer.VAR_INT, entry.getIntKey()); + //noinspection unchecked + ItemComponent type = (ItemComponent) ItemComponent.fromId(entry.getIntKey()); + assert type != null; + type.write(entry.getValue()); + } + } + for (Int2ObjectMap.Entry entry : value.patch.int2ObjectEntrySet()) { + if (entry.getValue() == null) { + buffer.write(NetworkBuffer.VAR_INT, entry.getIntKey()); + } + } + } + + @Override + public ItemComponentPatch read(@NotNull NetworkBuffer buffer) { + int added = buffer.read(NetworkBuffer.VAR_INT); + int removed = buffer.read(NetworkBuffer.VAR_INT); + Int2ObjectMap patch = new Int2ObjectArrayMap<>(added + removed); + for (int i = 0; i < added; i++) { + int id = buffer.read(NetworkBuffer.VAR_INT); + //noinspection unchecked + ItemComponent type = (ItemComponent) ItemComponent.fromId(id); + Check.notNull(type, "Unknown item component id: {0}", id); + patch.put(id, type.read(buffer)); + } + for (int i = 0; i < removed; i++) { + int id = buffer.read(NetworkBuffer.VAR_INT); + patch.put(id, null); + } + return new ItemComponentPatch(patch); + } + }; + public static final @NotNull BinaryTagSerializer NBT_TYPE = BinaryTagSerializer.COMPOUND.map( + tag -> { + if (tag.size() == 0) return EMPTY; + Int2ObjectMap patch = new Int2ObjectArrayMap<>(tag.size()); + for (Map.Entry entry : tag) { + String key = entry.getKey(); + boolean remove = false; + if (!key.isEmpty() && key.charAt(0) == REMOVAL_PREFIX) { + key = key.substring(1); + remove = true; + } + ItemComponent type = ItemComponent.fromNamespaceId(key); + Check.notNull(type, "Unknown item component: {0}", key); + if (remove) { + patch.put(type.id(), null); + } else { + Object value = type.read(entry.getValue()); + patch.put(type.id(), value); + } + } + return new ItemComponentPatch(patch); + }, + patch -> { + if (patch.patch.isEmpty()) return CompoundBinaryTag.empty(); + CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder(); + for (Int2ObjectMap.Entry entry : patch.patch.int2ObjectEntrySet()) { + //noinspection unchecked + ItemComponent type = (ItemComponent) ItemComponent.fromId(entry.getIntKey()); + Check.notNull(type, "Unknown item component id: {0}", entry.getIntKey()); + if (entry.getValue() == null) { + builder.put(REMOVAL_PREFIX + type.name(), CompoundBinaryTag.empty()); + } else { + builder.put(type.name(), type.write(entry.getValue())); + } + } + return builder.build(); + } + ); + + public boolean has(@NotNull ItemComponentMap prototype, @NotNull ItemComponent component) { + if (patch.containsKey(component.id())) { + return patch.get(component.id()) != null; + } else { + return prototype.has(component); + } + } + + public @Nullable T get(@NotNull ItemComponentMap prototype, @NotNull ItemComponent component) { + if (patch.containsKey(component.id())) { + return (T) patch.get(component.id()); + } else { + return prototype.get(component); + } + } + + public @NotNull ItemComponentPatch with(@NotNull ItemComponent component, @NotNull T value) { + Int2ObjectMap newPatch = new Int2ObjectArrayMap<>(patch); + newPatch.put(component.id(), value); + return new ItemComponentPatch(newPatch); + } + + public @NotNull ItemComponentPatch without(@NotNull ItemComponent component) { + Int2ObjectMap newPatch = new Int2ObjectArrayMap<>(patch); + newPatch.put(component.id(), null); + return new ItemComponentPatch(newPatch); + } interface Builder extends ItemComponentMap { diff --git a/src/main/java/net/minestom/server/item/ItemStack.java b/src/main/java/net/minestom/server/item/ItemStack.java index 2809830ff..adfc8f390 100644 --- a/src/main/java/net/minestom/server/item/ItemStack.java +++ b/src/main/java/net/minestom/server/item/ItemStack.java @@ -4,6 +4,7 @@ import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEventSource; import net.minestom.server.item.component.CustomData; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.item.component.ItemComponent; import net.minestom.server.item.component.ItemComponentMap; import net.minestom.server.inventory.ContainerInventory; @@ -33,6 +34,8 @@ public sealed interface ItemStack extends TagReadable, ItemComponentMap, HoverEv */ @NotNull ItemStack AIR = ItemStack.of(Material.AIR); + @NotNull NetworkBuffer.Type NETWORK_TYPE = ItemStackImpl.NETWORK_TYPE; + @Contract(value = "_ -> new", pure = true) static @NotNull Builder builder(@NotNull Material material) { return new ItemStackImpl.Builder(material, 1); @@ -93,7 +96,7 @@ public sealed interface ItemStack extends TagReadable, ItemComponentMap, HoverEv @Contract(value = "_, _ -> new", pure = true) default @NotNull ItemStack withTag(@NotNull Tag tag, @Nullable T value) { - return withMeta(builder -> builder.set(tag, value)); + return with(ItemComponent.CUSTOM_DATA, get(ItemComponent.CUSTOM_DATA, CustomData.EMPTY).withTag(tag, value)); } @Override diff --git a/src/main/java/net/minestom/server/item/ItemStackImpl.java b/src/main/java/net/minestom/server/item/ItemStackImpl.java index b5ba800e7..4574d5de6 100644 --- a/src/main/java/net/minestom/server/item/ItemStackImpl.java +++ b/src/main/java/net/minestom/server/item/ItemStackImpl.java @@ -2,7 +2,10 @@ package net.minestom.server.item; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.minestom.server.item.component.CustomData; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.tag.Tag; +import net.minestom.server.utils.nbt.BinaryTagSerializer; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,23 +14,26 @@ import java.util.function.Consumer; record ItemStackImpl(Material material, int amount, ItemComponentPatch components) implements ItemStack { + static final NetworkBuffer.Type NETWORK_TYPE = null; + static final BinaryTagSerializer NBT_TYPE = BinaryTagSerializer.COMPOUND.map(ItemStackImpl::fromCompound, ItemStackImpl::toCompound); + static ItemStack create(Material material, int amount, ItemComponentPatch components) { if (amount <= 0) return AIR; return new ItemStackImpl(material, amount, components); } static ItemStack create(Material material, int amount) { - return create(material, amount, ItemComponentPatch.builder(material).build()); + return create(material, amount, ItemComponentPatch.EMPTY); } @Override public @Nullable T get(@NotNull ItemComponent component) { - return components.get(component); + return components.get(material.prototype(), component); } @Override public boolean has(@NotNull ItemComponent component) { - return components.has(component); + return components.has(material.prototype(), component); } @Override @@ -71,6 +77,8 @@ record ItemStackImpl(Material material, int amount, ItemComponentPatch component @Override public @NotNull CompoundBinaryTag toItemNBT() { + return (CompoundBinaryTag) NBT_TYPE.write(this); + // CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() // .putString("id", material.name()) // .putByte("Count", (byte) amount); @@ -85,6 +93,23 @@ record ItemStackImpl(Material material, int amount, ItemComponentPatch component return new Builder(material, amount, components.builder()); } + private static @NotNull ItemStack fromCompound(@NotNull CompoundBinaryTag tag) { + String id = tag.getString("id"); + Material material = Material.fromNamespaceId(id); + Check.notNull(material, "Unknown material: {0}", id); + int count = tag.getInt("count", 1); + ItemComponentPatch patch = ItemComponentPatch.NBT_TYPE.read(tag.getCompound("components")); + return new ItemStackImpl(material, count, patch); + } + + private static @NotNull CompoundBinaryTag toCompound(@NotNull ItemStack itemStack) { + return CompoundBinaryTag.builder() + .putString("id", itemStack.material().name()) + .putInt("count", itemStack.amount()) + .put("components", ItemComponentPatch.NBT_TYPE.write(((ItemStackImpl) itemStack).components)) + .build(); + } + static final class Builder implements ItemStack.Builder { final Material material; int amount; diff --git a/src/main/java/net/minestom/server/item/book/FilteredText.java b/src/main/java/net/minestom/server/item/book/FilteredText.java new file mode 100644 index 000000000..38c0ad757 --- /dev/null +++ b/src/main/java/net/minestom/server/item/book/FilteredText.java @@ -0,0 +1,60 @@ +package net.minestom.server.item.book; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.utils.nbt.BinaryTagSerializer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record FilteredText(@NotNull T text, @Nullable T filtered) { + + public static @NotNull NetworkBuffer.Type> STRING_NETWORK_TYPE = createNetworkType(NetworkBuffer.STRING); + public static @NotNull BinaryTagSerializer> STRING_NBT_TYPE = createNbtType(BinaryTagSerializer.STRING); + + public static @NotNull NetworkBuffer.Type> COMPONENT_NETWORK_TYPE = createNetworkType(NetworkBuffer.COMPONENT); + public static @NotNull BinaryTagSerializer> COMPONENT_NBT_TYPE = createNbtType(BinaryTagSerializer.JSON_COMPONENT); + + private static NetworkBuffer.@NotNull Type> createNetworkType(@NotNull NetworkBuffer.Type inner) { + return new NetworkBuffer.Type<>() { + @Override + public void write(@NotNull NetworkBuffer buffer, FilteredText value) { + buffer.write(inner, value.text); + buffer.writeOptional(inner, value.filtered); + } + + @Override + public FilteredText read(@NotNull NetworkBuffer buffer) { + return new FilteredText<>(buffer.read(inner), buffer.readOptional(inner)); + } + }; + } + + private static @NotNull BinaryTagSerializer> createNbtType(@NotNull BinaryTagSerializer inner) { + return new BinaryTagSerializer<>() { + @Override + public @NotNull BinaryTag write(@NotNull FilteredText value) { + CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder(); + builder.put("text", inner.write(value.text)); + if (value.filtered != null) { + builder.put("filtered", inner.write(value.filtered)); + } + return builder.build(); + } + + @Override + public @NotNull FilteredText read(@NotNull BinaryTag tag) { + if (tag instanceof CompoundBinaryTag compound) { + BinaryTag textTag = compound.get("text"); + if (textTag != null) { + BinaryTag filteredTag = compound.get("filtered"); + T filtered = filteredTag == null ? null : inner.read(filteredTag); + return new FilteredText<>(inner.read(textTag), filtered); + } + } + return new FilteredText<>(inner.read(tag), null); + } + }; + } +} diff --git a/src/main/java/net/minestom/server/item/component/WritableBookContent.java b/src/main/java/net/minestom/server/item/component/WritableBookContent.java new file mode 100644 index 000000000..b416f20cc --- /dev/null +++ b/src/main/java/net/minestom/server/item/component/WritableBookContent.java @@ -0,0 +1,57 @@ +package net.minestom.server.item.component; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.minestom.server.item.book.FilteredText; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.utils.nbt.BinaryTagSerializer; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public record WritableBookContent(@NotNull List> pages) { + public static final WritableBookContent EMPTY = new WritableBookContent(List.of()); + + public static final NetworkBuffer.Type NETWORK_TYPE = new NetworkBuffer.Type<>() { + @Override + public void write(@NotNull NetworkBuffer buffer, WritableBookContent value) { + buffer.writeCollection(FilteredText.STRING_NETWORK_TYPE, value.pages); + } + + @Override + public WritableBookContent read(@NotNull NetworkBuffer buffer) { + return new WritableBookContent(buffer.readCollection(FilteredText.STRING_NETWORK_TYPE, 100)); + } + }; + + public static final BinaryTagSerializer NBT_TYPE = new BinaryTagSerializer<>() { + @Override + public @NotNull BinaryTag write(@NotNull WritableBookContent value) { + ListBinaryTag.Builder pages = ListBinaryTag.builder(); + for (FilteredText page : value.pages) { + pages.add(FilteredText.STRING_NBT_TYPE.write(page)); + } + return CompoundBinaryTag.builder().put("pages", pages.build()).build(); + } + + @Override + public @NotNull WritableBookContent read(@NotNull BinaryTag tag) { + if (!(tag instanceof CompoundBinaryTag compound)) return EMPTY; + ListBinaryTag pagesTag = compound.getList("pages"); + if (pagesTag.size() == 0) return EMPTY; + + List> pages = new ArrayList<>(pagesTag.size()); + for (BinaryTag pageTag : pagesTag) { + pages.add(FilteredText.STRING_NBT_TYPE.read(pageTag)); + } + return new WritableBookContent(pages); + } + }; + + public WritableBookContent { + pages = List.copyOf(pages); + } + +} diff --git a/src/main/java/net/minestom/server/item/component/WrittenBookContent.java b/src/main/java/net/minestom/server/item/component/WrittenBookContent.java new file mode 100644 index 000000000..5ab694a26 --- /dev/null +++ b/src/main/java/net/minestom/server/item/component/WrittenBookContent.java @@ -0,0 +1,72 @@ +package net.minestom.server.item.component; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.text.Component; +import net.minestom.server.item.book.FilteredText; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.utils.nbt.BinaryTagSerializer; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record WrittenBookContent(@NotNull List> pages, @NotNull FilteredText title, @NotNull String author, int generation, boolean resolved) { + public static final WrittenBookContent EMPTY = new WrittenBookContent(List.of(), new FilteredText<>("", null), "", 0, true); + + public static final @NotNull NetworkBuffer.Type NETWORK_TYPE = new NetworkBuffer.Type<>() { + @Override + public void write(@NotNull NetworkBuffer buffer, WrittenBookContent value) { + buffer.write(FilteredText.STRING_NETWORK_TYPE, value.title); + buffer.write(NetworkBuffer.STRING, value.author); + buffer.write(NetworkBuffer.VAR_INT, value.generation); + buffer.writeCollection(FilteredText.COMPONENT_NETWORK_TYPE, value.pages); + buffer.write(NetworkBuffer.BOOLEAN, value.resolved); + } + + @Override + public WrittenBookContent read(@NotNull NetworkBuffer buffer) { + FilteredText title = buffer.read(FilteredText.STRING_NETWORK_TYPE); + String author = buffer.read(NetworkBuffer.STRING); + int generation = buffer.read(NetworkBuffer.VAR_INT); + List> pages = buffer.readCollection(FilteredText.COMPONENT_NETWORK_TYPE, 100); + boolean resolved = buffer.read(NetworkBuffer.BOOLEAN); + return new WrittenBookContent(pages, title, author, generation, resolved); + } + }; + + public static final @NotNull BinaryTagSerializer NBT_TYPE = BinaryTagSerializer.COMPOUND.map( + compound -> { + ListBinaryTag pagesTag = compound.getList("pages"); + List> pages = pagesTag.stream() + .map(FilteredText.COMPONENT_NBT_TYPE::read) + .toList(); + FilteredText title = FilteredText.STRING_NBT_TYPE.read(compound.get("title")); + String author = compound.getString("author"); + int generation = compound.getInt("generation"); + boolean resolved = compound.getBoolean("resolved"); + return new WrittenBookContent(pages, title, author, generation, resolved); + }, + value -> { + ListBinaryTag.Builder pagesTag = ListBinaryTag.builder(); + for (FilteredText page : value.pages) { + pagesTag.add(FilteredText.COMPONENT_NBT_TYPE.write(page)); + } + return CompoundBinaryTag.builder() + .put("pages", pagesTag.build()) + .put("title", FilteredText.STRING_NBT_TYPE.write(value.title)) + .putString("author", value.author) + .putInt("generation", value.generation) + .putBoolean("resolved", value.resolved) + .build(); + } + ); + + public WrittenBookContent { + pages = List.copyOf(pages); + } + + public WrittenBookContent(@NotNull List pages, @NotNull String title, @NotNull String author, int generation, boolean resolved) { + this(pages.stream().map(page -> new FilteredText<>(page, null)).toList(), new FilteredText<>(title, null), author, generation, resolved); + } +} diff --git a/src/main/java/net/minestom/server/utils/nbt/BinaryTagUtil.java b/src/main/java/net/minestom/server/utils/nbt/BinaryTagUtil.java index d61c1074f..d61f07235 100644 --- a/src/main/java/net/minestom/server/utils/nbt/BinaryTagUtil.java +++ b/src/main/java/net/minestom/server/utils/nbt/BinaryTagUtil.java @@ -4,6 +4,7 @@ import net.kyori.adventure.nbt.*; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public final class BinaryTagUtil { @@ -54,6 +55,10 @@ public final class BinaryTagUtil { } } + public static @Nullable String getStringOrNull(@NotNull CompoundBinaryTag tag, @NotNull String key) { + return tag.keySet().contains(key) ? tag.getString(key) : null; + } + private BinaryTagUtil() { } }