diff --git a/README.md b/README.md index 122ae35f1..3c1e18a81 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here! ## Supported Versions -Geyser is currently supporting Minecraft Bedrock 1.21.50 - 1.21.70 and Minecraft Java 1.21.5. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.21.50 - 1.21.71 and Minecraft Java 1.21.5. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). ## Setting Up Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser. diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index c83fce4c4..b6626aa6a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -98,7 +98,7 @@ public abstract class PackCodec { * @since 2.1.1 */ @NonNull - public static PathPackCodec path(@NonNull Path path) { + public static PackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } @@ -110,7 +110,7 @@ public abstract class PackCodec { * @since 2.6.2 */ @NonNull - public static UrlPackCodec url(@NonNull String url) { + public static PackCodec url(@NonNull String url) { return GeyserApi.api().provider(UrlPackCodec.class, url); } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java index 9903d0d2e..da66b32c3 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java @@ -87,7 +87,7 @@ public class GeyserModLogger implements GeyserLogger { @Override public void debug(String message, Object... arguments) { if (debug) { - logger.info(message, arguments); + logger.info(String.format(message, arguments)); } } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java index 510ac0e79..8739add8a 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java @@ -117,7 +117,8 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey @Override public void debug(String message, Object... arguments) { - log.debug(ChatColor.GRAY + message, arguments); + // We can't use the debug call that would format for us as we're using Java's string formatting + log.debug(ChatColor.GRAY + String.format(message, arguments)); } @Override diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java index 5155d8958..0331b825e 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java @@ -77,7 +77,7 @@ public class GeyserVelocityLogger implements GeyserLogger { @Override public void debug(String message, Object... arguments) { if (debug) { - logger.info(message, arguments); + logger.info(String.format(message, arguments)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index 92b50751a..5452d6d29 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -29,6 +29,7 @@ import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; import java.util.UUID; @@ -119,6 +120,15 @@ public interface GeyserLogger extends GeyserCommandSource { */ void setDebug(boolean debug); + /** + * A method to debug information specific to a session. + */ + default void debug(GeyserSession session, String message, Object... arguments) { + if (isDebug()) { + debug("(" + session.bedrockUsername() + ") " + message, arguments); + } + } + /** * If debug is enabled for this logger */ diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java index 75b9252da..2de7d91e0 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java @@ -51,6 +51,6 @@ public class AdvancedTooltipsCommand extends GeyserCommand { + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + " " + ChatColor.RESET + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale())); - session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); + session.getPlayerInventory().updateInventory(); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 03cb0b6e1..667b4190b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -33,6 +33,7 @@ import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; @@ -86,8 +87,10 @@ public class BoatEntity extends Entity implements Leashable, Tickable { @Override protected void initializeMetadata() { super.initializeMetadata(); - // Without this flag you cant stand on boats - setFlag(EntityFlag.COLLIDABLE, true); + if (GameProtocol.is1_21_70orHigher(session)) { + // Without this flag you cant stand on boats + setFlag(EntityFlag.COLLIDABLE, true); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java index fe760c75a..08c64d267 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java @@ -30,6 +30,7 @@ import lombok.Setter; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; @@ -62,8 +63,8 @@ public class AnvilContainer extends Container { private int lastTargetSlot = -1; - public AnvilContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public AnvilContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } /** diff --git a/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java index 1b59772fa..1869c474e 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.inventory; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import lombok.Getter; import lombok.Setter; @@ -35,7 +37,7 @@ public class BeaconContainer extends Container { private int primaryId; private int secondaryId; - public BeaconContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public BeaconContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java index ace3f93ad..71c400643 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java @@ -25,10 +25,12 @@ package org.geysermc.geyser.inventory; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; public class CartographyContainer extends Container { - public CartographyContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public CartographyContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Container.java index f2db415c0..0a7a68bdb 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Container.java @@ -46,8 +46,8 @@ public class Container extends Inventory { */ private boolean isUsingRealBlock = false; - public Container(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType); + public Container(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, translator); this.playerInventory = playerInventory; this.containerSize = this.size + InventoryTranslator.PLAYER_INVENTORY_SIZE; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java index fb118252d..f9439cc69 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java @@ -48,8 +48,8 @@ public class CrafterContainer extends Container { */ private short disabledSlotsMask = 0; - public CrafterContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public CrafterContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java index 08397ab44..1e4cb4355 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java @@ -25,24 +25,25 @@ package org.geysermc.geyser.inventory; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.data.inventory.EnchantOptionData; import lombok.Getter; +@Getter public class EnchantingContainer extends Container { /** * A cache of what Bedrock sees */ - @Getter private final EnchantOptionData[] enchantOptions; /** * A mutable cache of what the server sends us */ - @Getter private final GeyserEnchantOption[] geyserEnchantOptions; - public EnchantingContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public EnchantingContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); enchantOptions = new EnchantOptionData[3]; geyserEnchantOptions = new GeyserEnchantOption[3]; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java index 0b14d1105..2e1869f80 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java @@ -30,19 +30,20 @@ import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.Generic3X3InventoryTranslator; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Getter public class Generic3X3Container extends Container { /** * Whether we need to set the container type as {@link org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType#DROPPER}. *

* Used at {@link Generic3X3InventoryTranslator#openInventory(GeyserSession, Inventory)} */ - @Getter private boolean isDropper = false; - public Generic3X3Container(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public Generic3X3Container(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java index 2c0c2798d..5e49baac1 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java @@ -32,8 +32,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.inventory.click.ClickPlan; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; @@ -46,6 +48,10 @@ public abstract class Inventory { @Getter protected final int javaId; + @Setter + @Getter + private int bedrockId; + /** * The Java inventory state ID from the server. As of Java Edition 1.18.1 this value has one instance per player. * If this is out of sync with the server when a packet containing it is handled, the server will resync items. @@ -55,7 +61,7 @@ public abstract class Inventory { @Setter private int stateId; /** - * See {@link org.geysermc.geyser.inventory.click.ClickPlan#execute(boolean)}; used as a hack + * See {@link ClickPlan#execute(boolean)}; used as a hack */ @Getter private int nextStateId = -1; @@ -75,43 +81,72 @@ public abstract class Inventory { protected final GeyserItemStack[] items; /** - * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location + * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location. */ @Getter @Setter protected Vector3i holderPosition = Vector3i.ZERO; + /** + * The entity id of the entity holding the inventory. + * Either this, or the holder position must be set in order for Bedrock to open inventories. + */ @Getter @Setter protected long holderId = -1; + /** + * Whether this inventory is currently pending. + * It can be pending if this inventory was opened while another inventory was still open, + * or because opening this inventory takes more time (e.g. virtual inventories). + */ @Getter @Setter private boolean pending = false; + /** + * Whether this inventory is currently shown to the Bedrock player. + */ @Getter @Setter private boolean displayed = false; - protected Inventory(int id, int size, ContainerType containerType) { - this("Inventory", id, size, containerType); + /** + * The translator for this inventory. Stored here to avoid de-syncs of the inventory and current translator. + */ + @Getter + private final InventoryTranslator translator; + + @Getter + private final GeyserSession session; + + protected Inventory(GeyserSession session, int id, int size, ContainerType containerType, InventoryTranslator translator) { + this(session, "Inventory", id, size, containerType, translator); } - protected Inventory(String title, int javaId, int size, ContainerType containerType) { + protected Inventory(GeyserSession session, String title, int javaId, int size, ContainerType containerType, InventoryTranslator translator) { this.title = title; this.javaId = javaId; this.size = size; this.containerType = containerType; this.items = new GeyserItemStack[size]; Arrays.fill(items, GeyserItemStack.EMPTY); - } + this.translator = translator; + this.session = session; - // This is to prevent conflicts with special bedrock inventory IDs. - // The vanilla java server only sends an ID between 1 and 100 when opening an inventory, - // so this is rarely needed. (certain plugins) - // Example: https://github.com/GeyserMC/Geyser/issues/3254 - public int getBedrockId() { - return javaId <= 100 ? javaId : (javaId % 100) + 1; + // This is to prevent conflicts with special bedrock inventory IDs. + // The vanilla java server only sends an ID between 1 and 100 when opening an inventory, + // so this is rarely needed. (certain plugins) + // Example: https://github.com/GeyserMC/Geyser/issues/3254 + this.bedrockId = javaId <= 100 ? javaId : (javaId % 100) + 1; + + // We occasionally need to re-open inventories with a delay in cases where + // Java wouldn't - e.g. for virtual chest menus that switch pages. + // And, well, we want to avoid reusing Bedrock inventory id's that are currently being used in a closing inventory; + // so to be safe we just deviate in that case as well. + if ((session.getOpenInventory() != null && session.getOpenInventory().getBedrockId() == bedrockId) || session.isClosingInventory()) { + this.bedrockId += 1; + } } public GeyserItemStack getItem(int slot) { @@ -179,4 +214,20 @@ public abstract class Inventory { public boolean shouldConfirmContainerClose() { return true; } + + /* + * Helper methods to avoid using the wrong translator to update specific inventories. + */ + + public void updateInventory() { + this.translator.updateInventory(session, this); + } + + public void updateProperty(int rawProperty, int value) { + this.translator.updateProperty(session, this, rawProperty, value); + } + + public void updateSlot(int slot) { + this.translator.updateSlot(session, this, slot); + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java index 9988188f1..c82b666f7 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java @@ -31,6 +31,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.protocol.java.inventory.JavaOpenBookTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; @@ -45,8 +46,8 @@ public class LecternContainer extends Container { private boolean isBookInPlayerInventory = false; - public LecternContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public LecternContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } /** diff --git a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java index 156d5e691..d3a149364 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java @@ -30,6 +30,7 @@ import lombok.Setter; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.VillagerTrade; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundMerchantOffersPacket; @@ -46,8 +47,8 @@ public class MerchantContainer extends Container { @Getter private int tradeExperience; - public MerchantContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public MerchantContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } public void onTradeSelected(GeyserSession session, int slot) { diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java index 3ea9cd112..f3f68cb9e 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java @@ -31,24 +31,24 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.jetbrains.annotations.Range; +@Getter public class PlayerInventory extends Inventory { /** * Stores the held item slot, starting at index 0. * Add 36 in order to get the network item slot. */ - @Getter @Setter private int heldItemSlot; - @Getter @NonNull private GeyserItemStack cursor = GeyserItemStack.EMPTY; - public PlayerInventory() { - super(0, 46, null); + public PlayerInventory(GeyserSession session) { + super(session, 0, 46, null, InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); heldItemSlot = 0; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java index 1eb115847..1f5f8f4b8 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java @@ -29,18 +29,19 @@ import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Setter +@Getter public class StonecutterContainer extends Container { /** * The button that has currently been pressed Java-side */ - @Getter - @Setter private int stonecutterButton = -1; - public StonecutterContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public StonecutterContainer(GeyserSession session, String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory, InventoryTranslator translator) { + super(session, title, id, size, containerType, playerInventory, translator); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java index 1bf30bc7e..8c905022b 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java @@ -40,11 +40,11 @@ import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.util.InventoryUtils; import java.util.Collections; import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -77,24 +77,29 @@ public class BlockInventoryHolder extends InventoryHolder { } @Override - public boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { - // Check to see if there is an existing block we can use that the player just selected. - // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. - // (This could be a virtual inventory that the player is opening) - if (checkInteractionPosition(session)) { - // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid - // and the bedrock block is vanilla - BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); - if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { - if (isValidBlock(state)) { - // We can safely use this block - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, state.block()); - setCustomName(session, session.getLastInteractionBlockPosition(), inventory, state); + public boolean canReuseContainer(GeyserSession session, Container container, Container previous) { + // We already ensured that the inventories are using the same type, size, and title - return true; - } - } + // TODO this would currently break, so we're not reusing this + if (previous.isUsingRealBlock()) { + return false; + } + + // Check if we'd be using the same virtual inventory position. + Vector3i position = InventoryUtils.findAvailableWorldSpace(session); + if (Objects.equals(position, previous.getHolderPosition())) { + return true; + } else { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory (%s) due to virtual block holder changing (%s -> %s)!", + InventoryUtils.debugInventory(container), previous.getHolderPosition(), position); + return false; + } + } + + @Override + public boolean prepareInventory(GeyserSession session, Container inventory) { + if (canUseRealBlock(session, inventory)) { + return true; } Vector3i position = InventoryUtils.findAvailableWorldSpace(session); @@ -115,6 +120,29 @@ public class BlockInventoryHolder extends InventoryHolder { return true; } + protected boolean canUseRealBlock(GeyserSession session, Container inventory) { + // Check to see if there is an existing block we can use that the player just selected. + // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. + // (This could be a virtual inventory that the player is opening) + if (checkInteractionPosition(session)) { + // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid + // and the bedrock block is vanilla + BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { + if (isValidBlock(state)) { + // We can safely use this block + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + inventory.setUsingRealBlock(true, state.block()); + setCustomName(session, session.getLastInteractionBlockPosition(), inventory, state); + + return true; + } + } + } + + return false; + } + /** * Will be overwritten in the beacon inventory translator to remove the check, since virtual inventories can't exist. * @@ -145,57 +173,49 @@ public class BlockInventoryHolder extends InventoryHolder { } @Override - public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void openInventory(GeyserSession session, Container container) { ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket(); - containerOpenPacket.setId((byte) inventory.getBedrockId()); + containerOpenPacket.setId((byte) container.getBedrockId()); containerOpenPacket.setType(containerType); - containerOpenPacket.setBlockPosition(inventory.getHolderPosition()); - containerOpenPacket.setUniqueEntityId(inventory.getHolderId()); + containerOpenPacket.setBlockPosition(container.getHolderPosition()); + containerOpenPacket.setUniqueEntityId(container.getHolderId()); session.sendUpstreamPacket(containerOpenPacket); + + GeyserImpl.getInstance().getLogger().debug(session, containerOpenPacket.toString()); } @Override - public void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory, ContainerType type) { - if (!(inventory instanceof Container container)) { - GeyserImpl.getInstance().getLogger().warning("Tried to close a non-container inventory in a block inventory holder! Please report this error on discord."); - GeyserImpl.getInstance().getLogger().warning("Current inventory translator: " + translator.getClass().getSimpleName()); - GeyserImpl.getInstance().getLogger().warning("Current inventory: " + inventory.getClass().getSimpleName()); - // Try to save ourselves? maybe? - // https://github.com/GeyserMC/Geyser/issues/4141 - // TODO: improve once this issue is pinned down - session.setOpenInventory(null); - session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); - return; - } + public void closeInventory(GeyserSession session, Container container, ContainerType type) { + if (container.isDisplayed() && !(container instanceof LecternContainer)) { + // No need to reset a block since we didn't change any blocks + // But send a container close packet because we aren't destroying the original. + ContainerClosePacket packet = new ContainerClosePacket(); + packet.setId((byte) container.getBedrockId()); + packet.setServerInitiated(true); + packet.setType(type != null ? type : containerType); + session.sendUpstreamPacket(packet); - // Bedrock broke inventory closing. I wish i was kidding. - // "type" is explicitly passed to keep track of which inventory types can be closed without - // ""workarounds"". yippie. - // Further, Lecterns cannot be closed with any of the two methods below. - if (container.isUsingRealBlock() && !(container instanceof LecternContainer)) { - if (type != null) { - // No need to reset a block since we didn't change any blocks - // But send a container close packet because we aren't destroying the original. - ContainerClosePacket packet = new ContainerClosePacket(); - packet.setId((byte) inventory.getBedrockId()); - packet.setServerInitiated(true); - packet.setType(type); - session.sendUpstreamPacket(packet); - return; + if (container.isUsingRealBlock()) { + // Type being null indicates that the ContainerClosePacket is not effective. + // So we yeet away the block! + if (type == null) { + Vector3i holderPos = container.getHolderPosition(); + UpdateBlockPacket blockPacket = new UpdateBlockPacket(); + blockPacket.setDataLayer(0); + blockPacket.setBlockPosition(holderPos); + blockPacket.setDefinition(session.getBlockMappings().getBedrockAir()); + blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(blockPacket); + } else { + // We're using a real block and are able to close the block without destroying it, + // so we can don't need to reset it below. + return; + } } - - // Destroy the block. There's no inventory to view => it gets closed! - Vector3i holderPos = inventory.getHolderPosition(); - UpdateBlockPacket blockPacket = new UpdateBlockPacket(); - blockPacket.setDataLayer(0); - blockPacket.setBlockPosition(holderPos); - blockPacket.setDefinition(session.getBlockMappings().getBedrockAir()); - blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); - session.sendUpstreamPacket(blockPacket); } // Reset to correct block - Vector3i holderPos = inventory.getHolderPosition(); + Vector3i holderPos = container.getHolderPosition(); int realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos.getX(), holderPos.getY(), holderPos.getZ()); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java index d61193c7a..6cce5b4a9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java @@ -26,12 +26,12 @@ package org.geysermc.geyser.inventory.holder; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; -import org.geysermc.geyser.inventory.Inventory; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; public abstract class InventoryHolder { - public abstract boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); - public abstract void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); - public abstract void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory, ContainerType containerType); + public abstract boolean canReuseContainer(GeyserSession session, Container inventory, Container oldInventory); + public abstract boolean prepareInventory(GeyserSession session, Container inventory); + public abstract void openInventory(GeyserSession session, Container inventory); + public abstract void closeInventory(GeyserSession session, Container inventory, ContainerType containerType); } diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 95452486b..a3e2ac6ac 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -98,6 +98,10 @@ public final class GameProtocol { return protocolVersion < 776; } + public static boolean is1_21_70orHigher(GeyserSession session) { + return session.protocolVersion() >= Bedrock_v786.CODEC.getProtocolVersion(); + } + /** * Gets the {@link PacketCodec} for Minecraft: Java Edition. * diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 096ea4353..e5f71ce05 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -175,7 +175,6 @@ import org.geysermc.geyser.session.cache.WorldBorder; import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.EntityUtils; @@ -292,13 +291,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private boolean isInWorldBorderWarningArea = false; private final PlayerInventory playerInventory; + @Setter private @Nullable Inventory openInventory; + @Setter private boolean closingInventory; + /** + * Stores the bedrock inventory id of the pending inventory, or -1 if no inventory is pending. + * This id is only set when the block that should be opened exists. + */ @Setter - private @NonNull InventoryTranslator inventoryTranslator = InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR; + private int pendingOrCurrentBedrockInventoryId = -1; /** * Use {@link #getNextItemNetId()} instead for consistency @@ -676,6 +681,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private int stepTicks = 0; + /* + * Stores the number of attempts to open virtual inventories. + * Capped at 3, and isn't used in ideal circumstances. + * Used to resolve https://github.com/GeyserMC/Geyser/issues/5426 + */ + @Setter + private int containerOpenAttempts; + public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop tickEventLoop) { this.geyser = geyser; @@ -710,7 +723,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { this.playerEntity = new SessionPlayerEntity(this); collisionManager.updatePlayerBoundingBox(this.playerEntity.getPosition()); - this.playerInventory = new PlayerInventory(); + this.playerInventory = new PlayerInventory(this); this.openInventory = null; this.craftingRecipes = new Int2ObjectOpenHashMap<>(); this.javaToBedrockRecipeIds = new Int2ObjectOpenHashMap<>(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/FormCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/FormCache.java index 3f7df97c1..2f5d5e517 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/FormCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/FormCache.java @@ -25,23 +25,32 @@ package org.geysermc.geyser.session.cache; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.RequiredArgsConstructor; import org.cloudburstmc.protocol.bedrock.packet.ModalFormRequestPacket; import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import lombok.RequiredArgsConstructor; +import org.geysermc.cumulus.component.util.ComponentType; +import org.geysermc.cumulus.form.CustomForm; import org.geysermc.cumulus.form.Form; import org.geysermc.cumulus.form.SimpleForm; import org.geysermc.cumulus.form.impl.FormDefinitions; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - @RequiredArgsConstructor public class FormCache { + private static final Gson GSON_TEMP = new Gson(); /** * The magnitude of this doesn't actually matter, but it must be negative so that @@ -100,9 +109,52 @@ public class FormCache { return; } + String responseData = response.getFormData(); + //todo work on a proper solution in Cumulus, but that'd require all Floodgate instances to update as well and + // drops support for older Bedrock versions (because Cumulus isn't made to support multiple versions). That's + // why this hotfix exists. + if (form instanceof CustomForm customForm && GameProtocol.is1_21_70orHigher(session)) { + // Labels are no longer included as a json null, so we have to manually add them for now. + IntList labelIndexes = new IntArrayList(); + for (int i = 0; i < customForm.content().size(); i++) { + var component = customForm.content().get(i); + if (component == null) { + continue; + } + if (component.type() == ComponentType.LABEL) { + labelIndexes.add(i); + } + } + if (!labelIndexes.isEmpty()) { + // If the form only has labels, the response is the literal + // null (with a newline char) instead of a json array + if (responseData.startsWith("null")) { + List newResponse = new ArrayList<>(); + for (int i = 0; i < labelIndexes.size(); i++) { + newResponse.add(null); + } + responseData = GSON_TEMP.toJson(newResponse); + } else { + JsonArray responseDataArray = GSON_TEMP.fromJson(responseData, JsonArray.class); + List newResponse = new ArrayList<>(); + + int handledLabelCount = 0; + for (int i = 0; i < responseDataArray.size() + labelIndexes.size(); i++) { + if (labelIndexes.contains(i)) { + newResponse.add(null); + handledLabelCount++; + continue; + } + newResponse.add(responseDataArray.get(i - handledLabelCount)); + } + responseData = GSON_TEMP.toJson(newResponse); + } + } + } + try { formDefinitions.definitionFor(form) - .handleFormResponse(form, response.getFormData()); + .handleFormResponse(form, responseData); } catch (Exception e) { GeyserImpl.getInstance().getLogger().error("Error while processing form response!", e); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/AbstractBlockInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/AbstractBlockInventoryTranslator.java index 0cd60987d..50a6f0e4d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/AbstractBlockInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/AbstractBlockInventoryTranslator.java @@ -25,8 +25,10 @@ package org.geysermc.geyser.translator.inventory; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.holder.BlockInventoryHolder; import org.geysermc.geyser.inventory.holder.InventoryHolder; @@ -75,19 +77,34 @@ public abstract class AbstractBlockInventoryTranslator extends BaseInventoryTran this.updater = updater; } + @Override + public boolean requiresOpeningDelay(GeyserSession session, Inventory inventory) { + return inventory instanceof Container container && !container.isUsingRealBlock(); + } + + @Override + public boolean canReuseInventory(GeyserSession session, @NonNull Inventory inventory, @NonNull Inventory previous) { + if (super.canReuseInventory(session, inventory, previous) + && inventory instanceof Container container + && previous instanceof Container previousContainer) { + return holder.canReuseContainer(session, container, previousContainer); + } + return false; + } + @Override public boolean prepareInventory(GeyserSession session, Inventory inventory) { - return holder.prepareInventory(this, session, inventory); + return holder.prepareInventory(session, (Container) inventory); } @Override public void openInventory(GeyserSession session, Inventory inventory) { - holder.openInventory(this, session, inventory); + holder.openInventory(session, (Container) inventory); } @Override public void closeInventory(GeyserSession session, Inventory inventory) { - holder.closeInventory(this, session, inventory, closeContainerType(inventory)); + holder.closeInventory(session, (Container) inventory, closeContainerType(inventory)); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/AnvilInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/AnvilInventoryTranslator.java index f43d1301b..70022c4f4 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/AnvilInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/AnvilInventoryTranslator.java @@ -103,8 +103,8 @@ public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator { } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new AnvilContainer(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new AnvilContainer(session, name, windowId, this.size, containerType, playerInventory, this); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BaseInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BaseInventoryTranslator.java index fd6d9a930..6ec682bb8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BaseInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BaseInventoryTranslator.java @@ -90,7 +90,7 @@ public abstract class BaseInventoryTranslator extends InventoryTranslator { } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new Container(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new Container(session, name, windowId, this.size, containerType, playerInventory, this); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java index 2b731000d..5e1992094 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java @@ -38,6 +38,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemS import org.cloudburstmc.protocol.bedrock.packet.BlockEntityDataPacket; import org.geysermc.geyser.inventory.BeaconContainer; import org.geysermc.geyser.inventory.BedrockContainerSlot; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.holder.BlockInventoryHolder; @@ -61,12 +62,12 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator } @Override - public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { - if (!((BeaconContainer) inventory).isUsingRealBlock()) { - InventoryUtils.closeInventory(session, inventory.getJavaId(), false); + public void openInventory(GeyserSession session, Container container) { + if (!container.isUsingRealBlock()) { + InventoryUtils.closeInventory(session, container.getJavaId(), false); return; } - super.openInventory(translator, session, inventory); + super.openInventory(session, container); } }, UIInventoryUpdater.INSTANCE); } @@ -144,8 +145,8 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new BeaconContainer(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new BeaconContainer(session, name, windowId, this.size, containerType, playerInventory, this); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/CartographyInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/CartographyInventoryTranslator.java index 2cb4d0f14..20e91ba49 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/CartographyInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/CartographyInventoryTranslator.java @@ -86,8 +86,8 @@ public class CartographyInventoryTranslator extends AbstractBlockInventoryTransl } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new CartographyContainer(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new CartographyContainer(session, name, windowId, this.size, containerType, playerInventory, this); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/CrafterInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/CrafterInventoryTranslator.java index 390c0d6e6..81e000c02 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/CrafterInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/CrafterInventoryTranslator.java @@ -139,9 +139,9 @@ public class CrafterInventoryTranslator extends AbstractBlockInventoryTranslator } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { // Java sends the triggered and slot bits incrementally through properties, which we store here - return new CrafterContainer(name, windowId, this.size, containerType, playerInventory); + return new CrafterContainer(session, name, windowId, this.size, containerType, playerInventory, this); } private static void updateBlockEntity(GeyserSession session, CrafterContainer container) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java index 9b2404314..e9cf15134 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java @@ -169,8 +169,8 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new EnchantingContainer(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new EnchantingContainer(session, name, windowId, this.size, containerType, playerInventory, this); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/Generic3X3InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/Generic3X3InventoryTranslator.java index 0f90240d1..da0df0503 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/Generic3X3InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/Generic3X3InventoryTranslator.java @@ -46,8 +46,8 @@ public class Generic3X3InventoryTranslator extends AbstractBlockInventoryTransla } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new Generic3X3Container(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new Generic3X3Container(session, name, windowId, this.size, containerType, playerInventory, this); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 6394c5312..04182356b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -35,7 +35,9 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSortedSet; import lombok.AllArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; @@ -85,6 +87,7 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.geysermc.geyser.translator.inventory.BundleInventoryTranslator.isBundle; @@ -134,6 +137,41 @@ public abstract class InventoryTranslator { public final int size; + // Whether the inventory open should be delayed. + public boolean requiresOpeningDelay(GeyserSession session, Inventory inventory) { + return false; + } + + /** + * Whether a new inventory should be prepared - or if we can re-use the previous one. + */ + public boolean canReuseInventory(GeyserSession session, @NonNull Inventory inventory, @NonNull Inventory previous) { + // Filter for mismatches that require a new inventory. + if (inventory.getContainerType() == null || previous.getContainerType() == null + || !Objects.equals(inventory.getContainerType(), previous.getContainerType()) + ) { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory (%s) due to type change! ", InventoryUtils.debugInventory(inventory)); + return false; + } + + if (inventory.getSize() != previous.getSize()) { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory (%s) due to size change! ", InventoryUtils.debugInventory(inventory)); + return false; + } + + if (!Objects.equals(inventory.getTitle(), previous.getTitle())) { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory (%s) due to title change! ", InventoryUtils.debugInventory(inventory)); + return false; + } + + if (previous.getHolderId() == -1 && previous.getHolderPosition() == Vector3i.ZERO) { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory (%s) since the old was not initialized! ", InventoryUtils.debugInventory(inventory)); + return false; + } + + // We can likely reuse the inventory! + return true; + } public abstract boolean prepareInventory(GeyserSession session, Inventory inventory); public abstract void openInventory(GeyserSession session, Inventory inventory); public abstract void closeInventory(GeyserSession session, Inventory inventory); @@ -144,7 +182,7 @@ public abstract class InventoryTranslator { public abstract int javaSlotToBedrock(int javaSlot); public abstract BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot); public abstract SlotType getSlotType(int javaSlot); - public abstract Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory); + public abstract Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory); /** * Used for crafting-related transactions. Will override in PlayerInventoryTranslator and CraftingInventoryTranslator. diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java index f62bfbc7c..a73221170 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java @@ -199,7 +199,7 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new LecternContainer(name, windowId, this.size + playerInventory.getSize(), containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new LecternContainer(session, name, windowId, this.size + playerInventory.getSize(), containerType, playerInventory, this); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java index 4ac159981..d8cb24aee 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java @@ -195,7 +195,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator { } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new MerchantContainer(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new MerchantContainer(session, name, windowId, this.size, containerType, playerInventory, this); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java index 7064f1169..ae7483924 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.inventory; import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; +import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; @@ -569,10 +570,15 @@ public class PlayerInventoryTranslator extends InventoryTranslator { } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { throw new UnsupportedOperationException(); } + @Override + public boolean canReuseInventory(GeyserSession session, @NonNull Inventory inventory, @NonNull Inventory previous) { + return true; + } + @Override public boolean prepareInventory(GeyserSession session, Inventory inventory) { return true; diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java index 90ef13fd5..cfb3c1e52 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java @@ -123,8 +123,8 @@ public class StonecutterInventoryTranslator extends AbstractBlockInventoryTransl } @Override - public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { - return new StonecutterContainer(name, windowId, this.size, containerType, playerInventory); + public Inventory createInventory(GeyserSession session, String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { + return new StonecutterContainer(session, name, windowId, this.size, containerType, playerInventory, this); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/ChestInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/ChestInventoryTranslator.java index 070537b81..b42ad9f3c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/ChestInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/ChestInventoryTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.inventory.chest; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; import org.geysermc.geyser.inventory.BedrockContainerSlot; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.updater.ChestInventoryUpdater; import org.geysermc.geyser.inventory.updater.InventoryUpdater; @@ -51,6 +52,11 @@ public abstract class ChestInventoryTranslator extends BaseInventoryTranslator { return bedrockDestinationContainer == ContainerSlotType.LEVEL_ENTITY && javaDestinationSlot >= this.size; } + @Override + public boolean requiresOpeningDelay(GeyserSession session, Inventory inventory) { + return inventory instanceof Container container && !container.isUsingRealBlock(); + } + @Override public void updateInventory(GeyserSession session, Inventory inventory) { updater.updateInventory(this, session, inventory); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java index 93b9d8e04..9fe901d6c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.translator.inventory.chest; +import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -37,6 +38,7 @@ import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.inventory.Inventory; +import org.geysermc.geyser.inventory.holder.BlockInventoryHolder; import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.property.ChestType; import org.geysermc.geyser.level.block.property.Properties; @@ -44,11 +46,12 @@ import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator; import org.geysermc.geyser.translator.level.block.entity.DoubleChestBlockEntityTranslator; import org.geysermc.geyser.util.InventoryUtils; +import java.util.Objects; + public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { private final int defaultJavaBlockState; @@ -60,31 +63,39 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { .javaId(); } + /** + * Additional checks to verify that we can re-use the block inventory holder. + * Mirrors {@link BlockInventoryHolder#canReuseContainer(GeyserSession, Container, Container)} + */ + @Override + public boolean canReuseInventory(GeyserSession session, @NonNull Inventory inventory, @NonNull Inventory oldInventory) { + if (!super.canReuseInventory(session, inventory, oldInventory) || + !(inventory instanceof Container container) || + !(oldInventory instanceof Container previous) + ) { + return false; + } + + // FIXME - but these aren't the reason we have this + if (previous.isUsingRealBlock()) { + return false; + } + + // Check if we'd be using the same virtual inventory position. + Vector3i position = InventoryUtils.findAvailableWorldSpace(session); + if (Objects.equals(position, previous.getHolderPosition())) { + return true; + } else { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory (%s) due to virtual block holder changing (%s -> %s)!", + InventoryUtils.debugInventory(inventory), previous.getHolderPosition(), position); + return false; + } + } + @Override public boolean prepareInventory(GeyserSession session, Inventory inventory) { - // See BlockInventoryHolder - same concept there except we're also dealing with a specific block state - if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) { - BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); - if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { - if ((state.block() == Blocks.CHEST || state.block() == Blocks.TRAPPED_CHEST) - && state.getValue(Properties.CHEST_TYPE) != ChestType.SINGLE) { - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, state.block()); - - NbtMapBuilder tag = BlockEntityTranslator.getConstantBedrockTag("Chest", session.getLastInteractionBlockPosition()) - .putString("CustomName", inventory.getTitle()); - - DoubleChestBlockEntityTranslator.translateChestValue(tag, state, - session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); - - BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); - dataPacket.setData(tag.build()); - dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); - session.sendUpstreamPacket(dataPacket); - - return true; - } - } + if (canUseRealBlock(session, inventory)) { + return true; } Vector3i position = InventoryUtils.findAvailableWorldSpace(session); @@ -102,12 +113,14 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); - NbtMap tag = BlockEntityTranslator.getConstantBedrockTag("Chest", position) + NbtMapBuilder tag = BlockEntityTranslator.getConstantBedrockTag("Chest", position) .putInt("pairx", pairPosition.getX()) .putInt("pairz", pairPosition.getZ()) - .putString("CustomName", inventory.getTitle()).build(); + .putString("CustomName", inventory.getTitle()) + .putBoolean("pairlead", false); + BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); - dataPacket.setData(tag); + dataPacket.setData(tag.build()); dataPacket.setBlockPosition(position); session.sendUpstreamPacket(dataPacket); @@ -125,9 +138,11 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { .putInt("z", pairPosition.getZ()) .putInt("pairx", position.getX()) .putInt("pairz", position.getZ()) - .putString("CustomName", inventory.getTitle()).build(); + .putString("CustomName", inventory.getTitle()) + .putBoolean("pairlead", true); + dataPacket = new BlockEntityDataPacket(); - dataPacket.setData(tag); + dataPacket.setData(tag.build()); dataPacket.setBlockPosition(pairPosition); session.sendUpstreamPacket(dataPacket); @@ -144,47 +159,82 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { containerOpenPacket.setBlockPosition(inventory.getHolderPosition()); containerOpenPacket.setUniqueEntityId(inventory.getHolderId()); session.sendUpstreamPacket(containerOpenPacket); + + GeyserImpl.getInstance().getLogger().debug(session, containerOpenPacket.toString()); } @Override public void closeInventory(GeyserSession session, Inventory inventory) { + // this should no longer be possible; as we're storing the translator with the inventory to avoid desyncs. + // TODO use generics to ensure we don't need to cast unsafely in the first place if (!(inventory instanceof Container container)) { GeyserImpl.getInstance().getLogger().warning("Tried to close a non-container inventory in a block inventory holder! Please report this error on discord."); - GeyserImpl.getInstance().getLogger().warning("Current inventory translator: " + session.getInventoryTranslator().getClass().getSimpleName()); + GeyserImpl.getInstance().getLogger().warning("Current inventory translator: " + InventoryUtils.getInventoryTranslator(session).getClass().getSimpleName()); GeyserImpl.getInstance().getLogger().warning("Current inventory: " + inventory.getClass().getSimpleName()); // Try to save ourselves? maybe? // https://github.com/GeyserMC/Geyser/issues/4141 // TODO: improve once this issue is pinned down - session.setOpenInventory(null); - session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); + if (session.getOpenInventory() != null) { + session.getOpenInventory().getTranslator().closeInventory(session, inventory); + session.setOpenInventory(null); + } return; } - if (container.isUsingRealBlock()) { - // No need to reset a block since we didn't change any blocks - // But send a container close packet because we aren't destroying the original. + // No need to reset a block since we didn't change any blocks + // But send a container close packet because we aren't destroying the original. + if (container.isDisplayed()) { ContainerClosePacket packet = new ContainerClosePacket(); packet.setId((byte) inventory.getBedrockId()); packet.setServerInitiated(true); packet.setType(ContainerType.CONTAINER); session.sendUpstreamPacket(packet); - return; } - Vector3i holderPos = inventory.getHolderPosition(); - int realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos); - UpdateBlockPacket blockPacket = new UpdateBlockPacket(); - blockPacket.setDataLayer(0); - blockPacket.setBlockPosition(holderPos); - blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(realBlock)); - session.sendUpstreamPacket(blockPacket); + if (!container.isUsingRealBlock()) { + Vector3i holderPos = inventory.getHolderPosition(); + int realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos); + UpdateBlockPacket blockPacket = new UpdateBlockPacket(); + blockPacket.setDataLayer(0); + blockPacket.setBlockPosition(holderPos); + blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(realBlock)); + session.sendUpstreamPacket(blockPacket); - holderPos = holderPos.add(Vector3i.UNIT_X); - realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos); - blockPacket = new UpdateBlockPacket(); - blockPacket.setDataLayer(0); - blockPacket.setBlockPosition(holderPos); - blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(realBlock)); - session.sendUpstreamPacket(blockPacket); + holderPos = holderPos.add(Vector3i.UNIT_X); + realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos); + blockPacket = new UpdateBlockPacket(); + blockPacket.setDataLayer(0); + blockPacket.setBlockPosition(holderPos); + blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(realBlock)); + session.sendUpstreamPacket(blockPacket); + } + } + + private boolean canUseRealBlock(GeyserSession session, Inventory inventory) { + // See BlockInventoryHolder - same concept there except we're also dealing with a specific block state + if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) { + BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { + if ((state.block() == Blocks.CHEST || state.block() == Blocks.TRAPPED_CHEST) + && state.getValue(Properties.CHEST_TYPE) != ChestType.SINGLE) { + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, state.block()); + + NbtMapBuilder tag = BlockEntityTranslator.getConstantBedrockTag("Chest", session.getLastInteractionBlockPosition()) + .putString("CustomName", inventory.getTitle()); + + DoubleChestBlockEntityTranslator.translateChestValue(tag, state, + session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); + + BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); + dataPacket.setData(tag.build()); + dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); + session.sendUpstreamPacket(dataPacket); + + return true; + } + } + } + return false; } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java index 30a9ff8d6..d31efadd5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.translator.inventory.chest; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.holder.BlockInventoryHolder; import org.geysermc.geyser.inventory.holder.InventoryHolder; @@ -57,16 +58,16 @@ public class SingleChestInventoryTranslator extends ChestInventoryTranslator { @Override public boolean prepareInventory(GeyserSession session, Inventory inventory) { - return holder.prepareInventory(this, session, inventory); + return holder.prepareInventory(session, (Container) inventory); } @Override public void openInventory(GeyserSession session, Inventory inventory) { - holder.openInventory(this, session, inventory); + holder.openInventory(session, (Container) inventory); } @Override public void closeInventory(GeyserSession session, Inventory inventory) { - holder.closeInventory(this, session, inventory, ContainerType.CONTAINER); + holder.closeInventory(session, (Container) inventory, ContainerType.CONTAINER); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBookEditTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBookEditTranslator.java index e1ab6d242..ec3c8543d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBookEditTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBookEditTranslator.java @@ -25,12 +25,6 @@ package org.geysermc.geyser.translator.protocol.bedrock; -import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.Filterable; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.WritableBookContent; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundEditBookPacket; import org.cloudburstmc.protocol.bedrock.packet.BookEditPacket; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.WrittenBookItem; @@ -38,8 +32,17 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Filterable; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.WritableBookContent; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundEditBookPacket; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; @Translator(packet = BookEditPacket.class) public class BedrockBookEditTranslator extends PacketTranslator { @@ -129,7 +132,7 @@ public class BedrockBookEditTranslator extends PacketTranslator // Update local copy session.getPlayerInventory().setItem(36 + session.getPlayerInventory().getHeldItemSlot(), GeyserItemStack.from(bookItem), session); - session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); + session.getPlayerInventory().updateInventory(); String title; if (packet.getAction() == BookEditPacket.Action.SIGN_BOOK) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java index 4554bbe38..ded3f8afb 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java @@ -26,38 +26,73 @@ package org.geysermc.geyser.translator.protocol.bedrock; import org.cloudburstmc.protocol.bedrock.packet.ContainerClosePacket; +import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.MerchantContainer; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.MerchantInventoryTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.protocol.java.inventory.JavaMerchantOffersTranslator; import org.geysermc.geyser.util.InventoryUtils; +import java.util.concurrent.TimeUnit; + +import static org.geysermc.geyser.util.InventoryUtils.MAGIC_VIRTUAL_INVENTORY_HACK; + @Translator(packet = ContainerClosePacket.class) public class BedrockContainerCloseTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, ContainerClosePacket packet) { + GeyserImpl.getInstance().getLogger().debug(session, packet.toString()); byte bedrockId = packet.getId(); //Client wants close confirmation session.sendUpstreamPacket(packet); session.setClosingInventory(false); - if (bedrockId == -1 && session.getOpenInventory() instanceof MerchantContainer) { - // 1.16.200 - window ID is always -1 sent from Bedrock - bedrockId = (byte) session.getOpenInventory().getBedrockId(); + // 1.21.70: Bedrock can reject opening inventories - in those cases it replies with -1 + Inventory openInventory = session.getOpenInventory(); + if (bedrockId == -1 && openInventory != null) { + // 1.16.200 - window ID is always -1 sent from Bedrock for merchant containers + if (openInventory.getTranslator() instanceof MerchantInventoryTranslator) { + bedrockId = (byte) openInventory.getBedrockId(); + } else if (openInventory.getBedrockId() == session.getPendingOrCurrentBedrockInventoryId()) { + // If virtual inventories are opened too quickly, they can be occasionally rejected + // We just try and queue a new one. + // Before making another attempt to re-open, let's make sure we actually need this inventory open. + if (session.getContainerOpenAttempts() < 3) { + openInventory.setPending(true); + + session.scheduleInEventLoop(() -> { + NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket(); + latencyPacket.setFromServer(true); + latencyPacket.setTimestamp(MAGIC_VIRTUAL_INVENTORY_HACK); + session.sendUpstreamPacket(latencyPacket); + GeyserImpl.getInstance().getLogger().debug(session, "Unable to open a virtual inventory, sent another latency packet!"); + }, 100, TimeUnit.MILLISECONDS); + return; + } else { + GeyserImpl.getInstance().getLogger().debug(session, "Exceeded 3 attempts to open a virtual inventory!"); + GeyserImpl.getInstance().getLogger().debug(session, packet + " " + session.getOpenInventory().getClass().getSimpleName()); + } + } } - Inventory openInventory = session.getOpenInventory(); + session.setPendingOrCurrentBedrockInventoryId(-1); + session.setContainerOpenAttempts(0); + closeCurrentOrOpenPending(session, bedrockId, openInventory); + } + + private void closeCurrentOrOpenPending(GeyserSession session, byte bedrockId, Inventory openInventory) { if (openInventory != null) { if (bedrockId == openInventory.getBedrockId()) { InventoryUtils.sendJavaContainerClose(session, openInventory); InventoryUtils.closeInventory(session, openInventory.getJavaId(), false); } else if (openInventory.isPending()) { InventoryUtils.displayInventory(session, openInventory); - openInventory.setPending(false); if (openInventory instanceof MerchantContainer merchantContainer && merchantContainer.getPendingOffersPacket() != null) { JavaMerchantOffersTranslator.openMerchant(session, merchantContainer.getPendingOffersPacket(), merchantContainer); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockFilterTextTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockFilterTextTranslator.java index 921f24c19..ca7ba89ff 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockFilterTextTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockFilterTextTranslator.java @@ -48,7 +48,7 @@ public class BedrockFilterTextTranslator extends PacketTranslator currentJavaPage) { for (int i = currentJavaPage; i < newJavaPage; i++) { - ServerboundContainerButtonClickPacket clickButtonPacket = new ServerboundContainerButtonClickPacket(session.getOpenInventory().getJavaId(), 2); + ServerboundContainerButtonClickPacket clickButtonPacket = new ServerboundContainerButtonClickPacket(lecternContainer.getJavaId(), 2); session.sendDownstreamGamePacket(clickButtonPacket); } } else { for (int i = currentJavaPage; i > newJavaPage; i--) { - ServerboundContainerButtonClickPacket clickButtonPacket = new ServerboundContainerButtonClickPacket(session.getOpenInventory().getJavaId(), 1); + ServerboundContainerButtonClickPacket clickButtonPacket = new ServerboundContainerButtonClickPacket(lecternContainer.getJavaId(), 1); session.sendDownstreamGamePacket(clickButtonPacket); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java index 6ca0f3500..b4c47c1bf 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java @@ -32,6 +32,7 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundKeepAlivePacket; import java.util.Collections; @@ -64,20 +65,24 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator { - // Hack to fix the url image loading bug - UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); - attributesPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId()); + if (session.getPendingOrCurrentBedrockInventoryId() != -1) { + InventoryUtils.openPendingInventory(session); + } else { + session.scheduleInEventLoop(() -> { + // Hack to fix the url image loading bug + UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); + attributesPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId()); - AttributeData attribute = session.getPlayerEntity().getAttributes().get(GeyserAttributeType.EXPERIENCE_LEVEL); - if (attribute != null) { - attributesPacket.setAttributes(Collections.singletonList(attribute)); - } else { - attributesPacket.setAttributes(Collections.singletonList(GeyserAttributeType.EXPERIENCE_LEVEL.getAttribute(0))); - } + AttributeData attribute = session.getPlayerEntity().getAttributes().get(GeyserAttributeType.EXPERIENCE_LEVEL); + if (attribute != null) { + attributesPacket.setAttributes(Collections.singletonList(attribute)); + } else { + attributesPacket.setAttributes(Collections.singletonList(GeyserAttributeType.EXPERIENCE_LEVEL.getAttribute(0))); + } - session.sendUpstreamPacket(attributesPacket); - }, 500, TimeUnit.MILLISECONDS); + session.sendUpstreamPacket(attributesPacket); + }, 500, TimeUnit.MILLISECONDS); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index 556d8cd8d..09bcf8dc5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -65,8 +65,8 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat session.getEntityCache().updateBossBars(); // Double sigh - https://github.com/GeyserMC/Geyser/issues/2677 - as of Bedrock 1.18 - if (session.getOpenInventory() != null && session.getOpenInventory().isPending()) { - InventoryUtils.openInventory(session, session.getOpenInventory()); + if (session.getOpenInventory() != null) { + InventoryUtils.openPendingInventory(session); } // What am I to expect - as of Bedrock 1.18 diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java index 7cd1a1f55..316855b98 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java @@ -31,7 +31,6 @@ import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistries; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.ChunkUtils; @@ -62,7 +61,6 @@ public class JavaRespawnTranslator extends PacketTranslator { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java index cc229b993..112a089c1 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java @@ -79,7 +79,7 @@ public class JavaContainerSetContentTranslator extends PacketTranslator= inventory.getSize()) { @@ -285,11 +285,11 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator { @@ -45,6 +46,7 @@ public class JavaOpenScreenTranslator extends PacketTranslator())); + /** + * An arbitrary, negative long used to delay the opening of virtual inventories until the client is + * likely ready for it. The {@link org.geysermc.geyser.translator.protocol.bedrock.BedrockNetworkStackLatencyTranslator} + * will then call {@link #openPendingInventory(GeyserSession)}, which would finish opening the inventory. + */ + public static final long MAGIC_VIRTUAL_INVENTORY_HACK = -9876543210L; + + /** + * The main entrypoint to open an inventory. It will mark inventories as pending when the client isn't ready to + * open the new inventory yet. + * + * @param session the geyser session + * @param inventory the new inventory to open + */ public static void openInventory(GeyserSession session, Inventory inventory) { session.setOpenInventory(inventory); - if (session.isClosingInventory() || !session.getUpstream().isInitialized()) { + if (session.isClosingInventory() || !session.getUpstream().isInitialized() || session.getPendingOrCurrentBedrockInventoryId() != -1) { // Wait for close confirmation from client before opening the new inventory. // Handled in BedrockContainerCloseTranslator // or - client hasn't yet loaded in; wait until inventory is shown inventory.setPending(true); + GeyserImpl.getInstance().getLogger().debug(session, "Inventory (%s) set pending: closing inv? %s, pending inv id? %s", debugInventory(inventory), session.isClosingInventory(), session.getPendingOrCurrentBedrockInventoryId()); return; } displayInventory(session, inventory); } + /** + * Called when the Bedrock client is ready to open a pending inventory. + * Due to the nature of possible changes in the delayed time, this method also re-checks for changes that might have + * occurred in the time. For example, a queued virtual inventory might be "outdated", so we wouldn't open it. + */ + public static void openPendingInventory(GeyserSession session) { + Inventory currentInventory = session.getOpenInventory(); + if (currentInventory == null || !currentInventory.isPending()) { + session.setPendingOrCurrentBedrockInventoryId(-1); + GeyserImpl.getInstance().getLogger().debug(session, "No pending inventory, not opening an inventory! Current inventory: %s", debugInventory(currentInventory)); + return; + } + + // Current inventory isn't null! Let's see if we need to open it. + if (currentInventory.getBedrockId() == session.getPendingOrCurrentBedrockInventoryId()) { + GeyserImpl.getInstance().getLogger().debug(session, "Attempting to open currently delayed inventory with matching bedrock id! " + currentInventory.getBedrockId()); + openAndUpdateInventory(session, currentInventory); + return; + } + + GeyserImpl.getInstance().getLogger().debug(session, "Opening any pending inventory! " + debugInventory(currentInventory)); + displayInventory(session, currentInventory); + } + + /** + * Prepares and displays the current inventory. If necessary, it will queue the opening of virtual inventories. + * @param inventory the inventory to display + */ public static void displayInventory(GeyserSession session, Inventory inventory) { - InventoryTranslator translator = session.getInventoryTranslator(); + InventoryTranslator translator = inventory.getTranslator(); if (translator.prepareInventory(session, inventory)) { - if (translator instanceof DoubleChestInventoryTranslator && !((Container) inventory).isUsingRealBlock()) { - session.scheduleInEventLoop(() -> { - Inventory openInv = session.getOpenInventory(); - if (openInv != null && openInv.getJavaId() == inventory.getJavaId()) { - translator.openInventory(session, inventory); - translator.updateInventory(session, inventory); - openInv.setDisplayed(true); - } else if (openInv != null && openInv.isPending()) { - // Presumably, this inventory is no longer relevant, and the client doesn't care about it - displayInventory(session, openInv); - } - }, 200, TimeUnit.MILLISECONDS); + session.setPendingOrCurrentBedrockInventoryId(inventory.getBedrockId()); + if (translator.requiresOpeningDelay(session, inventory)) { + inventory.setPending(true); + + NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket(); + latencyPacket.setFromServer(true); + latencyPacket.setTimestamp(MAGIC_VIRTUAL_INVENTORY_HACK); + session.sendUpstreamPacket(latencyPacket); + + GeyserImpl.getInstance().getLogger().debug(session, "Queuing virtual inventory (%s)", debugInventory(inventory)); } else { - translator.openInventory(session, inventory); - translator.updateInventory(session, inventory); - inventory.setDisplayed(true); + openAndUpdateInventory(session, inventory); } } else { // Can occur if we e.g. did not find a spot to put a fake container in + session.setPendingOrCurrentBedrockInventoryId(-1); sendJavaContainerClose(session, inventory); session.setOpenInventory(null); - session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); } } + /** + * Opens and updates an inventory, and resets no longer used inventory variables. + */ + public static void openAndUpdateInventory(GeyserSession session, Inventory inventory) { + inventory.getTranslator().openInventory(session, inventory); + inventory.getTranslator().updateInventory(session, inventory); + inventory.setDisplayed(true); + inventory.setPending(false); + } + + /** + * Returns the current inventory translator. + */ + public static @NonNull InventoryTranslator getInventoryTranslator(GeyserSession session) { + Inventory inventory = session.getOpenInventory(); + if (inventory == null) { + return InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR; + } + return inventory.getTranslator(); + } + + /** + * Closes the inventory that matches the java id. + * @param session the session to close the inventory for + * @param javaId the id of the inventory to close + * @param confirm whether to wait for the session to process the close before opening a new inventory. + */ public static void closeInventory(GeyserSession session, int javaId, boolean confirm) { session.getPlayerInventory().setCursor(GeyserItemStack.EMPTY, session); updateCursor(session); Inventory inventory = getInventory(session, javaId); if (inventory != null) { - InventoryTranslator translator = session.getInventoryTranslator(); + InventoryTranslator translator = inventory.getTranslator(); translator.closeInventory(session, inventory); if (confirm && inventory.isDisplayed() && !inventory.isPending() && !(translator instanceof LecternInventoryTranslator) // Closing lecterns is not followed with a close confirmation @@ -139,8 +204,9 @@ public class InventoryUtils { session.setClosingInventory(true); } session.getBundleCache().onInventoryClose(inventory); + GeyserImpl.getInstance().getLogger().debug(session, "Closed inventory: (java id: %s/bedrock id: %s), waiting on confirm? %s", inventory.getJavaId(), inventory.getBedrockId(), session.isClosingInventory()); } - session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); + session.setOpenInventory(null); } @@ -421,4 +487,18 @@ public class InventoryUtils { } return true; } + + public static String debugInventory(@Nullable Inventory inventory) { + if (inventory == null) { + return "null"; + } + + String inventoryType = inventory.getContainerType() != null ? + inventory.getContainerType().name() : "null"; + + return inventory.getClass().getSimpleName() + ": javaId=" + inventory.getJavaId() + + ", bedrockId=" + inventory.getBedrockId() + ", size=" + inventory.getSize() + + ", type=" + inventoryType + ", pending=" + inventory.isPending() + + ", displayed=" + inventory.isPending() + ", delayed=" + inventory.isPending(); + } } diff --git a/gradle.properties b/gradle.properties index 343f7a51e..e718a2884 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,5 +8,5 @@ org.gradle.vfs.watch=false group=org.geysermc id=geyser -version=2.6.2-SNAPSHOT +version=2.7.0-SNAPSHOT description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers.