diff --git a/paper-api/src/main/java/org/bukkit/entity/Player.java b/paper-api/src/main/java/org/bukkit/entity/Player.java index 25c677958228..3eef8d533c2a 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Player.java +++ b/paper-api/src/main/java/org/bukkit/entity/Player.java @@ -3490,11 +3490,12 @@ default void setNoTickViewDistance(int viewDistance) { public void updateCommands(); /** - * Open a {@link Material#WRITTEN_BOOK} for a Player + * Open an ItemStack with {@link io.papermc.paper.datacomponent.DataComponentTypes#WRITTEN_BOOK_CONTENT} for a Player * - * @param book The book to open for this player + * @param book the item with written book content to open for this player + * @throws IllegalArgumentException if the ItemStack is null, empty or doesn't have a {@link io.papermc.paper.datacomponent.DataComponentTypes#WRITTEN_BOOK_CONTENT} */ - public void openBook(ItemStack book); + void openBook(ItemStack book); /** * Open a Sign for editing by the Player. diff --git a/paper-server/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/paper-server/src/main/java/io/papermc/paper/adventure/PaperAdventure.java index e990a0f5027f..efcf79712be2 100644 --- a/paper-server/src/main/java/io/papermc/paper/adventure/PaperAdventure.java +++ b/paper-server/src/main/java/io/papermc/paper/adventure/PaperAdventure.java @@ -17,7 +17,6 @@ import java.util.regex.Pattern; import java.util.stream.StreamSupport; import net.kyori.adventure.bossbar.BossBar; -import net.kyori.adventure.inventory.Book; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.sound.Sound; @@ -41,7 +40,6 @@ import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponentType; -import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.locale.Language; import net.minecraft.nbt.CompoundTag; @@ -55,13 +53,10 @@ import net.minecraft.resources.RegistryOps; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.network.Filterable; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; import net.minecraft.world.BossEvent; import net.minecraft.world.entity.Entity; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.component.WrittenBookContent; import org.bukkit.command.CommandSender; import org.bukkit.craftbukkit.CraftRegistry; import org.bukkit.craftbukkit.command.VanillaCommandWrapper; @@ -333,28 +328,6 @@ public static void setFlag(final BossBar bar, final BossBar.Flag flag, final boo } } - // Book - - public static ItemStack asItemStack(final Book book, final Locale locale) { - final ItemStack item = new ItemStack(net.minecraft.world.item.Items.WRITTEN_BOOK, 1); - item.set(DataComponents.WRITTEN_BOOK_CONTENT, new WrittenBookContent( - Filterable.passThrough(validateField(asPlain(book.title(), locale), WrittenBookContent.TITLE_MAX_LENGTH, "title")), - asPlain(book.author(), locale), - 0, - book.pages().stream().map(c -> Filterable.passThrough(PaperAdventure.asVanilla(c))).toList(), // TODO should we validate length? - false - )); - return item; - } - - private static String validateField(final String content, final int length, final String name) { - final int actual = content.length(); - if (actual > length) { - throw new IllegalArgumentException("Field '" + name + "' has a maximum length of " + length + " but was passed '" + content + "', which was " + actual + " characters long."); - } - return content; - } - // Sounds public static SoundSource asVanilla(final Sound.Source source) { diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 975bc2bf7c19..1b36e0b25c68 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -11,6 +11,8 @@ import io.papermc.paper.FeatureHooks; import io.papermc.paper.connection.PlayerGameConnection; import io.papermc.paper.connection.PluginMessageBridgeImpl; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.WrittenBookContent; import io.papermc.paper.dialog.Dialog; import io.papermc.paper.dialog.PaperDialog; import io.papermc.paper.entity.LookAnchor; @@ -48,6 +50,7 @@ import java.util.stream.Collectors; import net.kyori.adventure.dialog.DialogLike; import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.inventory.Book; import net.kyori.adventure.pointer.PointersSupplier; import net.kyori.adventure.util.TriState; import net.md_5.bungee.api.chat.BaseComponent; @@ -69,6 +72,7 @@ import net.minecraft.network.protocol.game.ClientboundBlockDestructionPacket; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundBundlePacket; import net.minecraft.network.protocol.game.ClientboundClearTitlesPacket; import net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket; import net.minecraft.network.protocol.game.ClientboundGameEventPacket; @@ -76,6 +80,7 @@ import net.minecraft.network.protocol.game.ClientboundLevelEventPacket; import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket; +import net.minecraft.network.protocol.game.ClientboundOpenBookPacket; import net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket; import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; @@ -90,6 +95,7 @@ import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; import net.minecraft.network.protocol.game.ClientboundSetExperiencePacket; import net.minecraft.network.protocol.game.ClientboundSetHealthPacket; +import net.minecraft.network.protocol.game.ClientboundSetPlayerInventoryPacket; import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket; import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket; import net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket; @@ -108,6 +114,7 @@ import net.minecraft.server.players.UserWhiteListEntry; import net.minecraft.sounds.SoundEvent; import net.minecraft.util.ProblemReporter; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.ai.attributes.AttributeInstance; @@ -202,6 +209,7 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.InventoryView.Property; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; import org.bukkit.map.MapCursor; import org.bukkit.map.MapView; import org.bukkit.metadata.MetadataValue; @@ -2824,17 +2832,6 @@ public void updateCommands() { this.server.getServer().getCommands().sendCommands(this.getHandle()); } - @Override - public void openBook(ItemStack book) { - Preconditions.checkArgument(book != null, "ItemStack cannot be null"); - Preconditions.checkArgument(book.getType() == Material.WRITTEN_BOOK, "ItemStack Material (%s) must be Material.WRITTEN_BOOK", book.getType()); - - ItemStack hand = this.getInventory().getItemInMainHand(); - this.getInventory().setItemInMainHand(book); - this.getHandle().openItemGui(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(book), net.minecraft.world.InteractionHand.MAIN_HAND); - this.getInventory().setItemInMainHand(hand); - } - @Override public void openSign(@NonNull Sign sign, @NonNull Side side) { CraftSign.openSign(sign, this, side); @@ -3073,17 +3070,30 @@ public void stopSound(final net.kyori.adventure.sound.SoundStop stop) { } @Override - public void openBook(final net.kyori.adventure.inventory.Book book) { - final java.util.Locale locale = this.getHandle().adventure$locale; - final net.minecraft.world.item.ItemStack item = io.papermc.paper.adventure.PaperAdventure.asItemStack(book, locale); - final ServerPlayer player = this.getHandle(); - final ServerGamePacketListenerImpl connection = player.connection; - final net.minecraft.world.entity.player.Inventory inventory = player.getInventory(); - final int slot = inventory.getNonEquipmentItems().size() + inventory.getSelectedSlot(); - final int stateId = getHandle().containerMenu.getStateId(); - connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(0, stateId, slot, item)); - connection.send(new net.minecraft.network.protocol.game.ClientboundOpenBookPacket(net.minecraft.world.InteractionHand.MAIN_HAND)); - connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(0, stateId, slot, inventory.getSelectedItem())); + public void openBook(ItemStack book) { + Preconditions.checkArgument(book != null, "ItemStack cannot be null"); + Preconditions.checkArgument(book.hasData(DataComponentTypes.WRITTEN_BOOK_CONTENT), "ItemStack must have a 'written_book_content' component"); + + final ItemStack previousItem = this.getInventory().getItemInMainHand(); + this.getInventory().setItemInMainHand(book); + this.getHandle().openItemGui(CraftItemStack.asNMSCopy(book), InteractionHand.MAIN_HAND); + this.getInventory().setItemInMainHand(previousItem); + } + + @Override + public void openBook(final Book book) { + final ItemStack mutatedItem = ItemType.WRITTEN_BOOK.createItemStack(); // dummy item + mutatedItem.setData(DataComponentTypes.WRITTEN_BOOK_CONTENT, WrittenBookContent.writtenBookContent("", "").addPages(book.pages())); + + final net.minecraft.world.item.ItemStack selectedItem = this.getHandle().getInventory().getSelectedItem(); + final int slot = this.getHandle().getInventory().getSelectedSlot(); + this.getHandle().connection.send(new ClientboundBundlePacket( + List.of( + new ClientboundSetPlayerInventoryPacket(slot, CraftItemStack.unwrap(mutatedItem)), + new ClientboundOpenBookPacket(InteractionHand.MAIN_HAND), + new ClientboundSetPlayerInventoryPacket(slot, selectedItem) + ) + )); } @Override