1
0
Fork 0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-04-17 19:12:14 +02:00

Fix some edge-cases related to lecterns

This commit is contained in:
onebeastchris 2025-04-10 17:58:40 +02:00
parent 966de4d62c
commit ed45df2a2e
9 changed files with 68 additions and 65 deletions

View file

@ -27,8 +27,6 @@ package org.geysermc.geyser.inventory;
import lombok.Getter;
import lombok.Setter;
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.protocol.java.inventory.JavaOpenBookTranslator;
@ -40,8 +38,6 @@ public class LecternContainer extends Container {
private int currentBedrockPage = 0;
@Setter
private NbtMap blockEntityTag;
@Setter
private Vector3i position;
private boolean isBookInPlayerInventory = false;
@ -49,24 +45,11 @@ public class LecternContainer extends Container {
super(session, title, id, size, containerType);
}
/**
* When the Java server asks the client to open a book in their hotbar, we create a fake lectern to show it to the client.
* We can't use the {@link #isUsingRealBlock()} check as we may also be dealing with a real virtual lectern (with its own inventory).
*/
@Override
public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
if (isBookInPlayerInventory) {
session.getPlayerInventory().setItem(slot, newItem, session);
} else {
super.setItem(slot, newItem, session);
}
}
/**
* This is used ONLY once to set the book of a fake lectern in {@link JavaOpenBookTranslator}.
* See {@link LecternContainer#setItem(int, GeyserItemStack, GeyserSession)} as for why this is separate.
*/
public void setFakeLecternBook(GeyserItemStack book, GeyserSession session) {
public void setVirtualLecternBook(GeyserItemStack book, GeyserSession session) {
this.isBookInPlayerInventory = true;
super.setItem(0, book, session);
}

View file

@ -138,8 +138,9 @@ import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
import org.geysermc.geyser.event.type.SessionDisconnectEventImpl;
import org.geysermc.geyser.impl.camera.CameraDefinitions;
import org.geysermc.geyser.impl.camera.GeyserCameraData;
import org.geysermc.geyser.inventory.InventoryHolder;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.InventoryHolder;
import org.geysermc.geyser.inventory.LecternContainer;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe;
@ -299,7 +300,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final InventoryHolder<PlayerInventory> playerInventoryHolder;
/**
* Stores the current open inventory, including the correct translator.
* Stores the current open Bedrock inventory, including the correct translator.
* Prefer using {@link InventoryUtils#getInventory(GeyserSession, int)}, as this
* method can e.g. return a {@code InventoryHolder<LecternContainer>} due to the
* workaround in {@link LecternContainer#isBookInPlayerInventory()} workaround.
*/
@Setter
private @Nullable InventoryHolder<? extends Inventory> inventoryHolder;

View file

@ -32,6 +32,7 @@ import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.InventoryHolder;
import org.geysermc.geyser.inventory.LecternContainer;
import org.geysermc.geyser.inventory.updater.ContainerInventoryUpdater;
import org.geysermc.geyser.level.block.Blocks;
@ -55,7 +56,7 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator
private boolean receivedBook = false;
public LecternInventoryTranslator() {
super(1, Blocks.LECTERN, org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType.LECTERN , ContainerInventoryUpdater.INSTANCE);
super(1, Blocks.LECTERN, org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType.LECTERN, ContainerInventoryUpdater.INSTANCE);
}
@Override
@ -87,7 +88,7 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator
// Of course, sending a simple ContainerClosePacket, or even breaking the block doesn't work to close a lectern.
// Heck, the latter crashes the client xd
// BDS just sends an empty base lectern tag... that kicks out the client. Fine. Let's do that!
Vector3i position = container.isUsingRealBlock() ? session.getLastInteractionBlockPosition() : container.getHolderPosition();
Vector3i position = container.getHolderPosition();
var baseLecternTag = LecternUtils.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0);
BlockEntityUtils.updateBlockEntity(session, baseLecternTag.build(), position);
@ -109,7 +110,7 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator
if (key == 0) { // Lectern page update
inventory.setCurrentBedrockPage(value / 2);
inventory.setBlockEntityTag(inventory.getBlockEntityTag().toBuilder().putInt("page", inventory.getCurrentBedrockPage()).build());
BlockEntityUtils.updateBlockEntity(session, inventory.getBlockEntityTag(), inventory.getPosition());
BlockEntityUtils.updateBlockEntity(session, inventory.getBlockEntityTag(), inventory.getHolderPosition());
}
}
@ -129,11 +130,6 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator
@Override
public void updateSlot(GeyserSession session, LecternContainer container, int slot) {
// If we're not in a real lectern, the Java server thinks we are still in the player inventory.
if (container.isBookInPlayerInventory()) {
session.getPlayerInventoryHolder().updateSlot(slot);
return;
}
super.updateSlot(session, container, slot);
if (slot == 0) {
updateBook(session, container, container.getItem(0));
@ -148,16 +144,19 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator
/**
* Translate the data of the book in the lectern into a block entity tag.
*/
private void updateBook(GeyserSession session, LecternContainer inventory, GeyserItemStack book) {
private void updateBook(GeyserSession session, LecternContainer container, GeyserItemStack book) {
if (session.isDroppingLecternBook()) {
// We have to enter the inventory GUI to eject the book
ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getJavaId(), 3);
session.sendDownstreamGamePacket(packet);
session.setDroppingLecternBook(false);
InventoryUtils.closeInventory(session, inventory.getJavaId(), false);
} else if (inventory.getBlockEntityTag() == null) {
Vector3i position = inventory.isUsingRealBlock() ?
session.getLastInteractionBlockPosition() : inventory.getHolderPosition();
InventoryHolder<?> holder = session.getInventoryHolder();
if (holder != null && !container.isBookInPlayerInventory()) {
// We have to enter the inventory GUI to eject the book
ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(container.getJavaId(), 3);
session.sendDownstreamGamePacket(packet);
session.setDroppingLecternBook(false);
InventoryUtils.sendJavaContainerClose(holder);
InventoryUtils.closeInventory(session, container.getJavaId(), false);
}
} else if (container.getBlockEntityTag() == null) {
Vector3i position = container.getHolderPosition();
NbtMap blockEntityTag;
if (book.hasNonBaseComponents()) {
@ -180,17 +179,14 @@ public class LecternInventoryTranslator extends AbstractBlockInventoryTranslator
.putString("Name", "minecraft:written_book")
.putCompound("tag", itemData.getTag())
.build());
lecternTag.putInt("page", inventory.getCurrentBedrockPage());
lecternTag.putInt("page", container.getCurrentBedrockPage());
blockEntityTag = lecternTag.build();
} else {
// There is *a* book here, but... no NBT.
blockEntityTag = LecternBlock.getBaseLecternTag(position, true);
}
// Even with serverside access to lecterns, we don't easily know which lectern this is, so we need to rebuild
// the block entity tag
inventory.setBlockEntityTag(blockEntityTag);
inventory.setPosition(position);
container.setBlockEntityTag(blockEntityTag);
BlockEntityUtils.updateBlockEntity(session, blockEntityTag, position);
}

View file

@ -91,7 +91,7 @@ public class BedrockContainerCloseTranslator extends PacketTranslator<ContainerC
if (holder != null) {
if (bedrockId == holder.bedrockId()) {
InventoryUtils.sendJavaContainerClose(holder);
InventoryUtils.closeInventory(session, holder.javaId(), false);
InventoryUtils.closeInventory(session, holder, false);
} else if (holder.pending()) {
InventoryUtils.displayInventory(holder);

View file

@ -52,7 +52,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) {
// The same page means Bedrock is closing the window
InventoryUtils.sendJavaContainerClose(holder);
InventoryUtils.closeInventory(session, lecternContainer.getJavaId(), false);
InventoryUtils.closeInventory(session, holder, false);
} else {
// Each "page" Bedrock gives to us actually represents two pages (think opening a book and seeing two pages)
// Each "page" on Java is just one page (think a spiral notebook folded back to only show one page)

View file

@ -26,6 +26,8 @@
package org.geysermc.geyser.translator.protocol.java.inventory;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.InventoryHolder;
import org.geysermc.geyser.inventory.LecternContainer;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@ -40,8 +42,24 @@ public class JavaContainerCloseTranslator extends PacketTranslator<ClientboundCo
public void translate(GeyserSession session, ClientboundContainerClosePacket packet) {
// Sometimes the server can request a window close of ID 0... when the window isn't even open
// Don't confirm in this instance
Inventory inventory = session.getOpenInventory();
session.setServerRequestedClosePlayerInventory(packet.getContainerId() == 0 && inventory instanceof PlayerInventory);
InventoryUtils.closeInventory(session, packet.getContainerId(), (inventory != null && inventory.getJavaId() == packet.getContainerId()));
InventoryHolder<? extends Inventory> holder = session.getInventoryHolder();
boolean confirm = false;
if (holder != null) {
if (packet.getContainerId() == 0) {
Inventory inventory = holder.inventory();
if (inventory instanceof PlayerInventory) {
session.setServerRequestedClosePlayerInventory(true);
}
if (inventory instanceof LecternContainer container && container.isBookInPlayerInventory()) {
InventoryUtils.closeInventory(session, holder, false);
return;
}
}
confirm = holder.javaId() == packet.getContainerId();
}
InventoryUtils.closeInventory(session, packet.getContainerId(), confirm);
}
}

View file

@ -28,13 +28,13 @@ package org.geysermc.geyser.translator.protocol.java.inventory;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.InventoryHolder;
import org.geysermc.geyser.inventory.LecternContainer;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
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.InventoryUtils;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundOpenBookPacket;
import java.util.Objects;
@ -59,21 +59,22 @@ public class JavaOpenBookTranslator extends PacketTranslator<ClientboundOpenBook
return;
}
if (stack.asItem().equals(Items.WRITTEN_BOOK)) {
// The item doesn't need to be a book; just needs to have either of these components.
if (stack.hasNonBaseComponents() &&
stack.getComponent(DataComponentTypes.WRITABLE_BOOK_CONTENT) != null ||
stack.getComponent(DataComponentTypes.WRITTEN_BOOK_CONTENT) != null
) {
InventoryHolder<?> openInventory = session.getInventoryHolder();
if (openInventory != null) {
InventoryUtils.closeInventory(session, openInventory.javaId(), true);
InventoryUtils.sendJavaContainerClose(openInventory);
InventoryUtils.closeInventory(session, openInventory, true);
}
//noinspection unchecked
InventoryTranslator<LecternContainer> translator = (InventoryTranslator<LecternContainer>) InventoryTranslator.inventoryTranslator(ContainerType.LECTERN);
Objects.requireNonNull(translator, "could not find lectern inventory translator!");
// Should never be null
Objects.requireNonNull(translator, "lectern translator must exist");
LecternContainer container = translator.createInventory(session, "", FAKE_LECTERN_WINDOW_ID, ContainerType.LECTERN);
container.setFakeLecternBook(stack, session);
container.setVirtualLecternBook(stack, session);
InventoryUtils.openInventory(new InventoryHolder<>(session, container, translator));
}
}

View file

@ -66,7 +66,7 @@ public class JavaOpenScreenTranslator extends PacketTranslator<ClientboundOpenSc
// No translator exists for this window type. Close all windows and return.
if (newTranslator == null) {
if (currentInventory != null) {
InventoryUtils.closeInventory(session, currentInventory.javaId(), true);
InventoryUtils.closeInventory(session, currentInventory, true);
}
ServerboundContainerClosePacket closeWindowPacket = new ServerboundContainerClosePacket(packet.getContainerId());
@ -93,7 +93,7 @@ public class JavaOpenScreenTranslator extends PacketTranslator<ClientboundOpenSc
return;
}
InventoryUtils.closeInventory(session, currentInventory.javaId(), true);
InventoryUtils.closeInventory(session, currentInventory, true);
}
InventoryUtils.openInventory(newInventoryHolder);

View file

@ -39,7 +39,6 @@ import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.InventoryHolder;
import org.geysermc.geyser.inventory.LecternContainer;
import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
@ -63,6 +62,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSta
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.TagSlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.WithRemainderSlotDisplay;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundOpenBookPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClosePacket;
import org.jetbrains.annotations.Contract;
@ -175,10 +175,14 @@ public class InventoryUtils {
* @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) {
InventoryHolder<?> holder = getInventory(session, javaId);
closeInventory(session, holder, confirm);
}
public static void closeInventory(GeyserSession session, InventoryHolder<?> holder, boolean confirm) {
session.getPlayerInventory().setCursor(GeyserItemStack.EMPTY, session);
updateCursor(session);
InventoryHolder<?> holder = getInventory(session, javaId);
if (holder != null) {
holder.closeInventory();
if (holder.shouldConfirmClose(confirm)) {
@ -192,18 +196,15 @@ public class InventoryUtils {
}
/**
* A util method to get the an inventory based on a Java id. This is used over
* {@link GeyserSession#getInventoryHolder()} when needing to account for player inventories instead of just the open inventory.
* A util method to get an (open) inventory based on a Java id. This method should be used over
* {@link GeyserSession#getOpenInventory()} (or {@link GeyserSession#getInventoryHolder()}) to account for an edge-case where the Java server expects
* Geyser to have the player inventory open while we're using a virtual lectern for the {@link ClientboundOpenBookPacket}.
*/
public static @Nullable InventoryHolder<?> getInventory(GeyserSession session, int javaId) {
InventoryHolder<?> holder = session.getInventoryHolder();
if (javaId == 0) {
// ugly hack: lecterns aren't their own inventory on Java, and can hence be closed with e.g. an id of 0
if (holder != null && holder.inventory() instanceof LecternContainer) {
return session.getInventoryHolder();
}
return session.getPlayerInventoryHolder();
} else {
InventoryHolder<?> holder = session.getInventoryHolder();
if (holder != null && javaId == holder.javaId()) {
return holder;
}