1.16.5 Help with NPCs Head Rotation

Discussion in 'Spigot Plugin Development' started by Dusk_2_Dawn, Apr 2, 2020.

  1. Okay, so I am trying to custom code NPCs right now. Before anyone says it:
    NO I WILL NOT USE CITIZENS OR AN API, I AM CUSTOM CODING IT.

    Now that I've got that out of the way, I have a question about how to make these NPCs better. Currently I have no problem making the NPCs themselves. I'm just wondering on how to make them a bit more interesting.

    My question is, how can I make my NPCs move their heads / bodies to look towards a player? Currently they just stand around and look in one direction and it's kind of boring. I am making both mob and player NPCs so it would be great if I someone could answer on how to do it for both.

    Here is the code that I am using for both the Player and Mob NPCs:
    Code (Java):
    package net.dusk2dawn.npc.Players;

    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import com.mojang.authlib.GameProfile;
    import com.mojang.authlib.properties.Property;
    import net.dusk2dawn.npc.Util;
    import net.minecraft.server.v1_8_R3.*;
    import org.bukkit.Bukkit;
    import org.bukkit.ChatColor;
    import org.bukkit.Location;
    import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
    import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
    import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
    import org.bukkit.entity.Player;

    import java.io.InputStreamReader;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.util.HashMap;
    import java.util.UUID;

    public class PlayerManager {

        private EntityPlayer entityPlayer;

        protected String texture, signature, name;
        protected Location location;
        protected Player player;
        protected UUID uuid;

        public PlayerManager(String p, String name, Location location) throws Exception {
            URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + p);
            InputStreamReader reader = new InputStreamReader(url.openStream());
            String uuid = new JsonParser().parse(reader).getAsJsonObject().get("id").getAsString();

            URL url1 = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false");
            InputStreamReader reader1 = new InputStreamReader(url1.openStream());
            JsonObject textureProperty = new JsonParser().parse(reader1).getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject();

            this.name = ChatColor.translateAlternateColorCodes('&', name);
            this.location = location;
            this.uuid = Util.formatUUID(uuid);
            this.player = Bukkit.getPlayer(p);
            this.texture = textureProperty.get("value").getAsString();
            this.signature = textureProperty.get("signature").getAsString();
        }

        public void createPlayer() {
            MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
            WorldServer world = ((CraftWorld) location.getWorld()).getHandle();

            GameProfile profile = new GameProfile(uuid, name);
            EntityPlayer player = new EntityPlayer(server, world, profile, new PlayerInteractManager(world));
            player.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
            profile.getProperties().put("textures", new Property("textures", texture, signature));

            this.entityPlayer = player;

            showNPCs();
        }

        public void showNPCs() {
            for(Player p : Bukkit.getOnlinePlayers()) {
                PlayerConnection connection = ((CraftPlayer) p).getHandle().playerConnection;
                connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, this.entityPlayer));
                connection.sendPacket(new PacketPlayOutNamedEntitySpawn(entityPlayer));
                connection.sendPacket(new PacketPlayOutEntityHeadRotation(entityPlayer, (byte) (entityPlayer.yaw * 256 / 360)));
            }
        }

    }
     
    Code (Java):
    package net.dusk2dawn.npc.Mobs;

    import net.minecraft.server.v1_8_R3.*;
    import org.bukkit.ChatColor;
    import org.bukkit.Location;
    import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;

    import java.util.HashMap;

    public class MobManager {

        protected Entity entity;
        protected String name;
        protected Location location;

        public MobManager(String mob, String name, Location location) {
            WorldServer world = ((CraftWorld) location.getWorld()).getHandle();

            this.name = ChatColor.translateAlternateColorCodes('&', name);
            this.location = location;

            if(alias(mob, "ZOMBIE")) this.entity = new EntityZombie(world);
            else if(alias(mob, "VILLAGER")) this.entity = new EntityVillager(world);
            else if(alias(mob, "CREEPER")) this.entity = new EntityCreeper(world);
            else if(alias(mob, "SKELETON")) this.entity = new EntitySkeleton(world);
            else if(alias(mob, "WITHER_SKELETON")) {
                EntitySkeleton witherSkeleton = new EntitySkeleton(world);
                witherSkeleton.setSkeletonType(1);
                this.entity = witherSkeleton;
            }
        }

        public void createMob() {
            WorldServer world = ((CraftWorld) location.getWorld()).getHandle();

            entity.setCustomName(name);
            entity.setCustomNameVisible(true);
            entity.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
            NBTTagCompound tag = entity.getNBTTag();
            if(tag == null) tag = new NBTTagCompound();

            entity.c(tag);
            tag.setInt("NoAI", 1);
            tag.setInt("Invulnerable", 1);
            entity.f(tag);

            world.addEntity(entity);
        }

        private boolean alias(String string1, String string2) {
            if(string1.equalsIgnoreCase(string2)) return true;
            else return false;
        }

    }
     

    P.S. If someone could help me with a more efficient way for converting a String with a mob into an NMS Entity instead of using a bunch of if's, that would be fantastic.

    EDIT:
    I have figured out a solution to this issue with the help of @Schottky

    Here is the full code that I am using
    Code (Java):
    public class NPC {

        private JavaPlugin plugin;
        private String name;
        private Integer taskID;
        private Location location;
        private EntityPlayer npc;

        public NPC(JavaPlugin plugin, String name, Location location) {
            // Get the NMS Server and World
            MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
            WorldServer world = ((CraftWorld) location.getWorld()).getHandle();

            // Download the Skin and create the GameProfile
            String[] skin = Mojang.getSkin(name);
            GameProfile profile = new GameProfile(UUID.randomUUID(), name);
            profile.getProperties().put("textures", new Property("textures", skin[0], skin[1]));

            // Create the NPC and set the Location
            EntityPlayer player = new EntityPlayer(server, world, profile, new PlayerInteractManager(world));
            player.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());

            // Set the NPC Properties
            player.getBukkitEntity().setRemoveWhenFarAway(false);
            player.getBukkitEntity().setCanPickupItems(false);
            player.getBukkitEntity().setCustomName("");
            player.getBukkitEntity().setCustomNameVisible(false);
            player.setPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());

            this.plugin = plugin;
            this.name = name;
            this.location = location;
            this.npc = player;
            Core.getNPCs().put(name, this);
        }

        public void showNPC(Player p) {
            PlayerConnection connection = ((CraftPlayer) p).getHandle().playerConnection;

            // Show the Second Skin Layer
            DataWatcher watcher = this.npc.getDataWatcher();
            watcher.set(new DataWatcherObject<>(16, DataWatcherRegistry.a), (byte) 255);

            connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, this.npc));
            connection.sendPacket(new PacketPlayOutNamedEntitySpawn(this.npc));
            connection.sendPacket(new PacketPlayOutEntityHeadRotation(this.npc, (byte) (this.npc.yaw * 256 / 360)));
            connection.sendPacket(new PacketPlayOutEntityMetadata(this.npc.getId(), watcher, true));

            Bukkit.getScheduler().runTaskLater(plugin, () -> connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, this.npc)), 20L);
        }

        public void enableRotation() {
            this.taskID = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
                for(Player p : Bukkit.getOnlinePlayers()) {
                    if(calculateDistance(p) > 5) continue;

                    // Get the Player Connection so we can send Packets
                    PlayerConnection connection = ((CraftPlayer) p).getHandle().playerConnection;

                    // Calculate the Yaw for the NPC
                    Vector difference = p.getLocation().subtract(npc.getBukkitEntity().getLocation()).toVector().normalize();
                    byte yaw = (byte) MathHelper.d((Math.toDegrees(Math.atan2(difference.getZ(), difference.getX()) - Math.PI / 2) * 256.0F) / 360.0F);

                    // Calculate the Pitch for the NPC
                    Vector height = npc.getBukkitEntity().getLocation().subtract(p.getLocation()).toVector().normalize();
                    byte pitch = (byte) MathHelper.d((Math.toDegrees(Math.atan(height.getY())) * 256.0F) / 360.0F);

                    // Send the Packets to update the NPC
                    connection.sendPacket(new PacketPlayOutEntityHeadRotation(this.npc, yaw));
                    connection.sendPacket(new PacketPlayOutEntity.PacketPlayOutEntityLook(this.npc.getId(), yaw, pitch, true));
                }
            }, 1, 1);
        }

        public void disableRotation() {
            Bukkit.getScheduler().cancelTask(this.taskID);
        }

        private double calculateDistance(Player p) {
            double diffX = this.npc.locX() - p.getLocation().getX(), diffZ = this.npc.locZ() - p.getLocation().getZ();
            double x = diffX < 0 ? (diffX * -1) : diffX, z = diffZ < 0 ? (diffZ * -1) : diffZ;
            return Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2));
        }

    }

    Thanks to anyone who helped me get this working
     
    #1 Dusk_2_Dawn, Apr 2, 2020
    Last edited: Jul 29, 2020
    • Friendly Friendly x 1
  2. I doubt that there will be a more efficient way, but there are ways that could be considered better:
    Code (Text):
    switch(mob.toUpperCase()) {
        case "ZOMBIE":
            this.entity = new EntityZombie(world);
            break;
        case "VILLAGER":
            this.villager = new EntityVillager(world);
            break;
        ...
    }
    If you want the custom player to follow a player's head movement, you'll need to send out a packet each tick (or whatever time-interval seems suiting) that adjusts the NPC's head to look at the player. The angle can be computed using the atan2 function on the vector that "connects" the player with the NPC (I think you'll also need to add 90 degrees or something so it's the Minecraft yaw).
     
  3. Example?
     
  4. Something like this:
    Code (Java):
    new BukkitRunnable() {
        @Override
        public void run() {
            Vector diff = player.getLocation().subtract(npc.getLocation()).toVector().normalize();
            double angle = Math.toDegrees(Math.atan2(diff.getZ(), diff.getX()) + Math.PI / 2);
            npc.setYaw(angle)
        }
    }.runTaskTimer(plugin, 0, 1)
     
  5. I tried that and it didn't do anything. This is what I have:
    Code (Java):
    public void headRotation() {
            Bukkit.getScheduler().scheduleSyncRepeatingTask(NPC.getPlugin(), new Runnable() {
                @Override
                public void run() {
                    for(Player p : Bukkit.getOnlinePlayers()) {
                        Vector diff = p.getLocation().subtract(location).toVector().normalize();
                        Float angle = (float) Math.toDegrees(Math.atan2(diff.getZ(), diff.getX()) + Math.PI / 2);
                        entityPlayer.setLocation(location.getX(), location.getY(), location.getZ(), angle, location.getPitch());
                    }
                }
            }, 0L, 1L);
        }
    Don't know if I did that right
     
  6.  
  7. I'm sorry to bump an old thread but I have come back to this issue and I'm trying to get this working again.

    Which packet do I need to send?
     
  8. Spz

    Spz

    PacketPlayOutEntityHeadRotation
     
  9. This thread is actually interesting, I feel the same about not using citizens or other NPC plugins. I like making my own things to learn from it etc. I have a NPC class that I made (I was really happy when I finished and got it working) but I didn't add a feature that makes their head follow a player as I didn't need it to do that. But if you find a solution let me know. I'll be back tomorrow to give it a try, and I'll get back to you unless you already got a solution.
     
  10. Alright so an update on the issue.

    I have been working with @Schottky in private messages to get this working and this is what I have currently
    Code (Java):
    Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
                for(Player p : Bukkit.getOnlinePlayers()) {
                    if(calculateDistance(p) > 5) continue;

                    PlayerConnection connection = ((CraftPlayer) p).getHandle().playerConnection;
                    Vector difference = p.getLocation().subtract(npc.getBukkitEntity().getLocation()).toVector().normalize();
                    float degrees = (float) Math.toDegrees(Math.atan2(difference.getZ(), difference.getX()) - Math.PI / 2);
                    byte angle = (byte) MathHelper.d((degrees * 256.0F) / 360.0F);

                    connection.sendPacket(new PacketPlayOutEntityHeadRotation(this.npc, angle));
                    connection.sendPacket(new PacketPlayOutEntity.PacketPlayOutEntityLook(this.npc.getId(), angle, (byte) 0, true));
                }
            }, 1, 1);
    The calculateDistance function just determines how far the player is from the NPC (I don't want to send unnecessary packets). If anyone would like the method, here you go
    Code (Java):
    private double calculateDistance(Player p) {
            double diffX = this.npc.locX() - p.getLocation().getX(), diffZ = this.npc.locZ() - p.getLocation().getZ();
            double x = diffX < 0 ? (diffX * -1) : diffX, z = diffZ < 0 ? (diffZ * -1) : diffZ;
            return Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2));
        }

    After the connection declaration, that is just for calculating the yaw for the head.

    After calculating the yaw, I am sending 2 packets: a PacketPlayOutEntityHeadRotation packet and a PacketPlayOutEntityLook packet.
    PacketPlayOutEntityHeadRotation: Entity, Yaw
    PacketPlayOutEntityLook: Entity ID, Yaw, Pitch, Unknown

    Currently, the only thing that I still need to figure out if the pitch and how to adjust it. I will edit this reply or make a new one when I have this fully working.

    UPDATE #1:
    Well I know for certain now that the pitch argument for the PacketPlayOutEntityLook packet does change the heads pitch. The only thing needed now is to calculate the pitch.

    UPDATE #2:
    I have figured out how to calculate the pitch now
    Code (Java):
    Vector height = npc.getBukkitEntity().getLocation().subtract(p.getLocation()).toVector().normalize();
    byte pitch = (byte) MathHelper.d((Math.toDegrees(Math.atan(height.getY())) * 256.0F) / 360.0F);
    You can then insert that into the PacketPlayOutEntityLook packet and the head's pitch will automatically follow you.

    I will leave the full code in the main thread for any future people
     
    #10 Dusk_2_Dawn, Jul 27, 2020
    Last edited: Jul 29, 2020
    • Informative Informative x 1
    • Useful Useful x 1