import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.wrappers.EnumWrappers; import com.comphenix.protocol.wrappers.Pair; import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.google.common.collect.Sets; import lombok.Getter; import net.minecraft.core.Registry; import net.minecraft.world.entity.EntityType; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; @Getter public class FakeArmorstand { private static final int ARMORSTAND_ENTITY_TYPE_ID = Registry.ENTITY_TYPE.getId(EntityType.ARMOR_STAND); // EntityType.ARMOR_STAND.getTypeId() apparently spawns a ghast?? private static int ENTITY_ID = 50000; private final PacketListener packetListener; private final int entityId; private final UUID entityUUID; private final Set viewers = Sets.newConcurrentHashSet(); private final PacketContainer metadataPacket; private Location location; private byte metadataBitmask; private String customName; private boolean customNameVisible; private Consumer clickAction = (player -> { }); public FakeArmorstand(JavaPlugin main, Location location) { this.entityId = getNextEntityId(); this.entityUUID = UUID.randomUUID(); this.location = location; metadataPacket = prepareMetadata(); packetListener = new PacketAdapter(main, PacketType.Play.Client.USE_ENTITY) { @Override public void onPacketReceiving(PacketEvent event) { PacketContainer packet = event.getPacket(); int entityId = packet.getIntegers().read(0); if (entityId == FakeArmorstand.this.entityId) { clickAction.accept(event.getPlayer()); event.setCancelled(true); } } }; ProtocolLibrary.getProtocolManager().addPacketListener(packetListener); } public static int getNextEntityId() { return ENTITY_ID++; } public void onClick(Consumer clickAction) { this.clickAction = this.clickAction.andThen(clickAction); } public void addViewer(Player player) { viewers.add(player.getUniqueId()); PacketContainer spawnPacket = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY); spawnPacket.getIntegers().writeSafely(0, entityId)/*.writeSafely(1, ARMORSTAND_ENTITY_TYPE_ID)*/; spawnPacket.getEntityTypeModifier().writeSafely(0, org.bukkit.entity.EntityType.ARMOR_STAND); spawnPacket.getUUIDs().writeSafely(0, entityUUID); spawnPacket.getDoubles().writeSafely(0, location.getX()).writeSafely(1, location.getY()).writeSafely(2, location.getZ()); spawnPacket.getBytes().writeSafely(0, (byte) Math.floor(location.getYaw() * 256.0F / 360.0F)).writeSafely(1, (byte) Math.floor(location.getPitch() * 256.0F / 360.0F)).writeSafely(2, (byte) Math.floor(location.getYaw() * 256.0F / 360.0F)); try { ProtocolLibrary.getProtocolManager().sendServerPacket(player, spawnPacket); ProtocolLibrary.getProtocolManager().sendServerPacket(player, metadataPacket); } catch (InvocationTargetException e) { throw new RuntimeException(e); } // debugPacket(spawnPacket); } private void debugPacket(PacketContainer packet) { System.out.println("--- DEBUGGING PACKET ---"); System.out.println("Packet type : " + packet.getType()); Object handle = packet.getHandle(); System.out.println("Packet handle : " + handle.getClass().getName()); System.out.println("Handle objects and type: "); Class clazz = handle.getClass(); for(Field field : clazz.getDeclaredFields()) { field.setAccessible(true); try { System.out.println(field.getName() + " (" + field.getType().getName() + ") : " + field.get(handle)); } catch (IllegalAccessException e) { e.printStackTrace(); } } System.out.println("------------------------"); } public void removeViewer(Player player) { removeViewer(player.getUniqueId()); } public void addViewer(UUID uuid) { viewers.add(uuid); } public void removeViewer(UUID uuid) { removeViewer(uuid, true); } public void removeViewer(UUID uuid, boolean sendPacket) { viewers.remove(uuid); if (sendPacket) { Player player = Bukkit.getPlayer(uuid); if (player == null) return; PacketContainer removePacket = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY); removePacket.getIntLists().writeSafely(0, List.of(entityId)); try { ProtocolLibrary.getProtocolManager().sendServerPacket(player, removePacket); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } } public void setHelmet(ItemStack item) { PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_EQUIPMENT); packet.getSlotStackPairLists().writeSafely(0, Collections.singletonList(new Pair<>(EnumWrappers.ItemSlot.HEAD, item))); packet.getIntegers().writeSafely(0, entityId); sendPacketToAllViewers(packet); } private PacketContainer prepareMetadata() { PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA); packet.getIntegers().write(0, entityId); return packet; } public void moveTo(Location location) { double distanceX = location.getX() - this.location.getX(); double distanceY = location.getY() - this.location.getY(); double distanceZ = location.getZ() - this.location.getZ(); if (Math.abs(distanceX) > 8 || Math.abs(distanceY) > 8 || Math.abs(distanceZ) > 8) { teleport(location); } else { moveRelTo(location); } } public void moveRelTo(Location location) { double deltaX = (this.location.getX() * 32 - location.getX() * 32) * 128; double deltaY = (this.location.getY() * 32 - location.getY() * 32) * 128; double deltaZ = (this.location.getZ() * 32 - location.getZ() * 32) * 128; this.location = location; PacketContainer movementPacket = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE); movementPacket.getIntegers().write(0, entityId); movementPacket.getShorts().writeSafely(0, (short) deltaX); movementPacket.getShorts().writeSafely(1, (short) deltaY); movementPacket.getShorts().writeSafely(2, (short) deltaZ); movementPacket.getBooleans().writeSafely(0, false); sendPacketToAllViewers(movementPacket); } public void teleport(Location location) { this.location = location; PacketContainer teleportPacket = new PacketContainer(PacketType.Play.Server.ENTITY_TELEPORT); teleportPacket.getIntegers().write(0, entityId); teleportPacket.getDoubles().writeSafely(0, location.getX()); teleportPacket.getDoubles().writeSafely(1, location.getY()); teleportPacket.getDoubles().writeSafely(2, location.getZ()); teleportPacket.getBytes().writeSafely(0, (byte) (location.getYaw() * 256.0F / 360.0F)); teleportPacket.getBytes().writeSafely(1, (byte) (location.getPitch() * 256.0F / 360.0F)); teleportPacket.getBooleans().writeSafely(0, false); // is on ground sendPacketToAllViewers(teleportPacket); } public void setMetadatata(MetadataType type, boolean value) { if (value) { metadataBitmask |= type.getValue(); } else { metadataBitmask &= ~type.getValue(); } } private void sendPacketToAllViewers(PacketContainer packet) { for (UUID viewer : viewers) { Player player = Bukkit.getPlayer(viewer); if (player == null) { viewers.remove(viewer); continue; } try { ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } } public void updateMetadata() { EasyMetadataPacket easyMetadataPacket = new EasyMetadataPacket(null); easyMetadataPacket.write(0, metadataBitmask); if (customName == null || customName.isEmpty()) easyMetadataPacket.writeEmptyData(2, WrappedChatComponent.fromText("")); else easyMetadataPacket.writeOptional(2, WrappedChatComponent.fromText(customName)); easyMetadataPacket.write(3, customName != null && !customName.isEmpty()); // Name is visible metadataPacket.getWatchableCollectionModifier().write(0, easyMetadataPacket.export()); sendPacketToAllViewers(metadataPacket); } public void dispose() { PacketContainer removePacket = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY); removePacket.getIntLists().writeSafely(0, List.of(entityId)); sendPacketToAllViewers(removePacket); viewers.clear(); ProtocolLibrary.getProtocolManager().removePacketListener(packetListener); } public void setCustomName(String customName) { this.customName = ChatColor.translateAlternateColorCodes('&', customName); updateMetadata(); } public void setCustomNameVisible(boolean customNameVisible) { this.customNameVisible = customNameVisible; updateMetadata(); } public enum MetadataType { ON_FIRE(0x01), CROUCHED(0x02), // UNUSED(0x04), SPRINTING(0x08), SWIMMING(0x10), INVISIBLE(0x20), GLOWING(0x40), FLYING_WITH_ELYTRA(0x80); private final byte value; MetadataType(int value) { this.value = (byte) value; } public byte getValue() { return value; } } }