From 8c0d6f07fa44958e4b43309a4fe4bcb877b5d30e Mon Sep 17 00:00:00 2001 From: Pyrbu Date: Tue, 25 Apr 2023 16:52:23 +0100 Subject: [PATCH] rewrite skins & add support for placeholders skins --- .../github/znetworkw/znpcservers/npc/NPC.java | 4 +- .../InteractionPacketListener.java | 2 +- .../znpcservers/user/EventService.java | 2 +- .../znpcservers/utility/SchedulerUtils.java | 8 +- .../java/lol/pyr/znpcsplus/ZNPCsPlus.java | 24 +++- src/main/java/lol/pyr/znpcsplus/npc/NPC.java | 5 + .../lol/pyr/znpcsplus/npc/NPCProperty.java | 4 +- .../java/lol/pyr/znpcsplus/npc/NPCType.java | 38 +++-- .../pyr/znpcsplus/packets/PacketFactory.java | 3 +- .../pyr/znpcsplus/packets/V1_19Factory.java | 19 ++- .../pyr/znpcsplus/packets/V1_8Factory.java | 52 ++++--- .../java/lol/pyr/znpcsplus/skin/Skin.java | 45 ++++++ .../pyr/znpcsplus/skin/SkinDescriptor.java | 11 ++ .../pyr/znpcsplus/skin/cache/CachedId.java | 18 +++ .../pyr/znpcsplus/skin/cache/SkinCache.java | 131 ++++++++++++++++++ .../skin/cache/SkinCacheCleanTask.java | 15 ++ .../skin/descriptor/FetchingDescriptor.java | 38 +++++ .../skin/descriptor/MirrorDescriptor.java | 28 ++++ .../skin/descriptor/PrefetchedDescriptor.java | 30 ++++ 19 files changed, 422 insertions(+), 55 deletions(-) create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/Skin.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptor.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCache.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/descriptor/FetchingDescriptor.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java create mode 100644 src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java diff --git a/src/main/java/io/github/znetworkw/znpcservers/npc/NPC.java b/src/main/java/io/github/znetworkw/znpcservers/npc/NPC.java index 0587a1c..1148672 100644 --- a/src/main/java/io/github/znetworkw/znpcservers/npc/NPC.java +++ b/src/main/java/io/github/znetworkw/znpcservers/npc/NPC.java @@ -234,7 +234,7 @@ public class NPC { if (npcIsPlayer) { if (FunctionFactory.isTrue(this, "mirror")) updateProfile(user.getGameProfile().getProperties()); Utils.sendPackets(user, this.tabConstructor, this.updateTabConstructor); - ZNPCsPlus.SCHEDULER.runTask(() -> { + ZNPCsPlus.SCHEDULER.runNextTick(() -> { PacketEvents.getAPI().getPlayerManager().sendPacket(player, new WrapperPlayServerSpawnPlayer(entityID, this.gameProfile.getId(), SpigotConversionUtil.fromBukkitLocation(location.toBukkitLocation()))); PacketEvents.getAPI().getPlayerManager().sendPacket(player, new WrapperPlayServerEntityMetadata(entityID, @@ -257,7 +257,7 @@ public class NPC { updateMetadata(Collections.singleton(user)); sendEquipPackets(user); lookAt(user, getLocation(), true); - if (npcIsPlayer) ZNPCsPlus.SCHEDULER.scheduleSyncDelayedTask(() -> { + if (npcIsPlayer) ZNPCsPlus.SCHEDULER.runTaskLaterSync(() -> { removeFromTab(player); Utils.sendPackets(user, this.updateTabConstructor); }, 60); diff --git a/src/main/java/io/github/znetworkw/znpcservers/npc/interaction/InteractionPacketListener.java b/src/main/java/io/github/znetworkw/znpcservers/npc/interaction/InteractionPacketListener.java index 97c232b..dd97b22 100644 --- a/src/main/java/io/github/znetworkw/znpcservers/npc/interaction/InteractionPacketListener.java +++ b/src/main/java/io/github/znetworkw/znpcservers/npc/interaction/InteractionPacketListener.java @@ -30,7 +30,7 @@ public class InteractionPacketListener implements PacketListener { ClickType clickType = ClickType.forName(packet.getAction().name()); user.updateLastInteract(); - ZNPCsPlus.SCHEDULER.runTask(() -> { + ZNPCsPlus.SCHEDULER.runNextTick(() -> { Bukkit.getServer().getPluginManager().callEvent(new NPCInteractEvent(player, clickType, npc)); List actions = npc.getNpcPojo().getClickActions(); if (actions == null) return; diff --git a/src/main/java/io/github/znetworkw/znpcservers/user/EventService.java b/src/main/java/io/github/znetworkw/znpcservers/user/EventService.java index 7c8d898..5674204 100644 --- a/src/main/java/io/github/znetworkw/znpcservers/user/EventService.java +++ b/src/main/java/io/github/znetworkw/znpcservers/user/EventService.java @@ -31,7 +31,7 @@ public class EventService { } public void runAll(T event) { - ZNPCsPlus.SCHEDULER.runTask(() -> this.eventConsumers.forEach(consumer -> consumer.accept(event))); + ZNPCsPlus.SCHEDULER.runNextTick(() -> this.eventConsumers.forEach(consumer -> consumer.accept(event))); } public static EventService addService(ZUser user, Class eventClass) { diff --git a/src/main/java/io/github/znetworkw/znpcservers/utility/SchedulerUtils.java b/src/main/java/io/github/znetworkw/znpcservers/utility/SchedulerUtils.java index b43c832..394b89e 100644 --- a/src/main/java/io/github/znetworkw/znpcservers/utility/SchedulerUtils.java +++ b/src/main/java/io/github/znetworkw/znpcservers/utility/SchedulerUtils.java @@ -24,11 +24,15 @@ public class SchedulerUtils { return Bukkit.getScheduler().runTaskTimerAsynchronously(this.plugin, runnable, delay, continuousDelay); } - public void scheduleSyncDelayedTask(Runnable runnable, int delay) { + public void runTaskLaterSync(Runnable runnable, int delay) { Bukkit.getScheduler().scheduleSyncDelayedTask(this.plugin, runnable, delay); } - public BukkitTask runTask(Runnable runnable) { + public BukkitTask runNextTick(Runnable runnable) { return Bukkit.getScheduler().runTask(this.plugin, runnable); } + + public void runAsync(Runnable runnable) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable); + } } diff --git a/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java b/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java index 8ef9a4d..63e5ee5 100644 --- a/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java +++ b/src/main/java/lol/pyr/znpcsplus/ZNPCsPlus.java @@ -12,10 +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; import io.github.znetworkw.znpcservers.user.ZUser; import io.github.znetworkw.znpcservers.utility.BungeeUtils; import io.github.znetworkw.znpcservers.utility.SchedulerUtils; @@ -26,6 +23,11 @@ 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.skin.cache.SkinCache; +import lol.pyr.znpcsplus.skin.cache.SkinCacheCleanTask; +import lol.pyr.znpcsplus.skin.descriptor.FetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; import lol.pyr.znpcsplus.tasks.NPCVisibilityTask; import lol.pyr.znpcsplus.updater.UpdateChecker; import lol.pyr.znpcsplus.updater.UpdateNotificationListener; @@ -57,6 +59,7 @@ public class ZNPCsPlus extends JavaPlugin { public static SchedulerUtils SCHEDULER; public static BungeeUtils BUNGEE_UTILS; public static BukkitAudiences ADVENTURE; + public static boolean PLACEHOLDERS_SUPPORTED; private boolean enabled = false; @@ -106,6 +109,9 @@ public class ZNPCsPlus extends JavaPlugin { PacketEvents.getAPI().getEventManager().registerListener(new InteractionPacketListener(), PacketListenerPriority.MONITOR); PacketEvents.getAPI().init(); + PLACEHOLDERS_SUPPORTED = Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"); + if (PLACEHOLDERS_SUPPORTED) log(ChatColor.WHITE + " * Enabling PlaceholderAPI Support..."); + PLUGIN_FOLDER.mkdirs(); PATH_FOLDER.mkdirs(); @@ -121,11 +127,10 @@ public class ZNPCsPlus extends JavaPlugin { Bukkit.getOnlinePlayers().forEach(ZUser::find); log(ChatColor.WHITE + " * Starting tasks..."); - new NPCPositionTask(this); new NPCVisibilityTask(this); - new NPCSaveTask(this, ConfigurationConstants.SAVE_DELAY); new PlayerListener(this); new InventoryListener(this); + new SkinCacheCleanTask(this); if (ConfigurationConstants.CHECK_FOR_UPDATES) new UpdateNotificationListener(this, new UpdateChecker(this)); enabled = true; @@ -141,7 +146,7 @@ public class ZNPCsPlus extends JavaPlugin { for (NPCType type : NPCType.values()) { NPC npc = new NPC(world, type, new PacketLocation(x * 3, 200, z * 3, 0, 0)); if (type.getType() == EntityTypes.PLAYER) { - NPCSkin.forName("Notch", (skin, ex) -> npc.setProperty(NPCProperty.SKIN, skin)); + SkinCache.fetchByName("Notch").thenAccept(skin -> npc.setProperty(NPCProperty.SKIN, new PrefetchedDescriptor(skin))); } npc.setProperty(NPCProperty.GLOW, NamedTextColor.RED); npc.setProperty(NPCProperty.FIRE, true); @@ -151,6 +156,13 @@ public class ZNPCsPlus extends JavaPlugin { z++; } } + NPC npc = new NPC(world, NPCType.byName("player"), new PacketLocation(x * 3, 200, z * 3, 0, 0)); + npc.setProperty(NPCProperty.SKIN, new FetchingDescriptor("jeb_")); + NPCRegistry.register("debug_npc" + (z * wrap + x), npc); + x++; + npc = new NPC(world, NPCType.byName("player"), new PacketLocation(x * 3, 200, z * 3, 0, 0)); + npc.setProperty(NPCProperty.SKIN, new MirrorDescriptor()); + NPCRegistry.register("debug_npc" + (z * wrap + x), npc); } } diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPC.java b/src/main/java/lol/pyr/znpcsplus/npc/NPC.java index 0be9acc..bd14db7 100644 --- a/src/main/java/lol/pyr/znpcsplus/npc/NPC.java +++ b/src/main/java/lol/pyr/znpcsplus/npc/NPC.java @@ -65,6 +65,11 @@ public class NPC { _showAll(); } + public void respawn(Player player) { + _hide(player); + _show(player); + } + public void show(Player player) { if (viewers.contains(player)) return; _show(player); diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java b/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java index 9496537..588aca8 100644 --- a/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java +++ b/src/main/java/lol/pyr/znpcsplus/npc/NPCProperty.java @@ -1,6 +1,6 @@ package lol.pyr.znpcsplus.npc; -import io.github.znetworkw.znpcservers.npc.NPCSkin; +import lol.pyr.znpcsplus.skin.SkinDescriptor; import net.kyori.adventure.text.format.NamedTextColor; import java.util.HashMap; @@ -35,7 +35,7 @@ public class NPCProperty { } public static NPCProperty SKIN_LAYERS = new NPCProperty<>("skin_layers", true); - public static NPCProperty SKIN = new NPCProperty<>("skin"); + public static NPCProperty SKIN = new NPCProperty<>("skin"); public static NPCProperty GLOW = new NPCProperty<>("glow"); public static NPCProperty FIRE = new NPCProperty<>("fire", false); } \ No newline at end of file diff --git a/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java b/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java index f29463c..5ea159a 100644 --- a/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java +++ b/src/main/java/lol/pyr/znpcsplus/npc/NPCType.java @@ -5,22 +5,25 @@ import com.github.retrooper.packetevents.manager.server.ServerVersion; import com.github.retrooper.packetevents.protocol.entity.type.EntityType; import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; public class NPCType { - private final static Set npcTypes; + private final static Map BY_NAME = new HashMap<>(); - public static Set values() { - return npcTypes; + public static Collection values() { + return BY_NAME.values(); + } + + public static NPCType byName(String name) { + return BY_NAME.get(name.toUpperCase()); } private final EntityType type; private final Set> allowedProperties; + private final String name; - public NPCType(EntityType type, NPCProperty... allowedProperties) { + private NPCType(String name, EntityType type, NPCProperty... allowedProperties) { + this.name = name.toUpperCase(); this.type = type; ArrayList> list = new ArrayList<>(List.of(allowedProperties)); list.add(NPCProperty.FIRE); @@ -28,6 +31,10 @@ public class NPCType { this.allowedProperties = Set.copyOf(list); } + public String getName() { + return name; + } + public EntityType getType() { return type; } @@ -36,12 +43,15 @@ public class NPCType { return allowedProperties; } + private static void register(NPCType type) { + BY_NAME.put(type.getName(), type); + + } + static { - Set set = new HashSet<>(); - set.add(new NPCType(EntityTypes.PLAYER, NPCProperty.SKIN, NPCProperty.SKIN_LAYERS)); - set.add(new NPCType(EntityTypes.CREEPER)); - set.add(new NPCType(EntityTypes.ZOMBIE)); - set.add(new NPCType(EntityTypes.SKELETON)); - npcTypes = Set.copyOf(set); + register(new NPCType("player", EntityTypes.PLAYER, NPCProperty.SKIN, NPCProperty.SKIN_LAYERS)); + register(new NPCType("creeper", EntityTypes.CREEPER)); + register(new NPCType("zombie", EntityTypes.ZOMBIE)); + register(new NPCType("skeleton", 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 6c5f9f9..9cdaf43 100644 --- a/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java +++ b/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java @@ -9,13 +9,14 @@ import org.bukkit.entity.Player; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; public interface PacketFactory { 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, PacketEntity entity); + CompletableFuture addTabPlayer(Player player, PacketEntity entity); void removeTabPlayer(Player player, PacketEntity entity); void createTeam(Player player, PacketEntity entity); void removeTeam(Player player, PacketEntity entity); 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 3cd2a13..ad0b12c 100644 --- a/src/main/java/lol/pyr/znpcsplus/packets/V1_19Factory.java +++ b/src/main/java/lol/pyr/znpcsplus/packets/V1_19Factory.java @@ -10,16 +10,21 @@ import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; import java.util.EnumSet; +import java.util.concurrent.CompletableFuture; public class V1_19Factory extends V1_14Factory { @Override - public void addTabPlayer(Player player, PacketEntity entity) { - if (entity.getType() != EntityTypes.PLAYER) return; - WrapperPlayServerPlayerInfoUpdate.PlayerInfo info = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo( - 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)); + public CompletableFuture addTabPlayer(Player player, PacketEntity entity) { + if (entity.getType() != EntityTypes.PLAYER) return CompletableFuture.completedFuture(null); + CompletableFuture future = new CompletableFuture<>(); + skinned(player, entity, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))).thenAccept(profile -> { + WrapperPlayServerPlayerInfoUpdate.PlayerInfo info = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo( + profile, false, 1, GameMode.CREATIVE, Component.empty(), null); + sendPacket(player, new WrapperPlayServerPlayerInfoUpdate(EnumSet.of(WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER, + WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_LISTED), info, info)); + future.complete(null); + }); + return future; } @Override 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 5c8be45..8b32f96 100644 --- a/src/main/java/lol/pyr/znpcsplus/packets/V1_8Factory.java +++ b/src/main/java/lol/pyr/znpcsplus/packets/V1_8Factory.java @@ -6,35 +6,36 @@ 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.metadata.MetadataFactory; import lol.pyr.znpcsplus.npc.NPC; import lol.pyr.znpcsplus.npc.NPCProperty; +import lol.pyr.znpcsplus.skin.SkinDescriptor; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; public class V1_8Factory implements PacketFactory { @Override public void spawnPlayer(Player player, PacketEntity entity) { - 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())); - sendAllMetadata(player, entity); - ZNPCsPlus.SCHEDULER.scheduleSyncDelayedTask(() -> removeTabPlayer(player, entity), 60); + addTabPlayer(player, entity).thenAccept(ignored -> { + createTeam(player, entity); + PacketLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnPlayer(entity.getEntityId(), + entity.getUuid(), location.toVector3d(), location.getYaw(), location.getPitch(), List.of())); + sendAllMetadata(player, entity); + ZNPCsPlus.SCHEDULER.runTaskLaterSync(() -> removeTabPlayer(player, entity), 60); + }); } @Override @@ -64,11 +65,16 @@ public class V1_8Factory implements PacketFactory { } @Override - 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(""), - skinned(entity, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))), GameMode.CREATIVE, 1))); + public CompletableFuture addTabPlayer(Player player, PacketEntity entity) { + if (entity.getType() != EntityTypes.PLAYER) return CompletableFuture.completedFuture(null); + CompletableFuture future = new CompletableFuture<>(); + skinned(player, entity, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))).thenAccept(profile -> { + sendPacket(player, new WrapperPlayServerPlayerInfo( + WrapperPlayServerPlayerInfo.Action.ADD_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData(Component.text(""), + profile, GameMode.CREATIVE, 1))); + future.complete(null); + }); + return future; } @Override @@ -114,11 +120,19 @@ public class V1_8Factory implements PacketFactory { PacketEvents.getAPI().getPlayerManager().sendPacket(player, packet); } - protected UserProfile skinned(PacketEntity entity, UserProfile profile) { + protected CompletableFuture skinned(Player player, 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; + if (!owner.hasProperty(NPCProperty.SKIN)) return CompletableFuture.completedFuture(profile); + SkinDescriptor descriptor = owner.getProperty(NPCProperty.SKIN); + if (descriptor.supportsInstant(player)) { + descriptor.fetchInstant(player).apply(profile); + return CompletableFuture.completedFuture(profile); + } + CompletableFuture future = new CompletableFuture<>(); + descriptor.fetch(player).thenAccept(skin -> { + skin.apply(profile); + future.complete(profile); + }); + return future; } } diff --git a/src/main/java/lol/pyr/znpcsplus/skin/Skin.java b/src/main/java/lol/pyr/znpcsplus/skin/Skin.java new file mode 100644 index 0000000..7954304 --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/Skin.java @@ -0,0 +1,45 @@ +package lol.pyr.znpcsplus.skin; + +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.authlib.properties.PropertyMap; + +import java.util.ArrayList; +import java.util.List; + +public class Skin { + private final long timestamp = System.currentTimeMillis(); + private final List properties = new ArrayList<>(); + + public Skin(String texture, String signature) { + properties.add(new TextureProperty("textures", texture, signature)); + } + + public Skin(TextureProperty... properties) { + this.properties.addAll(List.of(properties)); + } + + public Skin(PropertyMap properties) { + this.properties.addAll(properties.values().stream() + .map(property -> new TextureProperty(property.getName(), property.getValue(), property.getSignature())) + .toList()); + } + + public Skin(JsonObject obj) { + for (JsonElement e : obj.get("properties").getAsJsonArray()) { + JsonObject o = e.getAsJsonObject(); + properties.add(new TextureProperty(o.get("name").getAsString(), o.get("value").getAsString(), o.has("signature") ? o.get("signature").getAsString() : null)); + } + } + + public UserProfile apply(UserProfile profile) { + profile.setTextureProperties(properties); + return profile; + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > 60000L; + } +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptor.java b/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptor.java new file mode 100644 index 0000000..0c5ad23 --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptor.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.skin; + +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public interface SkinDescriptor { + CompletableFuture fetch(Player player); + Skin fetchInstant(Player player); + boolean supportsInstant(Player player); +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java b/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java new file mode 100644 index 0000000..cf60441 --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.skin.cache; + +public class CachedId { + private final long timestamp = System.currentTimeMillis(); + private final String id; + + public CachedId(String id) { + this.id = id; + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > 60000L; + } + + public String getId() { + return id; + } +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCache.java b/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCache.java new file mode 100644 index 0000000..ff2afec --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCache.java @@ -0,0 +1,131 @@ +package lol.pyr.znpcsplus.skin.cache; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.authlib.GameProfile; +import io.github.znetworkw.znpcservers.reflection.Reflections; +import lol.pyr.znpcsplus.skin.Skin; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +public class SkinCache { + private final static Map cache = new ConcurrentHashMap<>(); + private final static Map idCache = new ConcurrentHashMap<>(); + + public static void cleanCache() { + for (Map.Entry entry : cache.entrySet()) if (entry.getValue().isExpired()) cache.remove(entry.getKey()); + for (Map.Entry entry : idCache.entrySet()) if (entry.getValue().isExpired()) cache.remove(entry.getKey()); + } + + public static CompletableFuture fetchByName(String name) { + Player player = Bukkit.getPlayerExact(name); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (cache.containsKey(name.toLowerCase())) return fetchByUUID(idCache.get(name.toLowerCase()).getId()); + + CompletableFuture future = new CompletableFuture<>(); + CompletableFuture.runAsync(() -> { + URL url = parseUrl("https://api.mojang.com/users/profiles/minecraft/" + name); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("errorMessage")) future.complete(null); + String id = obj.get("id").getAsString(); + idCache.put(name.toLowerCase(), new CachedId(id)); + fetchByUUID(id).thenAccept(future::complete); + } + } catch (IOException exception) { + exception.printStackTrace(); + } finally { + if (connection != null) connection.disconnect(); + } + }); + return future; + } + + public static CompletableFuture fetchByUUID(UUID uuid) { + return fetchByUUID(uuid.toString().replace("-", "")); + } + + public static boolean isNameFullyCached(String s) { + String name = s.toLowerCase(); + if (!idCache.containsKey(name)) return false; + CachedId id = idCache.get(name); + if (id.isExpired() || !cache.containsKey(id.getId())) return false; + Skin skin = cache.get(id.getId()); + return !skin.isExpired(); + } + + public static Skin getFullyCachedByName(String s) { + String name = s.toLowerCase(); + if (!idCache.containsKey(name)) return null; + CachedId id = idCache.get(name); + if (id.isExpired() || !cache.containsKey(id.getId())) return null; + Skin skin = cache.get(id.getId()); + if (skin.isExpired()) return null; + return skin; + } + + public static CompletableFuture fetchByUUID(String uuid) { + Player player = Bukkit.getPlayer(uuid); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (cache.containsKey(uuid)) { + Skin skin = cache.get(uuid); + if (!skin.isExpired()) return CompletableFuture.completedFuture(skin); + } + + return CompletableFuture.supplyAsync(() -> { + URL url = parseUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + Skin skin = new Skin(JsonParser.parseReader(reader).getAsJsonObject()); + cache.put(uuid, skin); + return skin; + } + } catch (IOException exception) { + exception.printStackTrace(); + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public static Skin getFromPlayer(Player player) { + try { + Object playerHandle = Reflections.GET_HANDLE_PLAYER_METHOD.get().invoke(player); + GameProfile gameProfile = (GameProfile) Reflections.GET_PROFILE_METHOD.get().invoke(playerHandle, new Object[0]); + return new Skin(gameProfile.getProperties()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static URL parseUrl(String url) { + try { + return new URL(url); + } catch (MalformedURLException exception) { + throw new RuntimeException(exception); + } + } +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java b/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java new file mode 100644 index 0000000..443d0ca --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java @@ -0,0 +1,15 @@ +package lol.pyr.znpcsplus.skin.cache; + +import lol.pyr.znpcsplus.ZNPCsPlus; +import org.bukkit.scheduler.BukkitRunnable; + +public class SkinCacheCleanTask extends BukkitRunnable { + public SkinCacheCleanTask(ZNPCsPlus plugin) { + runTaskTimerAsynchronously(plugin, 1200, 1200); + } + + @Override + public void run() { + SkinCache.cleanCache(); + } +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/descriptor/FetchingDescriptor.java b/src/main/java/lol/pyr/znpcsplus/skin/descriptor/FetchingDescriptor.java new file mode 100644 index 0000000..7c024cc --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/descriptor/FetchingDescriptor.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.ZNPCsPlus; +import lol.pyr.znpcsplus.skin.Skin; +import lol.pyr.znpcsplus.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.cache.SkinCache; +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class FetchingDescriptor implements SkinDescriptor { + private final String name; + + public FetchingDescriptor(String name) { + this.name = name; + } + + @Override + public CompletableFuture fetch(Player player) { + return SkinCache.fetchByName(papi(player)); + } + + @Override + public Skin fetchInstant(Player player) { + return SkinCache.getFullyCachedByName(papi(player)); + } + + @Override + public boolean supportsInstant(Player player) { + return SkinCache.isNameFullyCached(papi(player)); + } + + private String papi(Player player) { + if (ZNPCsPlus.PLACEHOLDERS_SUPPORTED) return PlaceholderAPI.setPlaceholders(player, name); + return name; + } +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java b/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java new file mode 100644 index 0000000..d2d124d --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.skin.Skin; +import lol.pyr.znpcsplus.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.cache.SkinCache; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class MirrorDescriptor implements SkinDescriptor { + + public MirrorDescriptor() {} + + @Override + public CompletableFuture fetch(Player player) { + return CompletableFuture.completedFuture(SkinCache.getFromPlayer(player)); + } + + @Override + public Skin fetchInstant(Player player) { + return SkinCache.getFromPlayer(player); + } + + @Override + public boolean supportsInstant(Player player) { + return true; + } +} diff --git a/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java b/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java new file mode 100644 index 0000000..c08d2e6 --- /dev/null +++ b/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.skin.Skin; +import lol.pyr.znpcsplus.skin.SkinDescriptor; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class PrefetchedDescriptor implements SkinDescriptor { + private final Skin skin; + + public PrefetchedDescriptor(Skin skin) { + this.skin = skin; + } + + @Override + public CompletableFuture fetch(Player player) { + return CompletableFuture.completedFuture(skin); + } + + @Override + public Skin fetchInstant(Player player) { + return skin; + } + + @Override + public boolean supportsInstant(Player player) { + return true; + } +}