This commit is contained in:
Camotoy 2020-12-28 00:29:27 -05:00
parent 790c695b27
commit 2265de3ae9
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
19 changed files with 502 additions and 20 deletions

View file

@ -157,14 +157,14 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
if (isViaVersion && isViaVersionNeeded()) {
if (isLegacy) {
// Pre-1.13
this.geyserWorldManager = new GeyserSpigot1_12NativeWorldManager();
this.geyserWorldManager = new GeyserSpigot1_12NativeWorldManager(this);
} else {
// Post-1.13
this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this, use3dBiomes);
}
} else {
// No ViaVersion
this.geyserWorldManager = new GeyserSpigotNativeWorldManager(use3dBiomes);
this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this, use3dBiomes);
}
geyserLogger.debug("Using NMS adapter: " + this.geyserWorldManager.getClass() + ", " + nmsVersion);
} catch (Exception e) {
@ -180,13 +180,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
// No NMS adapter
if (isLegacy && isViaVersion) {
// Use ViaVersion for converting pre-1.13 block states
this.geyserWorldManager = new GeyserSpigot1_12WorldManager();
this.geyserWorldManager = new GeyserSpigot1_12WorldManager(this);
} else if (isLegacy) {
// Not sure how this happens - without ViaVersion, we don't know any block states, so just assume everything is air
this.geyserWorldManager = new GeyserSpigotFallbackWorldManager();
this.geyserWorldManager = new GeyserSpigotFallbackWorldManager(this);
} else {
// Post-1.13
this.geyserWorldManager = new GeyserSpigotWorldManager(use3dBiomes);
this.geyserWorldManager = new GeyserSpigotWorldManager(this, use3dBiomes);
}
geyserLogger.debug("Using default world manager: " + this.geyserWorldManager.getClass());
}

View file

@ -27,6 +27,7 @@ package org.geysermc.platform.spigot.world.manager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.geysermc.adapters.spigot.SpigotAdapters;
import org.geysermc.adapters.spigot.SpigotWorldAdapter;
import org.geysermc.connector.network.session.GeyserSession;
@ -40,7 +41,8 @@ import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.storage.BlockStorage;
public class GeyserSpigot1_12NativeWorldManager extends GeyserSpigot1_12WorldManager {
private final SpigotWorldAdapter adapter;
public GeyserSpigot1_12NativeWorldManager() {
public GeyserSpigot1_12NativeWorldManager(Plugin plugin) {
super(plugin);
this.adapter = SpigotAdapters.getWorldAdapter();
// Unlike post-1.13, we can't build up a cache of block states, because block entities need some special conversion
}

View file

@ -30,6 +30,7 @@ import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import us.myles.ViaVersion.api.Pair;
@ -61,8 +62,8 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager {
*/
private final List<Pair<Integer, Protocol>> protocolList;
public GeyserSpigot1_12WorldManager() {
super(false);
public GeyserSpigot1_12WorldManager(Plugin plugin) {
super(plugin, false);
this.mappingData1_12to1_13 = ProtocolRegistry.getProtocol(Protocol1_13To1_12_2.class).getMappingData();
this.protocolList = ProtocolRegistry.getProtocolPath(CLIENT_PROTOCOL_VERSION,
ProtocolVersion.v1_13.getVersion());

View file

@ -26,6 +26,7 @@
package org.geysermc.platform.spigot.world.manager;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import org.bukkit.plugin.Plugin;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
@ -35,9 +36,9 @@ import org.geysermc.connector.network.translators.world.block.BlockTranslator;
* If this occurs to you somehow, please let us know!!
*/
public class GeyserSpigotFallbackWorldManager extends GeyserSpigotWorldManager {
public GeyserSpigotFallbackWorldManager() {
public GeyserSpigotFallbackWorldManager(Plugin plugin) {
// Since this is pre-1.13 (and thus pre-1.15), there will never be 3D biomes.
super(false);
super(plugin, false);
}
@Override

View file

@ -47,7 +47,7 @@ public class GeyserSpigotLegacyNativeWorldManager extends GeyserSpigotNativeWorl
private final Int2IntMap oldToNewBlockId;
public GeyserSpigotLegacyNativeWorldManager(GeyserSpigotPlugin plugin, boolean use3dBiomes) {
super(use3dBiomes);
super(plugin, use3dBiomes);
IntList allBlockStates = adapter.getAllBlockStates();
oldToNewBlockId = new Int2IntOpenHashMap(allBlockStates.size());
ProtocolVersion serverVersion = plugin.getServerProtocolVersion();

View file

@ -27,6 +27,7 @@ package org.geysermc.platform.spigot.world.manager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.geysermc.adapters.spigot.SpigotAdapters;
import org.geysermc.adapters.spigot.SpigotWorldAdapter;
import org.geysermc.connector.network.session.GeyserSession;
@ -35,8 +36,8 @@ import org.geysermc.connector.network.translators.world.block.BlockTranslator;
public class GeyserSpigotNativeWorldManager extends GeyserSpigotWorldManager {
protected final SpigotWorldAdapter adapter;
public GeyserSpigotNativeWorldManager(boolean use3dBiomes) {
super(use3dBiomes);
public GeyserSpigotNativeWorldManager(Plugin plugin, boolean use3dBiomes) {
super(plugin, use3dBiomes);
adapter = SpigotAdapters.getWorldAdapter();
}

View file

@ -28,23 +28,35 @@ package org.geysermc.platform.spigot.world.manager;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.steveice10.mc.protocol.MinecraftConstants;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.Lectern;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.plugin.Plugin;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.translators.LecternInventoryTranslator;
import org.geysermc.connector.network.translators.world.GeyserWorldManager;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.BlockEntityUtils;
import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.GameRule;
import org.geysermc.connector.utils.LanguageUtils;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* The base world manager to use when there is no supported NMS revision
@ -72,8 +84,11 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
*/
private final Int2IntMap biomeToIdMap = new Int2IntOpenHashMap(Biome.values().length);
public GeyserSpigotWorldManager(boolean use3dBiomes) {
private final Plugin plugin;
public GeyserSpigotWorldManager(Plugin plugin, boolean use3dBiomes) {
this.use3dBiomes = use3dBiomes;
this.plugin = plugin;
// Load the values into the biome-to-ID map
InputStream biomeStream = FileUtils.getResource("biomes.json");
@ -132,9 +147,6 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
@Override
@SuppressWarnings("deprecation")
public int[] getBiomeDataAt(GeyserSession session, int x, int z) {
if (session.getPlayerEntity() == null) {
return new int[1024];
}
int[] biomeData = new int[1024];
World world = Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld();
int chunkX = x << 4;
@ -167,6 +179,56 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
return biomeData;
}
@Override
public NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad) {
// Run as a task to prevent async issues
Bukkit.getScheduler().runTask(this.plugin, () -> {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
return;
}
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
if (!(block.getState() instanceof Lectern)) {
session.getConnector().getLogger().error("Lectern expected at: " + Vector3i.from(x, y, z).toString() + " but was not! " + block.toString());
return;
}
Lectern lectern = (Lectern) block.getState();
ItemStack itemStack = lectern.getInventory().getItem(0);
if (itemStack == null || !(itemStack.getItemMeta() instanceof BookMeta)) {
if (!isChunkLoad) {
// We need to update the lectern since it's not going to be updated otherwise
BlockEntityUtils.updateBlockEntity(session, LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z));
}
// We don't care; return
return;
}
BookMeta bookMeta = (BookMeta) itemStack.getItemMeta();
NbtMapBuilder lecternTag = LecternInventoryTranslator.getBaseLecternTag(x, y, z, bookMeta.getPageCount());
lecternTag.putInt("page", lectern.getPage() / 2);
NbtMapBuilder bookTag = NbtMap.builder()
.putByte("Count", (byte) itemStack.getAmount())
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:writable_book");
List<NbtMap> pages = new ArrayList<>();
for (String page : bookMeta.getPages()) {
NbtMapBuilder pageBuilder = NbtMap.builder()
.putString("photoname", "")
.putString("text", page);
pages.add(pageBuilder.build());
}
bookTag.putCompound("tag", NbtMap.builder().putList("pages", NbtType.COMPOUND, pages).build());
lecternTag.putCompound("book", bookTag.build());
NbtMap blockEntityTag = lecternTag.build();
BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z));
});
return LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(); // Will be updated later
}
@Override
public boolean shouldExpectLecternHandled() {
return true;
}
public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) {
return Boolean.parseBoolean(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID()));
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.inventory;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import lombok.Getter;
import lombok.Setter;
public class LecternContainer extends Container {
@Getter @Setter
private int currentBedrockPage = 0;
@Getter @Setter
private NbtMap blockEntityTag;
@Getter @Setter
private Vector3i position;
public LecternContainer(String title, int id, int size, PlayerInventory playerInventory) {
super(title, id, size, playerInventory);
}
}

View file

@ -153,6 +153,16 @@ public class GeyserSession implements CommandSender {
*/
private final Object2LongMap<Vector3i> itemFrameCache = new Object2LongOpenHashMap<>();
/**
* Stores a list of all lectern locations and their block entity tags.
* See {@link org.geysermc.connector.network.translators.world.WorldManager#getLecternDataAt(GeyserSession, int, int, int, boolean)}
* for more information.
*/
private final List<Vector3i> lecternCache = new ArrayList<>();
@Setter
private boolean droppingLecternBook;
@Setter
private Vector2i lastChunkPosition = null;
private int renderDistance;

View file

@ -50,7 +50,7 @@ public class BedrockContainerCloseTranslator extends PacketTranslator<ContainerC
if (openInventory != null && windowId == openInventory.getId()) {
System.out.println(packet);
ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(windowId);
session.getDownstream().getSession().send(closeWindowPacket);
session.sendDownstreamPacket(closeWindowPacket);
InventoryUtils.closeInventory(session, windowId);
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network.translators.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPlaceBlockPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.LecternUpdatePacket;
import org.geysermc.connector.inventory.LecternContainer;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.utils.InventoryUtils;
@Translator(packet = LecternUpdatePacket.class)
public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpdatePacket> {
@Override
public void translate(LecternUpdatePacket packet, GeyserSession session) {
session.getConnector().getLogger().error(packet.toString());
if (packet.isDroppingBook()) {
// Bedrock drops the book outside of the GUI. Java drops it in the GUI
// So, we enter the GUI and then drop it! :)
session.setDroppingLecternBook(true);
Vector3f diff = session.getPlayerEntity().getPosition().sub(packet.getBlockPosition().toFloat());
System.out.println(diff);
// Emulate an interact packet
ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket(
new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
BlockFace.values()[0],
Hand.MAIN_HAND,
packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ(), //TODO
false);
session.sendDownstreamPacket(blockPacket);
} else {
// Bedrock wants to either move a page or exit
LecternContainer lecternContainer = (LecternContainer) session.getOpenInventory();
if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) {
// The same page means Bedrock is closing the window
ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(lecternContainer.getId());
session.sendDownstreamPacket(closeWindowPacket);
InventoryUtils.closeInventory(session, lecternContainer.getId());
} 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)
int newJavaPage = (packet.getPage() * 2);
int currentJavaPage = (lecternContainer.getCurrentBedrockPage() * 2);
// Send as many click button packets as we need to
// Java has the option to specify exact page numbers by adding 100 to the number, but buttonId variable
// is a byte and therefore this stops us at 128
if (newJavaPage > currentJavaPage) {
for (int i = currentJavaPage; i < newJavaPage; i++) {
ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 2);
System.out.println(clickButtonPacket);
session.sendDownstreamPacket(clickButtonPacket);
}
} else {
for (int i = currentJavaPage; i > newJavaPage; i--) {
ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 1);
System.out.println(clickButtonPacket);
session.sendDownstreamPacket(clickButtonPacket);
}
}
}
}
}
}

View file

@ -97,6 +97,9 @@ public abstract class InventoryTranslator {
/* Generics */
put(WindowType.GENERIC_3X3, new GenericBlockInventoryTranslator(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER));
put(WindowType.HOPPER, new GenericBlockInventoryTranslator(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER));
/* Lectern */
put(WindowType.LECTERN, new LecternInventoryTranslator());
}
};

View file

@ -0,0 +1,159 @@
/*
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network.translators.inventory.translators;
import com.github.steveice10.mc.protocol.data.game.window.WindowType;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.inventory.LecternContainer;
import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater;
import org.geysermc.connector.utils.BlockEntityUtils;
import org.geysermc.connector.utils.InventoryUtils;
public class LecternInventoryTranslator extends BaseInventoryTranslator {
private final InventoryUpdater updater;
public LecternInventoryTranslator() {
super(1);
this.updater = new LecternInventoryUpdater();
}
@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {
}
@Override
public void openInventory(GeyserSession session, Inventory inventory) {
}
@Override
public void closeInventory(GeyserSession session, Inventory inventory) {
}
@Override
public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) {
if (key == 0) { // Lectern page update
LecternContainer lecternContainer = (LecternContainer) inventory;
lecternContainer.setCurrentBedrockPage(value / 2);
lecternContainer.setBlockEntityTag(lecternContainer.getBlockEntityTag().toBuilder().putInt("page", lecternContainer.getCurrentBedrockPage()).build());
System.out.println(lecternContainer.getBlockEntityTag());
BlockEntityUtils.updateBlockEntity(session, lecternContainer.getBlockEntityTag(), lecternContainer.getPosition());
}
}
@Override
public void updateInventory(GeyserSession session, Inventory inventory) {
}
@Override
public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
this.updater.updateSlot(this, session, inventory, slot);
if (slot == 0) {
LecternContainer lecternContainer = (LecternContainer) inventory;
if (session.isDroppingLecternBook()) {
// We have to enter the inventory GUI to eject the book
ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), 3);
session.sendDownstreamPacket(packet);
session.setDroppingLecternBook(false);
InventoryUtils.closeInventory(session, inventory.getId());
} else if (lecternContainer.getBlockEntityTag() == null) {
// If the method returns true, this is already handled for us
GeyserItemStack geyserItemStack = inventory.getItem(0);
CompoundTag tag = geyserItemStack.getNbt();
if (tag != null) {
// Position has to be the last interacted position... right?
Vector3i position = session.getLastInteractionPosition();
// shouldRefresh means that we should boot out the
boolean shouldRefresh = !session.getConnector().getWorldManager().shouldExpectLecternHandled() && !session.getLecternCache().contains(position);
int pagesSize = ((ListTag) tag.get("pages")).size();
ItemData itemData = geyserItemStack.getItemData(session);
NbtMapBuilder lecternTag = getBaseLecternTag(position.getX(), position.getY(), position.getZ(), pagesSize);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) itemData.getCount())
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:written_book")
.putCompound("tag", itemData.getTag())
.build());
lecternTag.putInt("page", lecternContainer.getCurrentBedrockPage());
NbtMap blockEntityTag = lecternTag.build();
// Even with serverside access to lecterns, we don't easily know which lectern this is, so we need to rebuild
// the block entity tag
lecternContainer.setBlockEntityTag(blockEntityTag);
System.out.println(blockEntityTag);
lecternContainer.setPosition(position);
if (shouldRefresh) {
// Update the lectern because it's not updated client-side
BlockEntityUtils.updateBlockEntity(session, blockEntityTag, position);
session.getLecternCache().add(position);
// Close the window - we will reopen it once the client has this data synced
ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(lecternContainer.getId());
session.sendDownstreamPacket(closeWindowPacket);
InventoryUtils.closeInventory(session, inventory.getId());
session.getConnector().getLogger().warning("Closing inventory");
}
}
}
}
}
@Override
public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) {
return new LecternContainer(name, windowId, this.size, playerInventory);
}
public static NbtMapBuilder getBaseLecternTag(int x, int y, int z, int totalPages) {
NbtMapBuilder builder = NbtMap.builder()
.putInt("x", x)
.putInt("y", y)
.putInt("z", z)
.putString("id", "Lectern");
if (totalPages != 0) {
builder.putByte("hasBook", (byte) 1);
builder.putInt("totalPages", totalPages);
} else {
builder.putByte("hasBook", (byte) 0);
}
return builder;
}
private static class LecternInventoryUpdater extends InventoryUpdater {
}
}

View file

@ -44,10 +44,19 @@ public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChun
Iterator<Vector3i> iterator = session.getSkullCache().keySet().iterator();
while (iterator.hasNext()) {
Vector3i position = iterator.next();
if (Math.floor(position.getX() / 16) == packet.getX() && Math.floor(position.getZ() / 16) == packet.getZ()) {
if (Math.floor((double) position.getX() / 16) == packet.getX() && Math.floor((double) position.getZ() / 16) == packet.getZ()) {
session.getSkullCache().get(position).despawnEntity(session);
iterator.remove();
}
}
// Do the same thing with lecterns
iterator = session.getLecternCache().iterator();
while (iterator.hasNext()) {
Vector3i position = iterator.next();
if (Math.floor((double) position.getX() / 16) == packet.getX() && Math.floor((double) position.getZ() / 16) == packet.getZ()) {
iterator.remove();
}
}
}
}

View file

@ -30,10 +30,13 @@ import com.github.steveice10.mc.protocol.data.game.chunk.Column;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.github.steveice10.mc.protocol.packet.ingame.client.ClientChatPacket;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.cache.ChunkCache;
import org.geysermc.connector.network.translators.inventory.translators.LecternInventoryTranslator;
import org.geysermc.connector.utils.GameRule;
public class GeyserWorldManager extends WorldManager {
@ -88,6 +91,29 @@ public class GeyserWorldManager extends WorldManager {
return new int[1024];
}
@Override
public NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad) {
// Without direct server access, we can't get lectern information on-the-fly.
// I should have set this up so it's only called when there is a book in the block state. - Camotoy
NbtMapBuilder lecternTag = LecternInventoryTranslator.getBaseLecternTag(x, y, z, 1);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) 1)
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:written_book")
.putCompound("tag", NbtMap.builder()
.putString("photoname", "")
.putString("text", "")
.build())
.build());
lecternTag.putInt("page", -1); // I'm surprisingly glad this exists - it forces Bedrock to stop reading immediately
return lecternTag.build();
}
@Override
public boolean shouldExpectLecternHandled() {
return false;
}
@Override
public void setGameRule(GeyserSession session, String name, Object value) {
session.sendDownstreamPacket(new ClientChatPacket("/gamerule " + name + " " + value));

View file

@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.GameRule;
@ -106,6 +107,32 @@ public abstract class WorldManager {
*/
public abstract int[] getBiomeDataAt(GeyserSession session, int x, int z);
/**
* Sigh. <br>
*
* So, on Java Edition, the lectern is an inventory. Java opens it and gets the contents of the book there.
* On Bedrock, the lectern contents are part of the block entity tag. Therefore, Bedrock expects to have the contents
* of the lectern ready and present in the world. If the contents are not there, it takes at least two clicks for the
* lectern to update the tag and then present itself. <br>
*
* We solve this problem by querying all loaded lecterns, where possible, and sending their information in a block entity
* tag.
*
* @param session the session of the player
* @param x the x coordinate of the lectern
* @param y the y coordinate of the lectern
* @param z the z coordinate of the lectern
* @param isChunkLoad if this is called during a chunk load or not. Changes behavior in certain instances.
* @return the Bedrock lectern block entity tag. This may not be the exact block entity tag - for example, Spigot's
* block handled must be done on the server thread, so we send the tag manually there.
*/
public abstract NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad);
/**
* @return whether we should expect lectern data to update, or if we have to fall back on a workaround.
*/
public abstract boolean shouldExpectLecternHandled();
/**
* Updates a gamerule value on the Java server
*

View file

@ -42,6 +42,7 @@ public class BlockStateValues {
private static final Int2ObjectMap<DoubleChestValue> DOUBLE_CHEST_VALUES = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<String> FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>();
private static final Map<String, NbtMap> FLOWER_POT_BLOCKS = new HashMap<>();
private static final Int2BooleanMap LECTERN_BOOK_STATES = new Int2BooleanOpenHashMap();
private static final Int2IntMap NOTEBLOCK_PITCHES = new Int2IntOpenHashMap();
private static final Int2BooleanMap IS_STICKY_PISTON = new Int2BooleanOpenHashMap();
private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap();
@ -88,6 +89,11 @@ public class BlockStateValues {
return;
}
if (entry.getKey().startsWith("minecraft:lectern")) {
LECTERN_BOOK_STATES.put(javaBlockState, entry.getKey().contains("has_book=true"));
return;
}
JsonNode notePitch = entry.getValue().get("note_pitch");
if (notePitch != null) {
NOTEBLOCK_PITCHES.put(javaBlockState, entry.getValue().get("note_pitch").intValue());
@ -197,6 +203,10 @@ public class BlockStateValues {
return FLOWER_POT_BLOCKS;
}
public static Int2BooleanMap getLecternBookStates() {
return LECTERN_BOOK_STATES;
}
/**
* The note that noteblocks output when hit is part of the block state in Java but sent as a BlockEventPacket in Bedrock.
* This gives an integer pitch that Bedrock can use.

View file

@ -53,6 +53,7 @@ import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.entity.player.SkullPlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.translators.LecternInventoryTranslator;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
@ -282,7 +283,6 @@ public class ChunkUtils {
}
String id = BlockEntityUtils.getBedrockBlockEntityId(tagName);
BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id);
Position pos = new Position((int) tag.get("x").getValue(), (int) tag.get("y").getValue(), (int) tag.get("z").getValue());
// Get Java blockstate ID from block entity position
@ -292,6 +292,14 @@ public class ChunkUtils {
blockState = section.get(pos.getX() & 0xF, pos.getY() & 0xF, pos.getZ() & 0xF);
}
if (tagName.equals("minecraft:lectern") && BlockStateValues.getLecternBookStates().get(blockState)) {
// If getLecternBookStates is false, let's just treat it like a normal block entity
bedrockBlockEntities[i] = session.getConnector().getWorldManager().getLecternDataAt(session, pos.getX(), pos.getY(), pos.getZ(), true);
i++;
continue;
}
BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id);
bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState);
// Check for custom skulls
@ -388,6 +396,29 @@ public class ChunkUtils {
}
session.sendUpstreamPacket(waterPacket);
if (BlockStateValues.getLecternBookStates().containsKey(blockState)) {
boolean lecternCachedHasBook = session.getLecternCache().contains(position);
boolean newLecternHasBook = BlockStateValues.getLecternBookStates().get(blockState);
if (!session.getConnector().getWorldManager().shouldExpectLecternHandled() && lecternCachedHasBook != newLecternHasBook) {
// Refresh the block entirely - it either has a book or no longer has a book
session.getConnector().getLogger().warning("Refreshing lectern entirely");
NbtMap newLecternTag;
if (newLecternHasBook) {
newLecternTag = session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
} else {
session.getLecternCache().remove(position);
newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build();
}
BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
} else {
// As of right now, no tag can be added asynchronously
session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
}
} else {
// Lectern has been destroyed, if it existed
session.getLecternCache().remove(position);
}
// Since Java stores bed colors/skull information as part of the namespaced ID and Bedrock stores it as a tag
// This is the only place I could find that interacts with the Java block state and block updates
// Iterates through all block entity translators and determines if the block state needs to be saved

View file

@ -66,6 +66,7 @@ public class DimensionUtils {
session.getEntityCache().removeAllEntities();
session.getItemFrameCache().clear();
session.getLecternCache().clear();
session.getSkullCache().clear();
if (session.getPendingDimSwitches().getAndIncrement() > 0) {
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);