Resource How to create and modify NPCs

Discussion in 'Spigot Plugin Development' started by CoolJWB, Oct 20, 2019.

?

What should my next resource be about?

  1. Armor stand modification

    36 vote(s)
    12.5%
  2. Custom world generation

    118 vote(s)
    40.8%
  3. Cheat detection

    127 vote(s)
    43.9%
  4. Scoreboards

    36 vote(s)
    12.5%
  5. Boss titles

    14 vote(s)
    4.8%
  6. Player skulls

    12 vote(s)
    4.2%
  7. Particles

    44 vote(s)
    15.2%
Multiple votes are allowed.
  1. How to create and modify NPCs
    This resource will cover how to: create, remove, rotate, move and modify skins on NPCs. It's strongly recommended that you have at least some knowledge on knowledge on how packets work and how to get an NMS class to more easily understand :).

    NOTES:
    • This was written in 1.14 and may not work the same in other versions.
    • If you find any mistake or have additional information please inform me about it privately or via the comments.
    • You can contact me on Discord for any questions!

    How to create an NPC
    To create an NPC there will be a few variables you need to initialize. These are the NMS server, world, and a new game profile. To get the server and world you will need to cast Bukkit classes to Craft classes and then get the NMS ones from that. The game profile can be created with a random UUID and 16 characters or less string (player name).

    The player name must be less than 16 characters or else all players that render that NPC will get disconnected from the server. The only way (that I currently know of) to make players have more than 16 character names is to fake it with invisible entities (such as armor stands) and teams that hide player names (which I won't cover unless it's heavily requested).
    Code (Java):

            MinecraftServer nmsServer = ((CraftServer) Bukkit.getServer()).getServer();
            WorldServer nmsWorld = ((CraftWorld)Bukkit.getWorld("world")).getHandle(); // Change "world" to the world the NPC should be spawned in.
            GameProfile gameProfile = new GameProfile(UUID.randomUUID(), "playername"); // Change "playername" to the name the NPC should have, max 16 characters.
            EntityPlayer npc = new EntityPlayer(nmsServer, nmsWorld, gameProfile, new PlayerInteractManager(nmsWorld)); // This will be the EntityPlayer (NPC) we send with the sendNPCPacket method.
            npc.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
     

    So now that you have created the NPC you need to send 2 packets to the player/players that should be able to see that specific entity. In most cases, you want to send the entity packet to all players for everyone to see them, so you would have to iterate over all online players (also remember to send the packet to players that enter the server or else they won't see the NPCs). The easiest would be to add all NPCs to an ArrayList and then make a method that sends a packet to spawn all NPCs in that array for a player.

    To send a packet you need to get the player connection (a class that handles ex. player disconnection, player commands, and packet sending). We can do that by casting the receiving player to a CraftPlayer and then from the NMS player (EntityPlayer) we can use playerConnection.
    Use the sendPacket method from playerConnection to send the PacketPlayOutPlayerInfo and PacketPlayOutNamedEntitySpawn.
    Code (Java):

    public void addNPCPacket(EntityPlayer npc, Player player) {
        PlayerConnection connection = ((CraftPlayer)player).getHandle().playerConnection;
        connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, npc)); // "Adds the player data for the client to use when spawning a player" - https://wiki.vg/Protocol#Spawn_Player
        connection.sendPacket(new PacketPlayOutNamedEntitySpawn(npc)); // Spawns the NPC for the player client.
        connection.sendPacket(new PacketPlayOutEntityHeadRotation(npc, (byte) (npc.yaw * 256 / 360))); // Correct head rotation when spawned in player look direction.
    }
     

    The result should look like this (either Steve or Alex):
    [​IMG]

    How to remove an NPC
    To remove an NPC you will only need the entity ID of that NPC. This will be the only input that is needed for the packet PacketPlayOutEntityDestroy.
    Code (Java):

    public void removeNPCPacket(Entity npc, Player player) {
        PlayerConnection connection = ((CraftPlayer)player).getHandle().playerConnection;
        connection.sendPacket(new PacketPlayOutEntityDestroy(npc.getId()));
    }
     

    How to rotate an NPC
    To rotate an NPC you will need to turn the angle of yaw and pitch (not delta) into bytes (-128 to 128). To do this multiply them by 256 and divide the product with 360.
    The packets needed are PacketPlayOutEntityHeadRotation and PacketPlayOutEntityLook. Both are needed as the head
    rotation packet will only rotate the NPC horizontally while the entity look packet only works vertically (for reasons I do not know).
    Code (Java):

    public void lookNPCPacket(Entity npc, Player player, float yaw, float pitch) {
            PlayerConnection connection = ((CraftPlayer)player).getHandle().playerConnection;
            connection.sendPacket(new PacketPlayOutEntityHeadRotation(npc, (byte)(yaw * 256 / 360)));
            connection.sendPacket(new PacketPlayOutEntity.PacketPlayOutEntityLook(npc.getId(), (byte)(yaw * 256 / 360), (byte)(pitch * 256 / 360), true));
    }
     

    How to move an NPC
    The packet to move an entity is the PacketPlayOutRelEntityMove which consist of the NPCs entity ID and then the distance of blocks that the entity should be moved in the x, y or z-axis (1 block = 4096). If you intend to move the entity more then 8 blocks (4096 * 8 = 32767) there will be an issue with overflow. Whenever you want to move the entity more then 8 blocks use the packet PacketPlayOutEntityTeleport instead.
    Code (Java):

    public static void sendMoveEntityPacket(Entity entity, Player player, double x, double y, double z) {
            ((CraftPlayer)player).getHandle().playerConnection.sendPacket(new PacketPlayOutEntity.PacketPlayOutRelEntityMove(entity.getId(), (short)(x * 4096), (short)(y * 4096), (short)(z * 4096), true));
    }
     

    How to change the skin on an NPC
    To change the skin you will need to modify the properties of the GameProfile with a new Property with the name of textures. This should contain the value and signature which you can get from either Mojangs API or (as I use) Electroids. To make this more optimized and less stressful on the API servers you should make sure to cache the skins (in memory, file, database, or such). Lastly, send the client settings with the PacketPlayOutEntityMetadata packet or else the outer layer of the skin wont show.
    Code (Java):

    public static void sendSetNPCSkinPacket(EntityPlayer npc, Player player, String username) { // The username is the name for the player that has the skin.
            removeNPCPacket(npc, player);

            try {
                HttpsURLConnection connection = (HttpsURLConnection) new URL(String.format("https://api.ashcon.app/mojang/v2/user/%s", username)).openConnection();
                if (connection.getResponseCode() == HttpsURLConnection.HTTP_OK) {
                    ArrayList<String> lines = new ArrayList<>();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    reader.lines().forEach(lines::add);

                    String reply = String.join(" ",lines);
                    int indexOfValue = reply.indexOf("\"value\": \"");
                    int indexOfSignature = reply.indexOf("\"signature\": \"");
                    String skin = reply.substring(indexOfValue + 10, reply.indexOf("\"", indexOfValue + 10));
                    String signature = reply.substring(indexOfSignature + 14, reply.indexOf("\"", indexOfSignature + 14));

                    profile.getProperties().put("textures", new Property("textures", skin, signature));
                }

                else {
                    Bukkit.getConsoleSender().sendMessage("Connection could not be opened when fetching player skin (Response code " + connection.getResponseCode() + ", " + connection.getResponseMessage() + ")");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            // The client settings.
            DataWatcher watcher = npc.getDataWatcher();
            watcher.set(new DataWatcherObject<>(15, DataWatcherRegistry.a), (byte)127);
            PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata(npc.getId(), watcher, true);
            ((CraftPlayer)player).getHandle().playerConnection.sendPacket(packet);

            addNPCPacket(npc, player);
     }
     
     
    #1 CoolJWB, Oct 20, 2019
    Last edited: Mar 14, 2021
    • Useful x 24
    • Like x 7
    • Winner x 2
    • Agree x 1
    • Informative x 1
    • Friendly x 1
    • Optimistic x 1
  2. Looks good and just in time, I was just going to depend on citizens! Haven't tested it out, but it looks straight forward and somewhat documented
     
    • Like Like x 1
    • Agree Agree x 1
    • Creative Creative x 1
  3. If you're going to do advanced NPC movement (such as walking between selected points) or have name tags that are large, this resource won't fully do it. Citizens may be a better option at the time.

    If you still want to make it yourself and have any questions I'll answer them to my best ability!
     
    #3 CoolJWB, Oct 20, 2019
    Last edited: Oct 20, 2019
    • Like Like x 1
  4. Small question why do u have variable called 'speed' in the 'sendMoveEntityPacket()', am I missing something obvious here or...
     
    • Like Like x 1
  5. My mistake, thanks for pointing It out!
    The code is from a test plugin I wrote so I forgot to remove a few variables that wasn't necessary.
     
    • Like Like x 1
  6. How can i make, that npc will not show in tab list? I got good results using player remove packet, but i have got problem with timing.
     
  7. Code (Text):
    Team team = scoreboard.registerNewTeam("NPCS");
            team.setNameTagVisibility(NameTagVisibility.NEVER);
            team.addEntry("§8[NPC]");
    Use it
     
    • Like Like x 1
  8. Why are you contacting the skin servers each time you send the skin?! Just cache the result

    Oh and btw the mojang servers wouldnt even allow that because they have a limit of 100 requests per 5 minutes or so
     
    • Like Like x 1
  9. I did't test it, but from what I know, this will do the reverse of what I need, it will show the name in tab list, but hide above the npc. As mentioned above, the player remove packet is doing the job quite good except fact, that the skin doesn't render if I fire it too soon.
     
    #9 RedstoneExpert, Jul 29, 2020
    Last edited: Jul 29, 2020
  10. I don't know if it's the best way, but what I do is use a runnable that'll be executed after a tick, the skin renders without problems.
    Code (Java):
            plugin.getServer().getScheduler().runTask(plugin, () -> {
                for (Player player : players) {
                    // Send PacketPlayOutPlayerInfo...
                }
            });
     
    • Like Like x 1
  11. I am using the same code, but I have problem when someone join because then this will be executed too soon and the skin will not render.
     
  12. What I mean (maybe I didn't explain myself), first send the packets PacketPlayOutPlayerInfo (with ADD_PLAYER), PacketPlayOutNamedEntitySpawn and PacketPlayOutEntityHeadRotation, then inside a runnable that will run after a tick, you send the packet PacketPlayOutPlayerInfo again but instead of ADD_PLAYER, using REMOVE_PLAYER. In this way the skin will be rendered. Check how I do it. :)

    If for some reason it still doesn't work, try changing runTask() to runTaskTimer() and run it after X ticks, but it should work
     
  13. I understand this, I am doing it exactly same (except I am using own class for sending packets), but the problem with timing is when someone join and I need to also render the npc to him.
     
  14. When the chunks load or when I'm far from this, it dissapear. How can I solve it?
     
  15. reflection pls
     
  16. Do it yourself. :D
     
    • Funny Funny x 3
  17. people are still asking for the reflection in 2020. that's annoying.
     
  18. Thanks for the post! This was really helpful.

    For the people that are attempting to apply skins in 1.16 versions:
    Code (Java):
    watcher.set(new DataWatcherObject<>(15, DataWatcherRegistry.a), (byte)127);
    needs to be
    Code (Java):
    watcher.set(new DataWatcherObject<>(16, DataWatcherRegistry.a), (byte)127);
    Also, for the skin to apply properly and you want to remove the players from the tablist, you must delay the PacketPlayOutPlayerInfo(REMOVE_PLAYER) using a bukkit scheduler.
     
    #18 Neoka, Dec 10, 2020
    Last edited: Dec 12, 2020
    • Like Like x 2
  19. For how long must I delay it?
     
  20. I delayed for 20 ticks and that seemed to work.