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 ee3f07f3ede..ddee4d5a09f 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 @@ -257,6 +257,12 @@ public class GeyserSession implements CommandSender { @Setter private Entity ridingVehicleEntity; + /** + * The entity that the client is currently looking at. + */ + @Setter + private Entity mouseoverEntity; + @Setter private Int2ObjectMap craftingRecipes; private final Set unlockedRecipes; @@ -791,6 +797,11 @@ public void setSneaking(boolean sneaking) { this.sneaking = sneaking; collisionManager.updatePlayerBoundingBox(); collisionManager.updateScaffoldingFlags(); + + if (mouseoverEntity != null) { + // Horses, etc can change their property depending on if you're sneaking + InteractiveTagManager.updateTag(this, mouseoverEntity); + } } @Override diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java index 3ffc2a8f3f4..9e920385237 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java @@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket; import org.geysermc.connector.utils.CooldownUtils; +import org.geysermc.connector.utils.InteractiveTagManager; @Translator(packet = MobEquipmentPacket.class) public class BedrockMobEquipmentTranslator extends PacketTranslator { @@ -55,5 +56,10 @@ public void translate(MobEquipmentPacket packet, GeyserSession session) { // Java sends a cooldown indicator whenever you switch an item CooldownUtils.sendCooldown(session); + + // Update the interactive tag, if an entity is present + if (session.getMouseoverEntity() != null) { + InteractiveTagManager.updateTag(session, session.getMouseoverEntity()); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java index 4b9149ea69b..740ab8a379a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java @@ -31,61 +31,21 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket; import com.nukkitx.protocol.bedrock.data.entity.EntityData; -import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.InteractPacket; -import lombok.Getter; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.living.animal.horse.AbstractHorseEntity; -import org.geysermc.connector.entity.living.animal.horse.HorseEntity; -import org.geysermc.connector.entity.type.EntityType; 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.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemRegistry; - -import java.util.Arrays; -import java.util.List; +import org.geysermc.connector.utils.InteractiveTagManager; @Translator(packet = InteractPacket.class) public class BedrockInteractTranslator extends PacketTranslator { - /** - * A list of all foods a horse/donkey can eat on Java Edition. - * Used to display interactive tag if needed. - */ - private static final List DONKEY_AND_HORSE_FOODS = Arrays.asList("golden_apple", "enchanted_golden_apple", - "golden_carrot", "sugar", "apple", "wheat", "hay_block"); - - /** - * A list of all flowers. Used for feeding bees. - */ - private static final List FLOWERS = Arrays.asList("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet", - "red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose", - "sunflower", "lilac", "rose_bush", "peony"); - - /** - * All entity types that can be leashed on Java Edition - */ - private static final List LEASHABLE_MOB_TYPES = Arrays.asList(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN, - EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE, - EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM, - EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT, - EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN); - - private static final List SADDLEABLE_WHEN_TAMED_MOB_TYPES = Arrays.asList(EntityType.DONKEY, EntityType.HORSE, - EntityType.ZOMBIE_HORSE, EntityType.MULE); - /** - * A list of all foods a wolf can eat on Java Edition. - * Used to display interactive tag if needed. - */ - private static final List WOLF_FOODS = Arrays.asList("pufferfish", "tropical_fish", "chicken", "cooked_chicken", - "porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton", - "cooked_rabbit"); - @Override public void translate(InteractPacket packet, GeyserSession session) { Entity entity; @@ -121,241 +81,16 @@ public void translate(InteractPacket packet, GeyserSession session) { // Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc if (packet.getRuntimeEntityId() != 0) { Entity interactEntity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId()); - if (interactEntity == null) + session.setMouseoverEntity(interactEntity); + if (interactEntity == null) { return; - EntityDataMap entityMetadata = interactEntity.getMetadata(); - ItemEntry itemEntry = session.getPlayerInventory().getItemInHand().getItemEntry(); - String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", ""); - - // TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen - // TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food - InteractiveTag interactiveTag = InteractiveTag.NONE; - if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) { - // Unleash the entity - interactiveTag = InteractiveTag.REMOVE_LEASH; - } else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) && - ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) || - interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) { - // Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed) - interactiveTag = InteractiveTag.SADDLE; - } else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null && - session.getPlayerInventory().getItemInHand().getNbt().contains("display")) { - // Holding a named name tag - interactiveTag = InteractiveTag.NAME; - } else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) && - entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == -1L) { - // Holding a leash and the mob is leashable for sure - // (Plugins can change this behavior so that's something to look into in the far far future) - interactiveTag = InteractiveTag.LEASH; - } else { - switch (interactEntity.getEntityType()) { - case BEE: - if (FLOWERS.contains(javaIdentifierStripped)) { - interactiveTag = InteractiveTag.FEED; - } - break; - case BOAT: - interactiveTag = InteractiveTag.BOARD_BOAT; - break; - case CAT: - if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) { - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && - entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; - break; - } - break; - case CHICKEN: - if (javaIdentifierStripped.contains("seeds")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case MOOSHROOM: - // Shear the mooshroom - if (javaIdentifierStripped.equals("shears")) { - interactiveTag = InteractiveTag.MOOSHROOM_SHEAR; - break; - } - // Bowls are acceptable here - else if (javaIdentifierStripped.equals("bowl")) { - interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW; - break; - } - // Fall down to COW as this works on mooshrooms - case COW: - if (javaIdentifierStripped.equals("wheat")) { - interactiveTag = InteractiveTag.FEED; - } else if (javaIdentifierStripped.equals("bucket")) { - // Milk the cow - interactiveTag = InteractiveTag.MILK; - } - break; - case CREEPER: - if (javaIdentifierStripped.equals("flint_and_steel")) { - // Today I learned that you can ignite a creeper with flint and steel! Huh. - interactiveTag = InteractiveTag.IGNITE_CREEPER; - } - break; - case DONKEY: - case LLAMA: - case MULE: - if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED) - && javaIdentifierStripped.equals("chest")) { - // Can attach a chest - interactiveTag = InteractiveTag.ATTACH_CHEST; - break; - } - // Intentional fall-through - case HORSE: - case SKELETON_HORSE: - case TRADER_LLAMA: - case ZOMBIE_HORSE: - boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED); - if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) { - interactiveTag = InteractiveTag.OPEN_CONTAINER; - break; - } - // have another switch statement as, while these share mount attributes they don't share food - switch (interactEntity.getEntityType()) { - case LLAMA: - case TRADER_LLAMA: - if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) { - interactiveTag = InteractiveTag.FEED; - break; - } - case DONKEY: - case HORSE: - // Undead can't eat - if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) { - interactiveTag = InteractiveTag.FEED; - break; - } - } - if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) { - // Can't ride a baby - if (tamed) { - interactiveTag = InteractiveTag.RIDE_HORSE; - } else if (itemEntry.equals(ItemEntry.AIR)) { - // Can't hide an untamed entity without having your hand empty - interactiveTag = InteractiveTag.MOUNT; - } - } - break; - case FOX: - if (javaIdentifierStripped.equals("sweet_berries")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case HOGLIN: - if (javaIdentifierStripped.equals("crimson_fungus")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case MINECART: - interactiveTag = InteractiveTag.RIDE_MINECART; - break; - case MINECART_CHEST: - case MINECART_COMMAND_BLOCK: - case MINECART_HOPPER: - interactiveTag = InteractiveTag.OPEN_CONTAINER; - break; - case OCELOT: - if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case PANDA: - if (javaIdentifierStripped.equals("bamboo")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case PARROT: - if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case PIG: - if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) { - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) { - interactiveTag = InteractiveTag.MOUNT; - } - break; - case PIGLIN: - if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) { - interactiveTag = InteractiveTag.BARTER; - } - break; - case RABBIT: - if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case SHEEP: - if (javaIdentifierStripped.equals("wheat")) { - interactiveTag = InteractiveTag.FEED; - } else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) { - if (javaIdentifierStripped.equals("shears")) { - // Shear the sheep - interactiveTag = InteractiveTag.SHEAR; - } else if (javaIdentifierStripped.contains("_dye")) { - // Dye the sheep - interactiveTag = InteractiveTag.DYE; - } - } - break; - case STRIDER: - if (javaIdentifierStripped.equals("warped_fungus")) { - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) { - interactiveTag = InteractiveTag.RIDE_STRIDER; - } - break; - case TURTLE: - if (javaIdentifierStripped.equals("seagrass")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case VILLAGER: - if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0 - && entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby - interactiveTag = InteractiveTag.TRADE; - } - break; - case WANDERING_TRADER: - interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably. - break; - case WOLF: - if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) { - // Bone and untamed - can tame - interactiveTag = InteractiveTag.TAME; - } else if (WOLF_FOODS.contains(javaIdentifierStripped)) { - // Compatible food in hand - feed - // Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && - entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; - } - break; - case ZOMBIE_VILLAGER: - // We can't guarantee the existence of the weakness effect so we just always show it. - if (javaIdentifierStripped.equals("golden_apple")) { - interactiveTag = InteractiveTag.CURE; - } - break; - default: - break; - } } - session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue()); - session.getPlayerEntity().updateBedrockMetadata(session); + + InteractiveTagManager.updateTag(session, interactEntity); } else { - if (!session.getPlayerEntity().getMetadata().getString(EntityData.INTERACTIVE_TAG).isEmpty()) { + if (session.getMouseoverEntity() != null) { // No interactive tag should be sent + session.setMouseoverEntity(null); session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, ""); session.getPlayerEntity().updateBedrockMetadata(session); } @@ -384,65 +119,4 @@ else if (javaIdentifierStripped.equals("bowl")) { break; } } - - /** - * All interactive tags in enum form. For potential API usage. - */ - public enum InteractiveTag { - NONE(true), - IGNITE_CREEPER("creeper"), - EDIT, - LEAVE_BOAT("exit.boat"), - FEED, - FISH("fishing"), - MILK, - MOOSHROOM_SHEAR("mooshear"), - MOOSHROOM_MILK_STEW("moostew"), - BOARD_BOAT("ride.boat"), - RIDE_MINECART("ride.minecart"), - RIDE_HORSE("ride.horse"), - RIDE_STRIDER("ride.strider"), - SHEAR, - SIT, - STAND, - TALK, - TAME, - DYE, - CURE, - OPEN_CONTAINER("opencontainer"), - CREATE_MAP("createMap"), - TAKE_PICTURE("takepicture"), - SADDLE, - MOUNT, - BOOST, - WRITE, - LEASH, - REMOVE_LEASH("unleash"), - NAME, - ATTACH_CHEST("attachchest"), - TRADE, - POSE_ARMOR_STAND("armorstand.pose"), - EQUIP_ARMOR_STAND("armorstand.equip"), - READ, - WAKE_VILLAGER("wakevillager"), - BARTER; - - /** - * The full string that should be passed on to the client. - */ - @Getter - private final String value; - - InteractiveTag(boolean isNone) { - this.value = ""; - } - - InteractiveTag(String value) { - this.value = "action.interact." + value; - } - - InteractiveTag() { - this.value = "action.interact." + name().toLowerCase(); - } - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java index e3c64d55ff6..73ef28263f9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java @@ -32,6 +32,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityMetadataPacket; +import org.geysermc.connector.utils.InteractiveTagManager; import org.geysermc.connector.utils.LanguageUtils; @Translator(packet = ServerEntityMetadataPacket.class) @@ -61,5 +62,10 @@ public void translate(ServerEntityMetadataPacket packet, GeyserSession session) } entity.updateBedrockMetadata(session); + + // Update the interactive tag, if necessary + if (session.getMouseoverEntity() != null && session.getMouseoverEntity().getEntityId() == entity.getEntityId()) { + InteractiveTagManager.updateTag(session, entity); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java b/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java new file mode 100644 index 00000000000..0b59efff334 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2019-2021 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.utils; + +import com.google.common.collect.ImmutableSet; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import lombok.Getter; +import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.living.animal.horse.HorseEntity; +import org.geysermc.connector.entity.type.EntityType; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.item.ItemEntry; + +import java.util.EnumSet; +import java.util.Set; + +public class InteractiveTagManager { + /** + * A list of all foods a horse/donkey can eat on Java Edition. + * Used to display interactive tag if needed. + */ + private static final Set DONKEY_AND_HORSE_FOODS = ImmutableSet.of("golden_apple", "enchanted_golden_apple", + "golden_carrot", "sugar", "apple", "wheat", "hay_block"); + + /** + * A list of all flowers. Used for feeding bees. + */ + private static final Set FLOWERS = ImmutableSet.of("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet", + "red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose", + "sunflower", "lilac", "rose_bush", "peony"); + + /** + * All entity types that can be leashed on Java Edition + */ + private static final Set LEASHABLE_MOB_TYPES = EnumSet.of(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN, + EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE, + EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM, + EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT, + EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN); + + private static final Set SADDLEABLE_WHEN_TAMED_MOB_TYPES = EnumSet.of(EntityType.DONKEY, EntityType.HORSE, + EntityType.ZOMBIE_HORSE, EntityType.MULE); + + /** + * A list of all foods a wolf can eat on Java Edition. + * Used to display interactive tag if needed. + */ + private static final Set WOLF_FOODS = ImmutableSet.of("pufferfish", "tropical_fish", "chicken", "cooked_chicken", + "porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton", + "cooked_rabbit"); + + /** + * Update the suggestion that the client currently has on their screen for this entity (for example, "Feed" or "Ride") + * + * @param session the Bedrock client session + * @param interactEntity the entity that the client is currently facing. + */ + public static void updateTag(GeyserSession session, Entity interactEntity) { + EntityDataMap entityMetadata = interactEntity.getMetadata(); + ItemEntry itemEntry = session.getPlayerInventory().getItemInHand().getItemEntry(); + String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", ""); + + // TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen + // TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food + InteractiveTag interactiveTag = InteractiveTag.NONE; + if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) { + // Unleash the entity + interactiveTag = InteractiveTag.REMOVE_LEASH; + } else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) && + ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !session.isSneaking()) || + interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) { + // Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed) + interactiveTag = InteractiveTag.SADDLE; + } else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null && + session.getPlayerInventory().getItemInHand().getNbt().contains("display")) { + // Holding a named name tag + interactiveTag = InteractiveTag.NAME; + } else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) && + entityMetadata.getLong(EntityData.LEASH_HOLDER_EID, -1L) == -1L) { + // Holding a leash and the mob is leashable for sure + // (Plugins can change this behavior so that's something to look into in the far far future) + interactiveTag = InteractiveTag.LEASH; + } else { + switch (interactEntity.getEntityType()) { + case BEE: + if (FLOWERS.contains(javaIdentifierStripped)) { + interactiveTag = InteractiveTag.FEED; + } + break; + case BOAT: + interactiveTag = InteractiveTag.BOARD_BOAT; + break; + case CAT: + if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) { + interactiveTag = InteractiveTag.FEED; + } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && + entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) { + // Tamed and owned by player - can sit/stand + interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; + break; + } + break; + case CHICKEN: + if (javaIdentifierStripped.contains("seeds")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case MOOSHROOM: + // Shear the mooshroom + if (javaIdentifierStripped.equals("shears")) { + interactiveTag = InteractiveTag.MOOSHROOM_SHEAR; + break; + } + // Bowls are acceptable here + else if (javaIdentifierStripped.equals("bowl")) { + interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW; + break; + } + // Fall down to COW as this works on mooshrooms + case COW: + if (javaIdentifierStripped.equals("wheat")) { + interactiveTag = InteractiveTag.FEED; + } else if (javaIdentifierStripped.equals("bucket")) { + // Milk the cow + interactiveTag = InteractiveTag.MILK; + } + break; + case CREEPER: + if (javaIdentifierStripped.equals("flint_and_steel")) { + // Today I learned that you can ignite a creeper with flint and steel! Huh. + interactiveTag = InteractiveTag.IGNITE_CREEPER; + } + break; + case DONKEY: + case LLAMA: + case MULE: + if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED) + && javaIdentifierStripped.equals("chest")) { + // Can attach a chest + interactiveTag = InteractiveTag.ATTACH_CHEST; + break; + } + // Intentional fall-through + case HORSE: + case SKELETON_HORSE: + case TRADER_LLAMA: + case ZOMBIE_HORSE: + boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED); + if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) { + interactiveTag = InteractiveTag.OPEN_CONTAINER; + break; + } + // have another switch statement as, while these share mount attributes they don't share food + switch (interactEntity.getEntityType()) { + case LLAMA: + case TRADER_LLAMA: + if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) { + interactiveTag = InteractiveTag.FEED; + break; + } + case DONKEY: + case HORSE: + // Undead can't eat + if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) { + interactiveTag = InteractiveTag.FEED; + break; + } + } + if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) { + // Can't ride a baby + if (tamed) { + interactiveTag = InteractiveTag.RIDE_HORSE; + } else if (itemEntry.equals(ItemEntry.AIR)) { + // Can't hide an untamed entity without having your hand empty + interactiveTag = InteractiveTag.MOUNT; + } + } + break; + case FOX: + if (javaIdentifierStripped.equals("sweet_berries")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case HOGLIN: + if (javaIdentifierStripped.equals("crimson_fungus")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case MINECART: + interactiveTag = InteractiveTag.RIDE_MINECART; + break; + case MINECART_CHEST: + case MINECART_COMMAND_BLOCK: + case MINECART_HOPPER: + interactiveTag = InteractiveTag.OPEN_CONTAINER; + break; + case OCELOT: + if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case PANDA: + if (javaIdentifierStripped.equals("bamboo")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case PARROT: + if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case PIG: + if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) { + interactiveTag = InteractiveTag.FEED; + } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) { + interactiveTag = InteractiveTag.MOUNT; + } + break; + case PIGLIN: + if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) { + interactiveTag = InteractiveTag.BARTER; + } + break; + case RABBIT: + if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case SHEEP: + if (javaIdentifierStripped.equals("wheat")) { + interactiveTag = InteractiveTag.FEED; + } else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) { + if (javaIdentifierStripped.equals("shears")) { + // Shear the sheep + interactiveTag = InteractiveTag.SHEAR; + } else if (javaIdentifierStripped.contains("_dye")) { + // Dye the sheep + interactiveTag = InteractiveTag.DYE; + } + } + break; + case STRIDER: + if (javaIdentifierStripped.equals("warped_fungus")) { + interactiveTag = InteractiveTag.FEED; + } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) { + interactiveTag = InteractiveTag.RIDE_STRIDER; + } + break; + case TURTLE: + if (javaIdentifierStripped.equals("seagrass")) { + interactiveTag = InteractiveTag.FEED; + } + break; + case VILLAGER: + if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0 + && entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby + interactiveTag = InteractiveTag.TRADE; + } + break; + case WANDERING_TRADER: + interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably. + break; + case WOLF: + if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) { + // Bone and untamed - can tame + interactiveTag = InteractiveTag.TAME; + } else if (WOLF_FOODS.contains(javaIdentifierStripped)) { + // Compatible food in hand - feed + // Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this + interactiveTag = InteractiveTag.FEED; + } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && + entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) { + // Tamed and owned by player - can sit/stand + interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; + } + break; + case ZOMBIE_VILLAGER: + // We can't guarantee the existence of the weakness effect so we just always show it. + if (javaIdentifierStripped.equals("golden_apple")) { + interactiveTag = InteractiveTag.CURE; + } + break; + default: + break; + } + } + session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue()); + session.getPlayerEntity().updateBedrockMetadata(session); + } + + /** + * All interactive tags in enum form. For potential API usage. + */ + public enum InteractiveTag { + NONE(true), + IGNITE_CREEPER("creeper"), + EDIT, + LEAVE_BOAT("exit.boat"), + FEED, + FISH("fishing"), + MILK, + MOOSHROOM_SHEAR("mooshear"), + MOOSHROOM_MILK_STEW("moostew"), + BOARD_BOAT("ride.boat"), + RIDE_MINECART("ride.minecart"), + RIDE_HORSE("ride.horse"), + RIDE_STRIDER("ride.strider"), + SHEAR, + SIT, + STAND, + TALK, + TAME, + DYE, + CURE, + OPEN_CONTAINER("opencontainer"), + CREATE_MAP("createMap"), + TAKE_PICTURE("takepicture"), + SADDLE, + MOUNT, + BOOST, + WRITE, + LEASH, + REMOVE_LEASH("unleash"), + NAME, + ATTACH_CHEST("attachchest"), + TRADE, + POSE_ARMOR_STAND("armorstand.pose"), + EQUIP_ARMOR_STAND("armorstand.equip"), + READ, + WAKE_VILLAGER("wakevillager"), + BARTER; + + /** + * The full string that should be passed on to the client. + */ + @Getter + private final String value; + + InteractiveTag(boolean isNone) { + this.value = ""; + } + + InteractiveTag(String value) { + this.value = "action.interact." + value; + } + + InteractiveTag() { + this.value = "action.interact." + name().toLowerCase(); + } + } +}