From 9846058377e12456e47c47c8cfb19e8a0910c57b Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+DoctorMacc@users.noreply.github.com> Date: Sat, 2 May 2020 16:44:05 -0400 Subject: [PATCH] Add item frames (#415) * Initial attempt * Item frames 'work' * Blocks in the item frames work * Remove commented code * Small changes * More progress * Whittling down * Fix swords, etc * NBT data implemented * Remove unused import * Add item frame item removing; add checks for removing item frames * Add requested changes; clean up logic * Add license * Always delay item frame updates by 500 milliseconds * Switch to per-session item frame cache * Revert item translator refactoring --- .../connector/entity/ItemFrameEntity.java | 226 ++++++++++++++++++ .../connector/entity/type/EntityType.java | 7 +- .../network/session/GeyserSession.java | 8 + ...BedrockInventoryTransactionTranslator.java | 25 ++ .../BedrockItemFrameDropItemTranslator.java | 57 +++++ .../translators/block/BlockTranslator.java | 34 ++- .../network/translators/item/ItemEntry.java | 1 - .../spawn/JavaSpawnObjectTranslator.java | 8 +- .../geysermc/connector/utils/ChunkUtils.java | 17 ++ .../connector/utils/DimensionUtils.java | 1 + 10 files changed, 370 insertions(+), 14 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemFrameDropItemTranslator.java diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java new file mode 100644 index 00000000..08831a45 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java @@ -0,0 +1,226 @@ +/* + * 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.entity; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.entity.type.object.HangingDirection; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.CompoundTagBuilder; +import com.nukkitx.nbt.tag.CompoundTag; +import com.nukkitx.protocol.bedrock.data.ItemData; +import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; +import com.nukkitx.protocol.bedrock.packet.StartGamePacket; +import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; +import org.geysermc.connector.entity.type.EntityType; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.Translators; +import org.geysermc.connector.network.translators.block.BlockTranslator; +import org.geysermc.connector.network.translators.item.ItemEntry; +import org.geysermc.connector.utils.Toolbox; + +import java.util.concurrent.TimeUnit; + +/** + * Item frames are an entity in Java but a block entity in Bedrock. + */ +public class ItemFrameEntity extends Entity { + + /** + * Used for getting the Bedrock block position. + * Blocks deal with integers whereas entities deal with floats. + */ + private final Vector3i bedrockPosition; + /** + * Specific block 'state' we are emulating in Bedrock. + */ + private final int bedrockRuntimeId; + /** + * Rotation of item in frame. + */ + private float rotation = 0.0f; + /** + * Cached item frame's Bedrock compound tag. + */ + private CompoundTag cachedTag; + + public ItemFrameEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, HangingDirection direction) { + super(entityId, geyserId, entityType, position, motion, rotation); + CompoundTagBuilder builder = CompoundTag.builder(); + builder.tag(CompoundTag.builder() + .stringTag("name", "minecraft:frame") + .intTag("version", BlockTranslator.getBlockStateVersion()) + .tag(CompoundTag.builder() + .intTag("facing_direction", direction.ordinal()) + .byteTag("item_frame_map_bit", (byte) 0) + .build("states")) + .build("block")); + builder.shortTag("id", (short) 199); + bedrockRuntimeId = BlockTranslator.getItemFrame(builder.buildRootTag()); + bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ()); + } + + @Override + public void spawnEntity(GeyserSession session) { + session.getItemFrameCache().put(bedrockPosition, entityId); + updateBlock(session); + valid = true; + session.getConnector().getLogger().debug("Spawned item frame at location " + bedrockPosition + " with java id " + entityId); + } + + @Override + public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { + if (entityMetadata.getId() == 7 && entityMetadata.getValue() != null) { + ItemData itemData = Translators.getItemTranslator().translateToBedrock(session, (ItemStack) entityMetadata.getValue()); + ItemEntry itemEntry = Translators.getItemTranslator().getItem((ItemStack) entityMetadata.getValue()); + CompoundTagBuilder builder = CompoundTag.builder(); + + String blockName = ""; + for (StartGamePacket.ItemEntry startGamePacketItemEntry: Toolbox.ITEMS) { + if (startGamePacketItemEntry.getId() == (short) itemEntry.getBedrockId()) { + blockName = startGamePacketItemEntry.getIdentifier(); + break; + } + } + + builder.byteTag("Count", (byte) itemData.getCount()); + if (itemData.getTag() != null) { + builder.tag(itemData.getTag().toBuilder().build("tag")); + } + builder.shortTag("Damage", itemData.getDamage()); + builder.stringTag("Name", blockName); + CompoundTagBuilder tag = getDefaultTag().toBuilder(); + tag.tag(builder.build("Item")); + tag.floatTag("ItemDropChance", 1.0f); + tag.floatTag("ItemRotation", rotation); + cachedTag = tag.buildRootTag(); + updateBlock(session); + } + else if (entityMetadata.getId() == 7 && entityMetadata.getValue() == null && cachedTag != null) { + cachedTag = getDefaultTag(); + updateBlock(session); + } + else if (entityMetadata.getId() == 8) { + rotation = ((int) entityMetadata.getValue()) * 45; + if (cachedTag == null) { + updateBlock(session); + return; + } + CompoundTagBuilder builder = cachedTag.toBuilder(); + builder.floatTag("ItemRotation", rotation); + cachedTag = builder.buildRootTag(); + updateBlock(session); + } + else { + updateBlock(session); + } + } + + @Override + public boolean despawnEntity(GeyserSession session) { + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(bedrockPosition); + updateBlockPacket.setRuntimeId(0); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NONE); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); + session.getUpstream().sendPacket(updateBlockPacket); + session.getItemFrameCache().remove(position, entityId); + valid = false; + return true; + } + + private CompoundTag getDefaultTag() { + CompoundTagBuilder builder = CompoundTag.builder(); + builder.intTag("x", bedrockPosition.getX()); + builder.intTag("y", bedrockPosition.getY()); + builder.intTag("z", bedrockPosition.getZ()); + builder.byteTag("isMovable", (byte) 1); + builder.stringTag("id", "ItemFrame"); + return builder.buildRootTag(); + } + + /** + * Updates the item frame as a block + * @param session GeyserSession. + */ + public void updateBlock(GeyserSession session) { + // Delay is required, or else loading in frames on chunk load is sketchy at best + session.getConnector().getGeneralThreadPool().schedule(() -> { + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(bedrockPosition); + updateBlockPacket.setRuntimeId(bedrockRuntimeId); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NONE); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); + session.getUpstream().sendPacket(updateBlockPacket); + + BlockEntityDataPacket blockEntityDataPacket = new BlockEntityDataPacket(); + blockEntityDataPacket.setBlockPosition(bedrockPosition); + if (cachedTag != null) { + blockEntityDataPacket.setData(cachedTag); + } else { + blockEntityDataPacket.setData(getDefaultTag()); + } + + session.getUpstream().sendPacket(blockEntityDataPacket); + }, 500, TimeUnit.MILLISECONDS); + } + + /** + * Finds the Java entity ID of an item frame from its Bedrock position. + * @param position position of item frame in Bedrock. + * @param session GeyserSession. + * @return Java entity ID or -1 if not found. + */ + public static long getItemFrameEntityId(GeyserSession session, Vector3i position) { + return session.getItemFrameCache().getOrDefault(position, -1); + } + + /** + * Determines if the position contains an item frame. + * Does largely the same thing as getItemFrameEntityId, but for speed purposes is implemented separately, + * since every block destroy packet has to check for an item frame. + * @param position position of block. + * @param session GeyserSession. + * @return true if position contains item frame, false if not. + */ + public static boolean positionContainsItemFrame(GeyserSession session, Vector3i position) { + return session.getItemFrameCache().containsKey(position); + } + + /** + * Force-remove from the position-to-ID map so it doesn't cause conflicts. + * @param session GeyserSession. + * @param position position of the removed item frame. + */ + public static void removePosition(GeyserSession session, Vector3i position) { + session.getItemFrameCache().remove(position); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java index 7a7e13c0..263d0041 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java +++ b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java @@ -147,7 +147,12 @@ public enum EntityType { COD(AbstractFishEntity.class, 112, 0.25f, 0.5f), PANDA(PandaEntity.class, 113, 1.25f, 1.125f, 1.825f), FOX(FoxEntity.class, 121, 0.5f, 1.25f), - BEE(BeeEntity.class, 122, 0.6f, 0.6f); + BEE(BeeEntity.class, 122, 0.6f, 0.6f), + + /** + * Item frames are handled differently since they are a block in Bedrock. + */ + ITEM_FRAME(ItemFrameEntity.class, 0, 0, 0); private Class entityClass; private final int type; diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 186576e1..1ea3a1c0 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -49,6 +49,8 @@ import com.nukkitx.protocol.bedrock.data.GamePublishSetting; import com.nukkitx.protocol.bedrock.data.GameRuleData; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.packet.*; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import lombok.Getter; import lombok.Setter; import org.geysermc.common.AuthType; @@ -100,6 +102,12 @@ public class GeyserSession implements CommandSender { @Setter private TeleportCache teleportCache; + /** + * A map of Vector3i positions to Java entity IDs. + * Used for translating Bedrock block actions to Java entity actions. + */ + private final Object2LongMap itemFrameCache = new Object2LongOpenHashMap<>(); + private DataCache javaPacketCache; @Setter diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index 3f6eba55..7890ead7 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -27,11 +27,13 @@ package org.geysermc.connector.network.translators.bedrock; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPlaceBlockPacket; import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.inventory.Inventory; 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.network.translators.Translators; +import org.geysermc.connector.network.translators.block.BlockTranslator; import org.geysermc.connector.network.translators.item.ItemTranslator; import org.geysermc.connector.utils.InventoryUtils; @@ -67,6 +69,20 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { + + @Override + public void translate(ItemFrameDropItemPacket packet, GeyserSession session) { + // I hope that, when we die, God (or whoever is waiting for us) tells us exactly why this code exists + // The packet sends the Y coordinate (and just the Y coordinate) divided by two, and it's negative if it needs to be subtracted by one + int y; + if (packet.getBlockPosition().getY() > 0) { + y = packet.getBlockPosition().getY() * 2; + } else { + y = (packet.getBlockPosition().getY() * -2) - 1; + } + Vector3i position = Vector3i.from(packet.getBlockPosition().getX(), y, packet.getBlockPosition().getZ()); + ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, position), + InteractAction.ATTACK, Hand.MAIN_HAND); + session.getDownstream().getSession().send(interactPacket); + } + +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java index 5b5d5c9c..650a494b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java @@ -32,16 +32,7 @@ import com.nukkitx.nbt.NbtUtils; import com.nukkitx.nbt.stream.NBTInputStream; import com.nukkitx.nbt.tag.CompoundTag; import com.nukkitx.nbt.tag.ListTag; -import it.unimi.dsi.fastutil.ints.Int2BooleanMap; -import it.unimi.dsi.fastutil.ints.Int2BooleanOpenHashMap; -import it.unimi.dsi.fastutil.ints.Int2DoubleMap; -import it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap; -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import org.geysermc.connector.GeyserConnector; @@ -61,6 +52,7 @@ public class BlockTranslator { private static final Int2ObjectMap BEDROCK_TO_JAVA_BLOCK_MAP = new Int2ObjectOpenHashMap<>(); private static final Map JAVA_ID_BLOCK_MAP = new HashMap<>(); private static final IntSet WATERLOGGED = new IntOpenHashSet(); + private static final Object2IntMap ITEM_FRAMES = new Object2IntOpenHashMap<>(); // Bedrock carpet ID, used in LlamaEntity.java for decoration public static final int CARPET = 171; @@ -202,6 +194,16 @@ public class BlockTranslator { paletteList.addAll(blockStateMap.values()); // Add any missing mappings that could crash the client + // Loop around again to find all item frame runtime IDs + int frameRuntimeId = 0; + for (CompoundTag tag : paletteList) { + CompoundTag blockTag = tag.getCompound("block"); + if (blockTag.getString("name").equals("minecraft:frame")) { + ITEM_FRAMES.put(tag, frameRuntimeId); + } + frameRuntimeId++; + } + BLOCKS = new ListTag<>("", CompoundTag.class, paletteList); } @@ -253,6 +255,18 @@ public class BlockTranslator { return BEDROCK_TO_JAVA_BLOCK_MAP.get(bedrockId); } + public static int getItemFrame(CompoundTag tag) { + return ITEM_FRAMES.getOrDefault(tag, -1); + } + + public static boolean isItemFrame(int bedrockBlockRuntimeId) { + return ITEM_FRAMES.values().contains(bedrockBlockRuntimeId); + } + + public static int getBlockStateVersion() { + return BLOCK_STATE_VERSION; + } + public static BlockState getJavaBlockState(String javaId) { return JAVA_ID_BLOCK_MAP.get(javaId); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java index e579c20e..e9815ba6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java @@ -36,7 +36,6 @@ public class ItemEntry { private final String javaIdentifier; private final int javaId; - private final int bedrockId; private final int bedrockData; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnObjectTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnObjectTranslator.java index c3998f87..d544e24b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnObjectTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnObjectTranslator.java @@ -29,8 +29,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import com.github.steveice10.mc.protocol.data.game.entity.type.object.FallingBlockData; +import com.github.steveice10.mc.protocol.data.game.entity.type.object.HangingDirection; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.FallingBlockEntity; +import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; @@ -46,8 +48,6 @@ public class JavaSpawnObjectTranslator extends PacketTranslator entityConstructor = entityClass.getConstructor(long.class, long.class, EntityType.class, Vector3f.class, Vector3f.class, Vector3f.class); diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java index c1ddb60a..1347aed4 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -39,6 +39,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.Getter; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.block.entity.*; import org.geysermc.connector.network.translators.Translators; @@ -49,6 +51,7 @@ import org.geysermc.connector.world.chunk.ChunkSection; import java.util.HashMap; import java.util.Map; +import static org.geysermc.connector.network.translators.block.BlockTranslator.AIR; import static org.geysermc.connector.network.translators.block.BlockTranslator.BEDROCK_WATER_ID; public class ChunkUtils { @@ -148,6 +151,20 @@ public class ChunkUtils { } public static void updateBlock(GeyserSession session, BlockState blockState, Vector3i position) { + + // Checks for item frames so they aren't tripped up and removed + if (ItemFrameEntity.positionContainsItemFrame(session, position) && blockState.equals(AIR)) { + ((ItemFrameEntity) session.getEntityCache().getEntityByJavaId(ItemFrameEntity.getItemFrameEntityId(session, position))).updateBlock(session); + return; + } else if (ItemFrameEntity.positionContainsItemFrame(session, position)) { + Entity entity = session.getEntityCache().getEntityByJavaId(ItemFrameEntity.getItemFrameEntityId(session, position)); + if (entity != null) { + session.getEntityCache().removeEntity(entity, false); + } else { + ItemFrameEntity.removePosition(session, position); + } + } + int blockId = BlockTranslator.getBedrockBlockId(blockState); UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index 9d874f13..6dd182a7 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -38,6 +38,7 @@ public class DimensionUtils { return; session.getEntityCache().removeAllEntities(); + session.getItemFrameCache().clear(); if (session.getPendingDimSwitches().getAndIncrement() > 0) { ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); }