From aff69dbb71fdb079d858dfcac3a4963668e09fb3 Mon Sep 17 00:00:00 2001
From: Pyrbu <pyrmcserver@gmail.com>
Date: Mon, 24 Apr 2023 22:31:48 +0100
Subject: [PATCH] skin layers & metadata

---
 .../commands/list/DefaultCommand.java         |  4 +-
 .../znetworkw/znpcservers/npc/NPCSkin.java    |  2 +-
 .../znpcservers/skin/SkinFetcher.java         |  5 ++-
 .../znpcservers/skin/SkinFetcherResult.java   |  4 +-
 .../java/lol/pyr/znpcsplus/ZNPCsPlus.java     |  7 +++-
 .../pyr/znpcsplus/entity/PacketEntity.java    | 13 ++++--
 .../pyr/znpcsplus/entity/PacketPlayer.java    | 24 -----------
 .../znpcsplus/metadata/MetadataFactory.java   | 38 ++++++++++++++++++
 .../pyr/znpcsplus/metadata/V1_14Factory.java  | 10 +++++
 .../pyr/znpcsplus/metadata/V1_16Factory.java  | 10 +++++
 .../pyr/znpcsplus/metadata/V1_17Factory.java  | 10 +++++
 .../pyr/znpcsplus/metadata/V1_8Factory.java   | 15 +++++++
 .../pyr/znpcsplus/metadata/V1_9Factory.java   | 10 +++++
 src/main/java/lol/pyr/znpcsplus/npc/NPC.java  | 22 +++++-----
 .../lol/pyr/znpcsplus/npc/NPCProperty.java    | 38 ++++++++++++++++++
 .../lol/pyr/znpcsplus/npc/NPCPropertyKey.java | 35 ----------------
 .../java/lol/pyr/znpcsplus/npc/NPCType.java   | 11 ++++-
 .../pyr/znpcsplus/packets/PacketFactory.java  | 13 +++---
 .../pyr/znpcsplus/packets/V1_19Factory.java   | 11 ++---
 .../pyr/znpcsplus/packets/V1_8Factory.java    | 40 ++++++++++++++-----
 20 files changed, 219 insertions(+), 103 deletions(-)
 delete mode 100644 src/main/java/lol/pyr/znpcsplus/entity/PacketPlayer.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/metadata/MetadataFactory.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/metadata/V1_14Factory.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/metadata/V1_16Factory.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/metadata/V1_17Factory.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/metadata/V1_8Factory.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/metadata/V1_9Factory.java
 create mode 100644 src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java
 delete mode 100644 src/main/java/lol/pyr/znpcsplus/npc/NPCPropertyKey.java

diff --git a/src/main/java/io/github/znetworkw/znpcservers/commands/list/DefaultCommand.java b/src/main/java/io/github/znetworkw/znpcservers/commands/list/DefaultCommand.java
index 4f5bd69..6cf4600 100644
--- a/src/main/java/io/github/znetworkw/znpcservers/commands/list/DefaultCommand.java
+++ b/src/main/java/io/github/znetworkw/znpcservers/commands/list/DefaultCommand.java
@@ -39,14 +39,14 @@ public class DefaultCommand extends Command {
 
     private static final Joiner SPACE_JOINER = Joiner.on(" ");
 
-    private static final SkinFunction DO_APPLY_SKIN = (sender, npc, skin) -> NPCSkin.forName(skin, (value, signature, ex) -> {
+    private static final SkinFunction DO_APPLY_SKIN = (sender, npc, skin) -> NPCSkin.forName(skin, (npcSkin, ex) -> {
         if (ex != null) {
             Configuration.MESSAGES.sendMessage(sender, ConfigurationValue.CANT_GET_SKIN, skin);
             ZNPCsPlus.LOGGER.warning("Failed to fetch skin:");
             ex.printStackTrace();
             return;
         }
-        npc.changeSkin(NPCSkin.forValues(value, signature));
+        npc.changeSkin(npcSkin);
         Configuration.MESSAGES.sendMessage(sender, ConfigurationValue.GET_SKIN);
     });
 
diff --git a/src/main/java/io/github/znetworkw/znpcservers/npc/NPCSkin.java b/src/main/java/io/github/znetworkw/znpcservers/npc/NPCSkin.java
index 94baaa9..9a51541 100644
--- a/src/main/java/io/github/znetworkw/znpcservers/npc/NPCSkin.java
+++ b/src/main/java/io/github/znetworkw/znpcservers/npc/NPCSkin.java
@@ -9,7 +9,7 @@ public class NPCSkin {
     private final String texture;
     private final String signature;
 
-    protected NPCSkin(String texture, String signature) {
+    public NPCSkin(String texture, String signature) {
         this.texture = texture;
         this.signature = signature;
     }
diff --git a/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcher.java b/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcher.java
index 0421af9..9984a0b 100644
--- a/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcher.java
+++ b/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcher.java
@@ -2,6 +2,7 @@ package io.github.znetworkw.znpcservers.skin;
 
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
+import io.github.znetworkw.znpcservers.npc.NPCSkin;
 
 import java.io.DataOutputStream;
 import java.io.InputStreamReader;
@@ -46,11 +47,11 @@ public class SkinFetcher {
         });
         completableFuture.whenComplete((response, throwable) -> {
             if (completableFuture.isCompletedExceptionally()) {
-                skinFetcherResult.onDone(null, null, throwable);
+                skinFetcherResult.onDone(null, throwable);
             } else {
                 JsonObject jsonObject = response.getAsJsonObject(this.builder.getAPIServer().getValueKey());
                 JsonObject properties = jsonObject.getAsJsonObject(this.builder.getAPIServer().getSignatureKey());
-                skinFetcherResult.onDone(properties.get("value").getAsString(), properties.get("signature").getAsString(), null);
+                skinFetcherResult.onDone(new NPCSkin(properties.get("value").getAsString(), properties.get("signature").getAsString()), null);
             }
         });
         return completableFuture;
diff --git a/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcherResult.java b/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcherResult.java
index ec55efa..f1c54d6 100644
--- a/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcherResult.java
+++ b/src/main/java/io/github/znetworkw/znpcservers/skin/SkinFetcherResult.java
@@ -1,5 +1,7 @@
 package io.github.znetworkw.znpcservers.skin;
 
+import io.github.znetworkw.znpcservers.npc.NPCSkin;
+
 public interface SkinFetcherResult {
-    void onDone(String value, String signature, Throwable paramThrowable);
+    void onDone(NPCSkin npcSkin, Throwable paramThrowable);
 }
diff --git a/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java b/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java
index 2c39893..43764f4 100644
--- a/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java
+++ b/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java
@@ -2,6 +2,7 @@ package lol.pyr.znpcsplus;
 
 import com.github.retrooper.packetevents.PacketEvents;
 import com.github.retrooper.packetevents.event.PacketListenerPriority;
+import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
@@ -11,6 +12,7 @@ import io.github.znetworkw.znpcservers.configuration.ConfigurationConstants;
 import io.github.znetworkw.znpcservers.listeners.InventoryListener;
 import io.github.znetworkw.znpcservers.listeners.PlayerListener;
 import io.github.znetworkw.znpcservers.npc.NPCPath;
+import io.github.znetworkw.znpcservers.npc.NPCSkin;
 import io.github.znetworkw.znpcservers.npc.interaction.InteractionPacketListener;
 import io.github.znetworkw.znpcservers.npc.task.NPCPositionTask;
 import io.github.znetworkw.znpcservers.npc.task.NPCSaveTask;
@@ -21,6 +23,7 @@ import io.github.znetworkw.znpcservers.utility.itemstack.ItemStackSerializer;
 import io.github.znetworkw.znpcservers.utility.location.ZLocation;
 import lol.pyr.znpcsplus.entity.PacketLocation;
 import lol.pyr.znpcsplus.npc.NPC;
+import lol.pyr.znpcsplus.npc.NPCProperty;
 import lol.pyr.znpcsplus.npc.NPCRegistry;
 import lol.pyr.znpcsplus.npc.NPCType;
 import lol.pyr.znpcsplus.tasks.NPCVisibilityTask;
@@ -135,7 +138,9 @@ public class ZNPCsPlus extends JavaPlugin {
             World world = Bukkit.getWorld("world");
             if (world == null) world = Bukkit.getWorlds().get(0);
             for (NPCType type : NPCType.values()) {
-                NPCRegistry.register("debug_npc" + (z * wrap + x), new NPC(world, type, new PacketLocation(x * 3, 200, z * 3, 0, 0)));
+                NPC npc = new NPC(world, type, new PacketLocation(x * 3, 200, z * 3, 0, 0));
+                if (type.getType() == EntityTypes.PLAYER) NPCSkin.forName("Pyrbu", (skin, ex) -> npc.setProperty(NPCProperty.SKIN, skin));
+                NPCRegistry.register("debug_npc" + (z * wrap + x), npc);
                 if (x++ > wrap) {
                     x = 0;
                     z++;
diff --git a/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java b/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java
index a77aab9..bfe8075 100644
--- a/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java
+++ b/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java
@@ -4,6 +4,7 @@ import com.github.retrooper.packetevents.protocol.entity.type.EntityType;
 import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
 import io.github.znetworkw.znpcservers.reflection.Reflections;
 import io.github.znetworkw.znpcservers.utility.Utils;
+import lol.pyr.znpcsplus.npc.NPC;
 import lol.pyr.znpcsplus.packets.PacketFactory;
 import org.bukkit.entity.Player;
 
@@ -11,14 +12,15 @@ import java.util.Set;
 import java.util.UUID;
 
 public class PacketEntity {
+    private final NPC owner;
     private final int entityId;
     private final UUID uuid;
 
     private final EntityType type;
     private PacketLocation location;
 
-    public PacketEntity(EntityType type, PacketLocation location) {
-        if (type == EntityTypes.PLAYER) throw new RuntimeException("Wrong class used for player");
+    public PacketEntity(NPC owner, EntityType type, PacketLocation location) {
+        this.owner = owner;
         this.entityId = reserveEntityID();
         this.uuid = UUID.randomUUID();
         this.type = type;
@@ -41,13 +43,18 @@ public class PacketEntity {
         return type;
     }
 
+    public NPC getOwner() {
+        return owner;
+    }
+
     public void setLocation(PacketLocation location, Set<Player> viewers) {
         this.location = location;
         for (Player viewer : viewers) PacketFactory.get().teleportEntity(viewer, this);
     }
 
     public void spawn(Player player) {
-        PacketFactory.get().spawnEntity(player, this);
+        if (type == EntityTypes.PLAYER) PacketFactory.get().spawnPlayer(player, this);
+        else PacketFactory.get().spawnEntity(player, this);
     }
 
     public void despawn(Player player) {
diff --git a/src/main/java/lol/pyr/znpcsplus/entity/PacketPlayer.java b/src/main/java/lol/pyr/znpcsplus/entity/PacketPlayer.java
deleted file mode 100644
index 373944f..0000000
--- a/src/main/java/lol/pyr/znpcsplus/entity/PacketPlayer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package lol.pyr.znpcsplus.entity;
-
-import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
-import com.github.retrooper.packetevents.protocol.player.UserProfile;
-import lol.pyr.znpcsplus.packets.PacketFactory;
-import org.bukkit.entity.Player;
-
-public class PacketPlayer extends PacketEntity {
-    private final UserProfile gameProfile;
-
-    public PacketPlayer(PacketLocation location) {
-        super(EntityTypes.PLAYER, location);
-        this.gameProfile = new UserProfile(getUuid(), Integer.toString(getEntityId()));
-    }
-
-    @Override
-    public void spawn(Player player) {
-        PacketFactory.get().spawnPlayer(player,  this);
-    }
-
-    public UserProfile getGameProfile() {
-        return gameProfile;
-    }
-}
diff --git a/src/main/java/lol/pyr/znpcsplus/metadata/MetadataFactory.java b/src/main/java/lol/pyr/znpcsplus/metadata/MetadataFactory.java
new file mode 100644
index 0000000..7800961
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/metadata/MetadataFactory.java
@@ -0,0 +1,38 @@
+package lol.pyr.znpcsplus.metadata;
+
+import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.manager.server.ServerVersion;
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+import lol.pyr.znpcsplus.util.LazyLoader;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public interface MetadataFactory {
+    EntityData skinLayers();
+
+    MetadataFactory factory = get();
+
+    static MetadataFactory get() {
+        if (factory != null) return factory;
+        ServerVersion version = PacketEvents.getAPI().getServerManager().getVersion();
+        Map<ServerVersion, LazyLoader<? extends MetadataFactory>> factories = buildFactoryMap();
+        if (factories.containsKey(version)) return factories.get(version).get();
+        for (ServerVersion v : ServerVersion.reversedValues()) {
+            if (v.isNewerThan(version)) continue;
+            if (!factories.containsKey(v)) continue;
+            return factories.get(v).get();
+        }
+        throw new RuntimeException("Unsupported version!");
+    }
+
+    private static Map<ServerVersion, LazyLoader<? extends MetadataFactory>> buildFactoryMap() {
+        HashMap<ServerVersion, LazyLoader<? extends MetadataFactory>> map = new HashMap<>();
+        map.put(ServerVersion.V_1_8, LazyLoader.of(V1_8Factory::new));
+        map.put(ServerVersion.V_1_9, LazyLoader.of(V1_9Factory::new));
+        map.put(ServerVersion.V_1_14, LazyLoader.of(V1_14Factory::new));
+        map.put(ServerVersion.V_1_16, LazyLoader.of(V1_16Factory::new));
+        map.put(ServerVersion.V_1_17, LazyLoader.of(V1_17Factory::new));
+        return map;
+    }
+}
diff --git a/src/main/java/lol/pyr/znpcsplus/metadata/V1_14Factory.java b/src/main/java/lol/pyr/znpcsplus/metadata/V1_14Factory.java
new file mode 100644
index 0000000..af0efe8
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/metadata/V1_14Factory.java
@@ -0,0 +1,10 @@
+package lol.pyr.znpcsplus.metadata;
+
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+
+public class V1_14Factory extends V1_9Factory {
+    @Override
+    public EntityData skinLayers() {
+        return createSkinLayers(15);
+    }
+}
diff --git a/src/main/java/lol/pyr/znpcsplus/metadata/V1_16Factory.java b/src/main/java/lol/pyr/znpcsplus/metadata/V1_16Factory.java
new file mode 100644
index 0000000..da7c8d7
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/metadata/V1_16Factory.java
@@ -0,0 +1,10 @@
+package lol.pyr.znpcsplus.metadata;
+
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+
+public class V1_16Factory extends V1_14Factory {
+    @Override
+    public EntityData skinLayers() {
+        return createSkinLayers(16);
+    }
+}
diff --git a/src/main/java/lol/pyr/znpcsplus/metadata/V1_17Factory.java b/src/main/java/lol/pyr/znpcsplus/metadata/V1_17Factory.java
new file mode 100644
index 0000000..b83b796
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/metadata/V1_17Factory.java
@@ -0,0 +1,10 @@
+package lol.pyr.znpcsplus.metadata;
+
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+
+public class V1_17Factory extends V1_16Factory {
+    @Override
+    public EntityData skinLayers() {
+        return createSkinLayers(17);
+    }
+}
diff --git a/src/main/java/lol/pyr/znpcsplus/metadata/V1_8Factory.java b/src/main/java/lol/pyr/znpcsplus/metadata/V1_8Factory.java
new file mode 100644
index 0000000..c368fc5
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/metadata/V1_8Factory.java
@@ -0,0 +1,15 @@
+package lol.pyr.znpcsplus.metadata;
+
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes;
+
+public class V1_8Factory implements MetadataFactory {
+    @Override
+    public EntityData skinLayers() {
+        return createSkinLayers(12);
+    }
+
+    protected EntityData createSkinLayers(int index) {
+        return new EntityData(index, EntityDataTypes.BYTE, Byte.MAX_VALUE);
+    }
+}
diff --git a/src/main/java/lol/pyr/znpcsplus/metadata/V1_9Factory.java b/src/main/java/lol/pyr/znpcsplus/metadata/V1_9Factory.java
new file mode 100644
index 0000000..0d5bd78
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/metadata/V1_9Factory.java
@@ -0,0 +1,10 @@
+package lol.pyr.znpcsplus.metadata;
+
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
+
+public class V1_9Factory extends V1_8Factory {
+    @Override
+    public EntityData skinLayers() {
+        return createSkinLayers(13);
+    }
+}
diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPC.java b/src/main/java/lol/pyr/znpcsplus/npc/NPC.java
index 9dee875..0be9acc 100644
--- a/src/main/java/lol/pyr/znpcsplus/npc/NPC.java
+++ b/src/main/java/lol/pyr/znpcsplus/npc/NPC.java
@@ -1,9 +1,7 @@
 package lol.pyr.znpcsplus.npc;
 
-import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
 import lol.pyr.znpcsplus.entity.PacketEntity;
 import lol.pyr.znpcsplus.entity.PacketLocation;
-import lol.pyr.znpcsplus.entity.PacketPlayer;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
 import org.bukkit.entity.Player;
@@ -20,19 +18,19 @@ public class NPC {
     private PacketLocation location;
     private NPCType type;
 
-    private final Map<NPCPropertyKey<?>, Object> propertyMap = new HashMap<>();
+    private final Map<NPCProperty<?>, Object> propertyMap = new HashMap<>();
 
     public NPC(World world, NPCType type, PacketLocation location) {
         this.worldName = world.getName();
         this.type = type;
         this.location = location;
-        entity = new PacketEntity(type.getType(), location);
+        entity = new PacketEntity(this, type.getType(), location);
     }
 
     public void setType(NPCType type) {
         _hideAll();
         this.type = type;
-        entity = type.getType() == EntityTypes.PLAYER ? new PacketPlayer(entity.getLocation()) : new PacketEntity(type.getType(), entity.getLocation());
+        entity = new PacketEntity(this, type.getType(), entity.getLocation());
         _showAll();
     }
 
@@ -100,20 +98,20 @@ public class NPC {
     }
 
     @SuppressWarnings("unchecked")
-    public <T> T getProperty(NPCPropertyKey<T> key) {
-        return (T) propertyMap.get(key);
+    public <T> T getProperty(NPCProperty<T> key) {
+        return hasProperty(key) ? (T) propertyMap.get(key) : key.getDefaultValue();
     }
 
-    public boolean hasProperty(NPCPropertyKey<?> key) {
+    public boolean hasProperty(NPCProperty<?> key) {
         return propertyMap.containsKey(key);
     }
 
-    public <T> void setProperty(NPCPropertyKey<T> key, T value) {
-        propertyMap.put(key, value);
-        key.update(this, value);
+    public <T> void setProperty(NPCProperty<T> key, T value) {
+        if (value.equals(key.getDefaultValue())) removeProperty(key);
+        else propertyMap.put(key, value);
     }
 
-    public void removeProperty(NPCPropertyKey<?> key) {
+    public void removeProperty(NPCProperty<?> key) {
         propertyMap.remove(key);
     }
 }
diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java b/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java
new file mode 100644
index 0000000..2752ed2
--- /dev/null
+++ b/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java
@@ -0,0 +1,38 @@
+package lol.pyr.znpcsplus.npc;
+
+import io.github.znetworkw.znpcservers.npc.NPCSkin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class NPCProperty<T> {
+    private final String name;
+    private final T defaultValue;
+
+    public NPCProperty(String name) {
+        this(name, null);
+    }
+
+    public NPCProperty(String name, T defaultValue) {
+        this.name = name.toUpperCase();
+        this.defaultValue = defaultValue;
+        BY_NAME.put(this.name, this);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    protected T getDefaultValue() {
+        return defaultValue;
+    }
+
+    private final static Map<String, NPCProperty<?>> BY_NAME = new HashMap<>();
+
+    public static NPCProperty<?> getByName(String name) {
+        return BY_NAME.get(name.toUpperCase());
+    }
+
+    public static NPCProperty<Boolean> SKIN_LAYERS = new NPCProperty<>("skin_layers", true);
+    public static NPCProperty<NPCSkin> SKIN = new NPCProperty<>("skin");
+}
\ No newline at end of file
diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPCPropertyKey.java b/src/main/java/lol/pyr/znpcsplus/npc/NPCPropertyKey.java
deleted file mode 100644
index 4cc09ca..0000000
--- a/src/main/java/lol/pyr/znpcsplus/npc/NPCPropertyKey.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package lol.pyr.znpcsplus.npc;
-
-import com.github.retrooper.packetevents.protocol.player.TextureProperty;
-import io.github.znetworkw.znpcservers.npc.NPCSkin;
-import lol.pyr.znpcsplus.entity.PacketPlayer;
-
-import java.util.List;
-
-public class NPCPropertyKey<T> {
-    private final UpdateCallback<T> updateCallback;
-
-    public NPCPropertyKey() {
-        this(null);
-    }
-
-    public NPCPropertyKey(UpdateCallback<T> updateCallback) {
-        this.updateCallback = updateCallback;
-    }
-
-    public void update(NPC npc, T value) {
-        if (updateCallback != null) updateCallback.onUpdate(npc, value);
-    }
-
-    @FunctionalInterface
-    public interface UpdateCallback<T> {
-        void onUpdate(NPC npc, T value);
-    }
-
-    public static NPCPropertyKey<NPCSkin> NPC_SKIN = new NPCPropertyKey<>((npc, skin) -> {
-        if (!(npc.getEntity() instanceof PacketPlayer entity))
-            throw new RuntimeException("Tried to set a skin on an entity that isn't a player");
-        entity.getGameProfile().setTextureProperties(List.of(new TextureProperty("textures", skin.getTexture(), skin.getSignature())));
-        npc.respawn();
-    });
-}
diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java b/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java
index 6773df1..3d81460 100644
--- a/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java
+++ b/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java
@@ -5,6 +5,7 @@ import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
+import java.util.Set;
 
 public class NPCType {
     private final static ImmutableList<NPCType> npcTypes;
@@ -14,19 +15,25 @@ public class NPCType {
     }
 
     private final EntityType type;
+    private final Set<NPCProperty<?>> allowedProperties;
 
-    public NPCType(EntityType type) {
+    public NPCType(EntityType type, NPCProperty<?>... allowedProperties) {
         this.type = type;
+        this.allowedProperties = Set.of(allowedProperties);
     }
 
     public EntityType getType() {
         return type;
     }
 
+    public Set<NPCProperty<?>> getAllowedProperties() {
+        return allowedProperties;
+    }
+
     static {
         ImmutableList.Builder<NPCType> builder = new ImmutableList.Builder<>();
 
-        builder.add(new NPCType(EntityTypes.PLAYER));
+        builder.add(new NPCType(EntityTypes.PLAYER, NPCProperty.SKIN));
         builder.add(new NPCType(EntityTypes.CREEPER));
         builder.add(new NPCType(EntityTypes.ZOMBIE));
         builder.add(new NPCType(EntityTypes.SKELETON));
diff --git a/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java b/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java
index e79702b..2005987 100644
--- a/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java
+++ b/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java
@@ -2,8 +2,8 @@ package lol.pyr.znpcsplus.packets;
 
 import com.github.retrooper.packetevents.PacketEvents;
 import com.github.retrooper.packetevents.manager.server.ServerVersion;
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
 import lol.pyr.znpcsplus.entity.PacketEntity;
-import lol.pyr.znpcsplus.entity.PacketPlayer;
 import lol.pyr.znpcsplus.util.LazyLoader;
 import org.bukkit.entity.Player;
 
@@ -11,14 +11,15 @@ import java.util.HashMap;
 import java.util.Map;
 
 public interface PacketFactory {
-    void spawnPlayer(Player player, PacketPlayer entity);
+    void spawnPlayer(Player player, PacketEntity entity);
     void spawnEntity(Player player, PacketEntity entity);
     void destroyEntity(Player player, PacketEntity entity);
     void teleportEntity(Player player, PacketEntity entity);
-    void addTabPlayer(Player player, PacketPlayer entity);
-    void removeTabPlayer(Player player, PacketPlayer entity);
-    void createTeam(Player player, PacketPlayer entity);
-    void removeTeam(Player player, PacketPlayer entity);
+    void addTabPlayer(Player player, PacketEntity entity);
+    void removeTabPlayer(Player player, PacketEntity entity);
+    void createTeam(Player player, PacketEntity entity);
+    void removeTeam(Player player, PacketEntity entity);
+    void sendMetadata(Player player, PacketEntity entity, EntityData... data);
 
     PacketFactory factory = get();
 
diff --git a/src/main/java/lol/pyr/znpcsplus/packets/V1_19Factory.java b/src/main/java/lol/pyr/znpcsplus/packets/V1_19Factory.java
index 230b3bc..3cd2a13 100644
--- a/src/main/java/lol/pyr/znpcsplus/packets/V1_19Factory.java
+++ b/src/main/java/lol/pyr/znpcsplus/packets/V1_19Factory.java
@@ -2,9 +2,10 @@ package lol.pyr.znpcsplus.packets;
 
 import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
 import com.github.retrooper.packetevents.protocol.player.GameMode;
+import com.github.retrooper.packetevents.protocol.player.UserProfile;
 import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoRemove;
 import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoUpdate;
-import lol.pyr.znpcsplus.entity.PacketPlayer;
+import lol.pyr.znpcsplus.entity.PacketEntity;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 
@@ -12,17 +13,17 @@ import java.util.EnumSet;
 
 public class V1_19Factory extends V1_14Factory {
     @Override
-    public void addTabPlayer(Player player, PacketPlayer entity) {
+    public void addTabPlayer(Player player, PacketEntity entity) {
         if (entity.getType() != EntityTypes.PLAYER) return;
         WrapperPlayServerPlayerInfoUpdate.PlayerInfo info = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo(
-                entity.getGameProfile(), false, 1, GameMode.CREATIVE,
-                Component.empty(), null);
+                skinned(entity, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))), false,
+                1, GameMode.CREATIVE, Component.empty(), null);
         sendPacket(player, new WrapperPlayServerPlayerInfoUpdate(EnumSet.of(WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER,
                 WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_LISTED), info, info));
     }
 
     @Override
-    public void removeTabPlayer(Player player, PacketPlayer entity) {
+    public void removeTabPlayer(Player player, PacketEntity entity) {
         if (entity.getType() != EntityTypes.PLAYER) return;
         sendPacket(player, new WrapperPlayServerPlayerInfoRemove(entity.getUuid()));
     }
diff --git a/src/main/java/lol/pyr/znpcsplus/packets/V1_8Factory.java b/src/main/java/lol/pyr/znpcsplus/packets/V1_8Factory.java
index 2951d1d..2a55199 100644
--- a/src/main/java/lol/pyr/znpcsplus/packets/V1_8Factory.java
+++ b/src/main/java/lol/pyr/znpcsplus/packets/V1_8Factory.java
@@ -1,17 +1,23 @@
 package lol.pyr.znpcsplus.packets;
 
 import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
 import com.github.retrooper.packetevents.protocol.entity.type.EntityType;
 import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
 import com.github.retrooper.packetevents.protocol.player.ClientVersion;
 import com.github.retrooper.packetevents.protocol.player.GameMode;
+import com.github.retrooper.packetevents.protocol.player.TextureProperty;
+import com.github.retrooper.packetevents.protocol.player.UserProfile;
 import com.github.retrooper.packetevents.util.Vector3d;
 import com.github.retrooper.packetevents.wrapper.PacketWrapper;
 import com.github.retrooper.packetevents.wrapper.play.server.*;
+import io.github.znetworkw.znpcservers.npc.NPCSkin;
 import lol.pyr.znpcsplus.ZNPCsPlus;
 import lol.pyr.znpcsplus.entity.PacketEntity;
 import lol.pyr.znpcsplus.entity.PacketLocation;
-import lol.pyr.znpcsplus.entity.PacketPlayer;
+import lol.pyr.znpcsplus.metadata.MetadataFactory;
+import lol.pyr.znpcsplus.npc.NPC;
+import lol.pyr.znpcsplus.npc.NPCProperty;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
 import org.bukkit.entity.Player;
@@ -21,12 +27,14 @@ import java.util.Optional;
 
 public class V1_8Factory implements PacketFactory {
     @Override
-    public void spawnPlayer(Player player, PacketPlayer entity) {
+    public void spawnPlayer(Player player, PacketEntity entity) {
+        NPC owner = entity.getOwner();
         addTabPlayer(player, entity);
         createTeam(player, entity);
         PacketLocation location = entity.getLocation();
         sendPacket(player, new WrapperPlayServerSpawnPlayer(entity.getEntityId(),
                 entity.getUuid(), location.toVector3d(), location.getYaw(), location.getPitch(), List.of()));
+        if (owner.getProperty(NPCProperty.SKIN_LAYERS)) sendMetadata(player, entity, MetadataFactory.get().skinLayers());
         ZNPCsPlus.SCHEDULER.scheduleSyncDelayedTask(() -> removeTabPlayer(player, entity), 60);
     }
 
@@ -45,7 +53,7 @@ public class V1_8Factory implements PacketFactory {
     @Override
     public void destroyEntity(Player player, PacketEntity entity) {
         sendPacket(player, new WrapperPlayServerDestroyEntities(entity.getEntityId()));
-        if (entity.getType() == EntityTypes.PLAYER) removeTeam(player, (PacketPlayer) entity);
+        if (entity.getType() == EntityTypes.PLAYER) removeTeam(player, entity);
     }
 
     @Override
@@ -56,22 +64,23 @@ public class V1_8Factory implements PacketFactory {
     }
 
     @Override
-    public void addTabPlayer(Player player, PacketPlayer entity) {
+    public void addTabPlayer(Player player, PacketEntity entity) {
         if (entity.getType() != EntityTypes.PLAYER) return;
         sendPacket(player, new WrapperPlayServerPlayerInfo(
-                WrapperPlayServerPlayerInfo.Action.ADD_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData(Component.text(""), entity.getGameProfile(), GameMode.CREATIVE, 1)));
+                WrapperPlayServerPlayerInfo.Action.ADD_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData(Component.text(""),
+                skinned(entity, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))), GameMode.CREATIVE, 1)));
     }
 
     @Override
-    public void removeTabPlayer(Player player, PacketPlayer entity) {
+    public void removeTabPlayer(Player player, PacketEntity entity) {
         if (entity.getType() != EntityTypes.PLAYER) return;
         sendPacket(player, new WrapperPlayServerPlayerInfo(
                 WrapperPlayServerPlayerInfo.Action.REMOVE_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData(null,
-                entity.getGameProfile(), null, -1)));
+                new UserProfile(entity.getUuid(), null), null, -1)));
     }
 
     @Override
-    public void createTeam(Player player, PacketPlayer entity) {
+    public void createTeam(Player player, PacketEntity entity) {
         sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.CREATE, new WrapperPlayServerTeams.ScoreBoardTeamInfo(
                 Component.empty(), Component.empty(), Component.empty(),
                 WrapperPlayServerTeams.NameTagVisibility.NEVER,
@@ -83,11 +92,24 @@ public class V1_8Factory implements PacketFactory {
     }
 
     @Override
-    public void removeTeam(Player player, PacketPlayer entity) {
+    public void removeTeam(Player player, PacketEntity entity) {
         sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.REMOVE, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null));
     }
 
+    @Override
+    public void sendMetadata(Player player, PacketEntity entity, EntityData... data) {
+        PacketEvents.getAPI().getPlayerManager().sendPacket(player, new WrapperPlayServerEntityMetadata(entity.getEntityId(), List.of(data)));
+    }
+
     protected void sendPacket(Player player, PacketWrapper<?> packet) {
         PacketEvents.getAPI().getPlayerManager().sendPacket(player, packet);
     }
+
+    protected UserProfile skinned(PacketEntity entity, UserProfile profile) {
+        NPC owner = entity.getOwner();
+        if (!owner.hasProperty(NPCProperty.SKIN)) return profile;
+        NPCSkin skin = owner.getProperty(NPCProperty.SKIN);
+        profile.setTextureProperties(List.of(new TextureProperty("textures", skin.getTexture(), skin.getSignature())));
+        return profile;
+    }
 }