[1.12.2-R0.1] How do you set a fake player's elytra flight metadata?

Discussion in 'Spigot Plugin Development' started by GreatThane, Jun 5, 2018.

  1. Hello, I'm having an issue with forcing the elytra flight animation of an artificially spawned player character.
    I'm currently attempting my own version of a gun plugin (passion project, not meant to be original or be anything revolutionary), and I thought it would be interesting to create a "prone" mode, allowing players to lie low and avoid some incoming damage by lying flat against the ground.
    Currently, I'm able to replicate the effect I want on an official player (myself), except I have one small issue: Because of how the elytra work in-game, your body will always point in the direction you are facing in both axes. this makes it so if you look down, your character model enters the ground, and if you look up, your model stands straight again. This is really a shame because, from the first person, it is exactly the effect I want, with the camera close to the ground with limited movement speed yet full control of the mouse.

    As such, I figured I could fix this issue by applying the effect to the player and turning them invisible, and making a duplicate player updated every tick with their location, with a catch being the pitch would always be 0.
    Unfortunately, I cannot get this to work for me at all. I have tried numerous methods to create a fake player and apply the animation, both through various libraries including protocollib, and direct NMS. I'm able to add and remove the fake player easily enough, but the animation refuses to play for these fake players.
    This is my current attempt, which as far as I can tell is the closest I've gotten it to functioning:
    Code (Java):

    private static List<UUID> coolDowns = new ArrayList<>();

    @EventHandler
    public void onClick(PlayerInteractEvent event) {
        if (event.getPlayer().hasPermission("thane.prone")) {
            if (event.getPlayer().getEyeLocation().getPitch() > 80) {
                if (event.getPlayer().isSneaking()) {
                    if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
                        if (event.getBlockFace() == BlockFace.UP) {
                            if (!Gun.isGun(event.getPlayer().getInventory().getItemInMainHand())) {
                                if (!coolDowns.contains(event.getPlayer().getUniqueId())) {
                                    coolDowns.add(event.getPlayer().getUniqueId());
                                    Bukkit.getScheduler().runTaskLaterAsynchronously(ThaneGuns.getPlugin(), () ->
                                            coolDowns.remove(event.getPlayer().getUniqueId()), 10);
                                    prone(event);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private static HashMap<UUID, BukkitTask> proneStates = new HashMap<>();
    private static HashMap<UUID, EntityPlayer> fakePlayers = new HashMap<>();

    private static void prone(PlayerInteractEvent event) {
        if (proneStates.containsKey(event.getPlayer().getUniqueId())) {
            // is in prone
            dismiss(event.getPlayer());
        } else {
            // is not in prone
            event.getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.SPEED, Integer.MAX_VALUE, 1, false, false));
            event.getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 1, false, false));

            MinecraftServer mcServer = ((CraftServer) Bukkit.getServer()).getServer();
            WorldServer nmsWorld = ((CraftWorld) event.getPlayer().getWorld()).getHandle();

            GameProfile gameProfile = new GameProfile(event.getPlayer().getUniqueId(), "");
            EntityPlayer fake = new EntityPlayer(mcServer, nmsWorld, gameProfile, new PlayerInteractManager(nmsWorld));
            fake.setLocation(event.getPlayer().getLocation().getX(), event.getPlayer().getLocation().getY(),
                    event.getPlayer().getLocation().getZ(), event.getPlayer().getLocation().getYaw(), event.getPlayer().getLocation().getPitch());
            PacketPlayOutPlayerInfo pi = new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, fake);
            PacketPlayOutNamedEntitySpawn spawn = new PacketPlayOutNamedEntitySpawn(fake);

            EntityPlayer target = ((CraftPlayer) event.getPlayer()).getHandle();

            for (Player player : Bukkit.getOnlinePlayers()) {
                EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
                nmsPlayer.playerConnection.sendPacket(pi);
                nmsPlayer.playerConnection.sendPacket(spawn);
            }
            fakePlayers.put(event.getPlayer().getUniqueId(), fake);
            BukkitTask task = Bukkit.getScheduler().runTaskTimer(ThaneGuns.getPlugin(), () -> {
                // here we have the fake and real player. the fake does not apply the animation,
                // yet for some reason the original player does just fine.
                fake.setFlag(7, true);
                target.setFlag(7, true);

            }, 2,1);
            proneStates.put(event.getPlayer().getUniqueId(), task);
        }
    }

    private static void dismiss(Player p) {
        proneStates.get(p.getUniqueId()).cancel();
        proneStates.remove(p.getUniqueId());
        p.removePotionEffect(PotionEffectType.SLOW);
        p.removePotionEffect(PotionEffectType.INVISIBILITY);
        PacketPlayOutPlayerInfo pi = new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, fakePlayers.get(p.getUniqueId()));
        int[] id = {};
        for (Player player : Bukkit.getOnlinePlayers()) {
            if (!player.equals(p)) {
                if (((CraftPlayer) player).getHandle().equals(fakePlayers.get(p.getUniqueId()))) {
                    id[0] = player.getEntityId();
                }
            }
        }
        fakePlayers.remove(p.getUniqueId());
        PacketPlayOutEntityDestroy destroy = new PacketPlayOutEntityDestroy(id);
        for (Player player : Bukkit.getOnlinePlayers()) {
            EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
            nmsPlayer.playerConnection.sendPacket(pi);
            nmsPlayer.playerConnection.sendPacket(destroy);
        }
    }
     
    I sincerely apologise of segments are written poorly / is sloppy, this class has gone through over a dozen revisions in the past few hours and I've yet to properly clean it up.

    Any help would be greatly appreciated, I feel as though I am so close and am only missing one important detail.
     
  2. You need to send the animation packets yourself. The fake player doesn't exist on the server, so when you tell the server to set flag on fake player nothing happens (I'm surprised it doesn't error, actually). It works for real player because server sends the packet for you. ;)
     
    • Agree Agree x 1
  3. Huh, I can't believe I didn't think of this. Would you happen to know what packet I should be using? Entity Metadata or action, possibly something else?

    EDIT: Okay, with a bit of looking about I believe the packet I want is PacketPlayOutEntityMetadata. Unfortunately, I have absolutely no idea how to utilize it. Without ProtocolLib, the obtusification of the source is making it nearly impossible for me to decipher what exactly I'm supposed to do with it, and even with a PacketWrapper, I'm running into issues. With the PacketWrapper, it asks for an entityID (easy enough, just use the same randomly generated ID I used for the spawn packet), except now to set the metadata I need a List<WrappedWatchableObject>. I'm assuming the list is just to change multiple metadata values, which I don't really need to worry about. however, to make a WrappedWatchableObject I need a DataWatcher.Item, which to make that I need a DataWatcherObject, and which to make THAT I need an object extending DataWatcherSerializer, which I'm not sure what to put for the given methods required to make it function. this goes along with two Objects (one for the DataWatcher.Item, and another for the DataWatcherObject) which I am completely in the dark as to what these should be set to.
    Is there some wiki, tutorial, or explanation for this packet I'm missing? I can't make sense of it.

    I have found this thread: https://www.spigotmc.org/threads/packet-discovery-rotating-player-models.318388/#post-3024113
    So I feel it must be possible, but something I'm trying is not working.

    EDIT2: Okay, I've gotten it working by messing with the post by JanTuck, and I've gotten it to this state:
    [​IMG]
    The only issue now is that the head orientation is wrong. I'll see what I can do to fix it.
     
    #3 GreatThane, Jun 5, 2018
    Last edited: Jun 5, 2018