From 9f7672153a7f47a56fee649c50bc0d9ec2b5a2a6 Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 06:35:27 +0000 Subject: [PATCH 01/10] Use a more modern method to check if the server is in online mode This method will also return true if the proxy is configured as online mode, whereas the other one did not and presumably always applied skins when behind a proxy. --- .../pw/kaboom/extras/modules/player/PlayerConnection.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java index b2be174..30c1a3d 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java +++ b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java @@ -8,7 +8,6 @@ import net.kyori.adventure.title.Title; import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.Server; import org.bukkit.World; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; @@ -145,10 +144,9 @@ void onPlayerLogin(final PlayerLoginEvent event) { player.setOp(true); } - final Server server = Bukkit.getServer(); + final var serverConfig = Bukkit.getServer().getServerConfig(); - - if (!server.getOnlineMode()) { + if (!serverConfig.isProxyOnlineMode()) { SkinManager.applySkin(player, player.getName(), false); } } From b4718be725d92ad834cff7193b88bcf4bcf24fbd Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 06:48:31 +0000 Subject: [PATCH 02/10] Use the singleton pattern instead of JavaPlugin.getPlugin --- src/main/java/pw/kaboom/extras/Main.java | 6 ++++++ .../java/pw/kaboom/extras/modules/entity/EntitySpawn.java | 3 +-- .../pw/kaboom/extras/modules/player/PlayerConnection.java | 3 +-- .../java/pw/kaboom/extras/modules/player/PlayerPrefix.java | 5 ++--- .../java/pw/kaboom/extras/modules/server/ServerCommand.java | 3 +-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/Main.java b/src/main/java/pw/kaboom/extras/Main.java index 3e23caa..5ea63a9 100644 --- a/src/main/java/pw/kaboom/extras/Main.java +++ b/src/main/java/pw/kaboom/extras/Main.java @@ -20,9 +20,15 @@ import java.io.File; public final class Main extends JavaPlugin { + public static Main PLUGIN; + private File prefixConfigFile; private FileConfiguration prefixConfig; + public Main() { + PLUGIN = this; + } + @Override public void onLoad() { /* Load missing config.yml defaults */ diff --git a/src/main/java/pw/kaboom/extras/modules/entity/EntitySpawn.java b/src/main/java/pw/kaboom/extras/modules/entity/EntitySpawn.java index 2b239c3..42fa4d9 100644 --- a/src/main/java/pw/kaboom/extras/modules/entity/EntitySpawn.java +++ b/src/main/java/pw/kaboom/extras/modules/entity/EntitySpawn.java @@ -32,12 +32,11 @@ import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent; import com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent; -import org.bukkit.plugin.java.JavaPlugin; import pw.kaboom.extras.Main; import pw.kaboom.extras.util.Utility; public final class EntitySpawn implements Listener { - private static final FileConfiguration CONFIG = JavaPlugin.getPlugin(Main.class).getConfig(); + private static final FileConfiguration CONFIG = Main.PLUGIN.getConfig(); private static final int MAX_ENTITIES_PER_CHUNK = CONFIG.getInt("maxEntitiesPerChunk"); public static final int MAX_ENTITIES_PER_WORLD = CONFIG.getInt("maxEntitiesPerWorld"); diff --git a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java index 30c1a3d..30011b9 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java +++ b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java @@ -15,7 +15,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.*; import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.bukkit.plugin.java.JavaPlugin; import pw.kaboom.extras.Main; import pw.kaboom.extras.modules.server.ServerTabComplete; import pw.kaboom.extras.modules.player.skin.SkinManager; @@ -27,7 +26,7 @@ import java.util.concurrent.ThreadLocalRandom; public final class PlayerConnection implements Listener { - private static final FileConfiguration CONFIG = JavaPlugin.getPlugin(Main.class).getConfig(); + private static final FileConfiguration CONFIG = Main.PLUGIN.getConfig(); private static final Component TITLE = LegacyComponentSerializer.legacySection() .deserialize( diff --git a/src/main/java/pw/kaboom/extras/modules/player/PlayerPrefix.java b/src/main/java/pw/kaboom/extras/modules/player/PlayerPrefix.java index efae707..2721717 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/PlayerPrefix.java +++ b/src/main/java/pw/kaboom/extras/modules/player/PlayerPrefix.java @@ -11,10 +11,8 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitScheduler; import org.jetbrains.annotations.Nullable; -import pw.kaboom.extras.Main; import java.io.File; import java.io.IOException; @@ -23,8 +21,9 @@ import java.util.Map; import java.util.UUID; +import static pw.kaboom.extras.Main.PLUGIN; + public final class PlayerPrefix implements Listener { - private static final Main PLUGIN = JavaPlugin.getPlugin(Main.class); private static final File PREFIX_CONFIG_FILE = PLUGIN.getPrefixConfigFile(); private static final FileConfiguration PREFIX_CONFIG = PLUGIN.getPrefixConfig(); private static final FileConfiguration PLUGIN_CONFIGURATION = PLUGIN.getConfig(); diff --git a/src/main/java/pw/kaboom/extras/modules/server/ServerCommand.java b/src/main/java/pw/kaboom/extras/modules/server/ServerCommand.java index a2dac79..20b9134 100644 --- a/src/main/java/pw/kaboom/extras/modules/server/ServerCommand.java +++ b/src/main/java/pw/kaboom/extras/modules/server/ServerCommand.java @@ -8,7 +8,6 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerCommandEvent; -import org.bukkit.plugin.java.JavaPlugin; import pw.kaboom.extras.Main; import java.util.Arrays; @@ -21,7 +20,7 @@ public final class ServerCommand implements Listener { private static final Pattern SELECTOR_PATTERN = Pattern.compile("(?>\\s)*@[aenprs](?>\\s)*"); - private static final Logger LOGGER = JavaPlugin.getPlugin(Main.class).getLogger(); + private static final Logger LOGGER = Main.PLUGIN.getLogger(); private static final Set BLOCKED_EXECUTE_COMMANDS = ImmutableSet.of( "clone", "fill", "give", "kick", "locate", "me", "msg", "save-all", "say", From 80248b5feb99ac84ad2ab906c165511d24613d2c Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 08:12:01 +0000 Subject: [PATCH 03/10] Rewrite the SkinManager to avoid race conditions & Mojang ratelimit Also removes the per-player cooldown since a maximum of 2 requests is sent per second. UUID request results are also cached. --- .../kaboom/extras/commands/CommandSkin.java | 17 +- .../modules/player/PlayerConnection.java | 19 +- .../modules/player/skin/SkinFillRequest.java | 12 + .../modules/player/skin/SkinManager.java | 251 ++++++++++-------- 4 files changed, 163 insertions(+), 136 deletions(-) create mode 100644 src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java diff --git a/src/main/java/pw/kaboom/extras/commands/CommandSkin.java b/src/main/java/pw/kaboom/extras/commands/CommandSkin.java index 2afe159..cbe06e7 100644 --- a/src/main/java/pw/kaboom/extras/commands/CommandSkin.java +++ b/src/main/java/pw/kaboom/extras/commands/CommandSkin.java @@ -13,8 +13,6 @@ import java.util.Map; public final class CommandSkin implements CommandExecutor { - private final Map lastUsedMillis = new HashMap<>(); - @Override public boolean onCommand(final @Nonnull CommandSender sender, final @Nonnull Command command, @@ -26,9 +24,6 @@ public boolean onCommand(final @Nonnull CommandSender sender, return true; } - final long millis = lastUsedMillis.getOrDefault(player, 0L); - final long millisDifference = System.currentTimeMillis() - millis; - if (args.length == 0) { player.sendMessage(Component .text("Usage: /" + label + " \n/" + label + " off", @@ -36,14 +31,6 @@ public boolean onCommand(final @Nonnull CommandSender sender, return true; } - if (millisDifference <= 2000) { - player.sendMessage(Component - .text("Please wait a few seconds before changing your skin")); - return true; - } - - lastUsedMillis.put(player, System.currentTimeMillis()); - final String name = args[0]; if (name.equalsIgnoreCase("off") || name.equalsIgnoreCase("remove") @@ -54,13 +41,13 @@ public boolean onCommand(final @Nonnull CommandSender sender, if (name.equalsIgnoreCase("auto") || name.equalsIgnoreCase("default") || name.equalsIgnoreCase("reset")) { - SkinManager.applySkin(player, player.getName(), true); + SkinManager.requestSkin(player, player.getName(), true); return true; } final boolean shouldSendMessage = true; - SkinManager.applySkin(player, name, shouldSendMessage); + SkinManager.requestSkin(player, name, shouldSendMessage); return true; } } diff --git a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java index 30011b9..7e55b26 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java +++ b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java @@ -12,6 +12,7 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.*; import org.bukkit.event.player.PlayerLoginEvent.Result; @@ -96,6 +97,18 @@ void onAsyncPlayerPreLogin(final AsyncPlayerPreLoginEvent event) { }*/ } + @EventHandler(priority = EventPriority.LOWEST) + void onEarlyPlayerJoin(final PlayerJoinEvent event) { + final var player = event.getPlayer(); + + // Must be done in the PlayerJoinEvent instead of PlayerLoginEvent otherwise skins may fetch + // after the player joins + final var serverConfig = Bukkit.getServer().getServerConfig(); + + if (!serverConfig.isProxyOnlineMode()) + SkinManager.requestSkin(player, player.getName(), false); + } + @EventHandler void onPlayerJoin(final PlayerJoinEvent event) { final Player player = event.getPlayer(); @@ -142,12 +155,6 @@ void onPlayerLogin(final PlayerLoginEvent event) { if (OP_ON_JOIN && !player.isOp()) { player.setOp(true); } - - final var serverConfig = Bukkit.getServer().getServerConfig(); - - if (!serverConfig.isProxyOnlineMode()) { - SkinManager.applySkin(player, player.getName(), false); - } } @EventHandler diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java new file mode 100644 index 0000000..1d0f1e4 --- /dev/null +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java @@ -0,0 +1,12 @@ +package pw.kaboom.extras.modules.player.skin; + +import org.bukkit.entity.Player; +import org.jspecify.annotations.Nullable; + +import java.lang.ref.WeakReference; +import java.util.function.Consumer; + +public record SkinFillRequest(WeakReference fromPlayer, + String toUser, Consumer<@Nullable SkinData> resultConsumer) { + +} diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index 02fec19..5dca9c0 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -2,41 +2,35 @@ import com.google.gson.Gson; import java.lang.InterruptedException; +import java.lang.ref.WeakReference; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.util.List; -import java.util.UUID; +import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; +import java.util.regex.Pattern; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; -import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import net.kyori.adventure.text.Component; -import org.bukkit.scheduler.BukkitScheduler; -import pw.kaboom.extras.Main; +import org.jspecify.annotations.Nullable; import pw.kaboom.extras.modules.player.skin.response.ProfileResponse; import pw.kaboom.extras.modules.player.skin.response.SkinResponse; -public final class SkinManager { - private static final HttpClient httpClient = HttpClient.newHttpClient(); +import static pw.kaboom.extras.Main.PLUGIN; + +public final class SkinManager extends Thread { + private static final Pattern PREMIUM_USERNAME = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); + private static final Pattern UNDASHED_UUID = + Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})"); private static final Gson GSON = new Gson(); - private static final ExecutorService executorService = Executors - .newCachedThreadPool(); private static final URI SESSION_HOST = URI.create( System.getProperty( @@ -55,117 +49,145 @@ public final class SkinManager { ) ) ); + private static final Component ERROR_MESSAGE = Component.text("Couldn't set your skin."); + private static final BlockingQueue SKIN_REQUEST_QUEUE + = new LinkedBlockingQueue<>(); + private static final long EXECUTION_INTERVAL = 1000; - public static void resetSkin(final Player player, final boolean shouldSendMessage) { - executorService.submit(() -> { - final PlayerProfile playerProfile = player.getPlayerProfile(); - playerProfile.removeProperty("textures"); - - final BukkitScheduler bukkitScheduler = Bukkit.getScheduler(); - final Main plugin = JavaPlugin.getPlugin(Main.class); - - bukkitScheduler.runTask(plugin, () -> player.setPlayerProfile(playerProfile)); - - if(!shouldSendMessage) { - return; - } + static { + new SkinManager().start(); + } - player.sendMessage(Component.text("Successfully removed your skin")); - }); + private SkinManager() { + // avoid blocking jvm shutdown + this.setDaemon(true); } - public static void applySkin(final Player player, final String name, - final boolean shouldSendMessage) { - executorService.submit(() -> { - final PlayerProfile profile = player.getPlayerProfile(); - final SkinData skinData; - - try { - skinData = getSkinData(name).get(15, TimeUnit.SECONDS); - } catch (InterruptedException e) { - if (!shouldSendMessage) { - return; - } + @Override + public void run() { + final Map nameToIdCache = new HashMap<>(); + long lastRequest = 0; - player.sendMessage(Component.text("Skin fetching was interrupted")); - return; - } catch (TimeoutException e) { - if (!shouldSendMessage) { - return; - } + try(final var client = HttpClient.newHttpClient()) { + for(;;) { + final SkinFillRequest request = SKIN_REQUEST_QUEUE.take(); - player.sendMessage(Component.text("Took too long to fetch skin")); - return; - } catch (ExecutionException | CompletionException e) { - if(!shouldSendMessage) { - return; + final long diff = System.currentTimeMillis() - lastRequest; + if (diff < EXECUTION_INTERVAL) { + //noinspection BusyWait + Thread.sleep(EXECUTION_INTERVAL - diff); } - player.sendMessage(Component.text("A player with that username doesn't exist")); - return; - } + final var ply = request.fromPlayer().get(); + if (ply == null || !ply.isConnected()) continue; - final String texture = skinData.texture(); - final String signature = skinData.signature(); - profile.setProperty(new ProfileProperty("textures", texture, signature)); + final var toUser = request.toUser(); + UUID id = null; - final BukkitScheduler bukkitScheduler = Bukkit.getScheduler(); - final Main plugin = JavaPlugin.getPlugin(Main.class); + if (toUser.equalsIgnoreCase(ply.getName()) + && Bukkit.getServerConfig().isProxyOnlineMode()) + id = ply.getUniqueId(); - bukkitScheduler.runTask(plugin, - () -> player.setPlayerProfile(profile)); + id = nameToIdCache.getOrDefault(toUser, id); + final var resultConsumer = request.resultConsumer(); - if(!shouldSendMessage) { - return; + try { + if (id == null) { + lastRequest = System.currentTimeMillis(); + id = getUUID(client, toUser); + nameToIdCache.put(toUser, id); + } + + lastRequest = System.currentTimeMillis(); + // always refetch + final var skin = getSkinData(client, id); + resultConsumer.accept(skin); + } catch (Exception ignored) { + resultConsumer.accept(null); + } } + } catch (InterruptedException ignored) { - player.sendMessage(Component.text("Successfully set your skin to ") - .append(Component.text(name)) - .append(Component.text("'s"))); - }); + } } - public static CompletableFuture getSkinData(final String playerName) { - return CompletableFuture.supplyAsync(() -> { - final UUID uuid; - try { - uuid = getUUID(playerName).get(); - } catch (Exception e) { - throw new RuntimeException(e); - } + public static void resetSkin(final Player player, final boolean shouldSendMessage) { + setSkin(player, null); - try { - return getSkinData(uuid).get(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }, executorService); + if (shouldSendMessage) + player.sendMessage(Component.text("Successfully removed your skin")); } - public static CompletableFuture getSkinData(final UUID uuid) { - return CompletableFuture.supplyAsync(() -> { - final SkinResponse response = sendRequestForJSON( - SESSION_HOST, - "/session/minecraft/profile/" + uuid + "?unsigned=false", - SkinResponse.class - ); + private static void setSkin(final Player player, final @Nullable SkinData skinData) { + final var profile = player.getPlayerProfile(); + if (skinData != null) { + profile.setProperty(new ProfileProperty( + "textures", + skinData.texture(), skinData.signature())); + } else { + profile.removeProperty("texture"); + } - final List properties = response.properties(); + Bukkit.getScheduler().runTask(PLUGIN, () -> player.setPlayerProfile(profile)); + } - for (ProfileProperty property : properties) { - if(!property.getName().equals("textures")) { - continue; - } + public static void requestSkin(final Player player, final String name, + final boolean shouldSendMessage) { + if (!PREMIUM_USERNAME.matcher(name).matches()) { + if (shouldSendMessage) player.sendMessage(ERROR_MESSAGE); + return; + } - return new SkinData(property.getValue(), property.getSignature()); - } + SKIN_REQUEST_QUEUE.removeIf(skinFillRequest -> { + final var requestingPlayer = skinFillRequest.fromPlayer().get(); + if (requestingPlayer == null) return false; + return requestingPlayer.getUniqueId().equals(player.getUniqueId()); + }); - throw new RuntimeException("No textures property"); - }, executorService); + SKIN_REQUEST_QUEUE.add( + new SkinFillRequest( + new WeakReference<>(player), + name, + skinData -> { + if (skinData == null) { + if (shouldSendMessage) player.sendMessage(ERROR_MESSAGE); + return; + } + + setSkin(player, skinData); + if (shouldSendMessage) + player.sendMessage(Component.text("Successfully set your skin to ") + .append(Component.text(name)) + .append(Component.text("'s"))); + } + ) + ); } - private static T sendRequestForJSON(URI uri, String endpoint, Class clazz) { + private static SkinData getSkinData(final HttpClient client, final UUID uuid) { + final SkinResponse response = sendRequestForJSON( + client, + SESSION_HOST, + "/session/minecraft/profile/" + uuid + "?unsigned=false", + SkinResponse.class + ); + + final List properties = response.properties(); + + for (ProfileProperty property : properties) { + if (!property.getName().equals("textures")) + continue; + + + return new SkinData(property.getValue(), property.getSignature()); + } + + throw new RuntimeException("No textures property"); + } + + private static T sendRequestForJSON(final HttpClient client, final URI uri, + final String endpoint, final Class clazz) { final HttpRequest request = HttpRequest.newBuilder() .GET() .uri(uri.resolve(endpoint)) @@ -174,7 +196,7 @@ private static T sendRequestForJSON(URI uri, String endpoint, Class clazz final HttpResponse response; try { - response = httpClient.send(request, BodyHandlers.ofString()); + response = client.send(request, BodyHandlers.ofString()); } catch (Exception e) { throw new RuntimeException(e); } @@ -182,19 +204,18 @@ private static T sendRequestForJSON(URI uri, String endpoint, Class clazz return GSON.fromJson(response.body(), clazz); } - private static CompletableFuture getUUID(final String playerName) { - return CompletableFuture.supplyAsync(() -> { - final ProfileResponse parsedResponse = sendRequestForJSON( - PROFILE_ENDPOINT, - "/users/profiles/minecraft/" + playerName, - ProfileResponse.class - ); + private static UUID getUUID(final HttpClient client, final String playerName) { + final ProfileResponse parsedResponse = sendRequestForJSON( + client, + PROFILE_ENDPOINT, + "/users/profiles/minecraft/" + playerName, + ProfileResponse.class + ); - final String dashedUuid = parsedResponse - .id() - .replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); + final String dashedUuid = UNDASHED_UUID + .matcher(parsedResponse.id()) + .replaceAll("$1-$2-$3-$4-$5"); - return UUID.fromString(dashedUuid); - }, executorService); + return UUID.fromString(dashedUuid); } } From 3ad12951b1c13baea4f1690818793104b5a7bcd9 Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 16:34:34 +0000 Subject: [PATCH 04/10] Remove oldest queue entries when size exceeds >32 per suggestion --- .../java/pw/kaboom/extras/modules/player/skin/SkinManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index 5dca9c0..e1fefb5 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -70,6 +70,7 @@ public void run() { try(final var client = HttpClient.newHttpClient()) { for(;;) { + while (SKIN_REQUEST_QUEUE.size() > 32) SKIN_REQUEST_QUEUE.remove(); final SkinFillRequest request = SKIN_REQUEST_QUEUE.take(); final long diff = System.currentTimeMillis() - lastRequest; From f4a643263aab28d6d0875738d368541e596fe05e Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 16:37:13 +0000 Subject: [PATCH 05/10] Don't leak player instance --- .../extras/modules/player/skin/SkinManager.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index e1fefb5..4d1f7e3 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -146,19 +146,23 @@ public static void requestSkin(final Player player, final String name, return requestingPlayer.getUniqueId().equals(player.getUniqueId()); }); + final var weakRef = new WeakReference<>(player); SKIN_REQUEST_QUEUE.add( new SkinFillRequest( - new WeakReference<>(player), + weakRef, name, skinData -> { + final var strongPlayer = weakRef.get(); + if (strongPlayer == null) return; if (skinData == null) { - if (shouldSendMessage) player.sendMessage(ERROR_MESSAGE); + if (shouldSendMessage) strongPlayer.sendMessage(ERROR_MESSAGE); return; } - setSkin(player, skinData); + setSkin(strongPlayer, skinData); if (shouldSendMessage) - player.sendMessage(Component.text("Successfully set your skin to ") + strongPlayer + .sendMessage(Component.text("Successfully set your skin to ") .append(Component.text(name)) .append(Component.text("'s"))); } From 0bd187b541846c47fb18036ffe49e5f55f81dcc5 Mon Sep 17 00:00:00 2001 From: amy <144570677+amyavi@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:24:52 -0300 Subject: [PATCH 06/10] style: misc fixes --- .../modules/player/PlayerConnection.java | 13 --------- .../extras/modules/player/skin/SkinData.java | 4 +-- .../modules/player/skin/SkinManager.java | 8 +++--- .../player/skin/response/SkinResponse.java | 28 +------------------ 4 files changed, 6 insertions(+), 47 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java index 7e55b26..2b9fb98 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java +++ b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java @@ -82,19 +82,6 @@ void onAsyncPlayerPreLogin(final AsyncPlayerPreLoginEvent event) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Component.text("A player with that username is already logged in")); } - - /*try { - final PlayerProfile profile = event.getPlayerProfile(); - - UUID offlineUUID = UUID.nameUUIDFromBytes( - ("OfflinePlayer:" + event.getName()).getBytes(Charsets.UTF_8)); - - profile.setId(offlineUUID); - - SkinDownloader skinDownloader = new SkinDownloader(); - skinDownloader.fillJoinProfile(profile, event.getName(), event.getUniqueId()); - } catch (Exception ignored) { - }*/ } @EventHandler(priority = EventPriority.LOWEST) diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinData.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinData.java index 17ac803..e5de180 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinData.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinData.java @@ -1,5 +1,3 @@ package pw.kaboom.extras.modules.player.skin; -public record SkinData(String texture, String signature) { - -} \ No newline at end of file +public record SkinData(String texture, String signature) {} \ No newline at end of file diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index 4d1f7e3..b5a415e 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -104,11 +104,11 @@ public void run() { // always refetch final var skin = getSkinData(client, id); resultConsumer.accept(skin); - } catch (Exception ignored) { + } catch (final Exception ignored) { resultConsumer.accept(null); } } - } catch (InterruptedException ignored) { + } catch (final InterruptedException ignored) { } } @@ -180,7 +180,7 @@ private static SkinData getSkinData(final HttpClient client, final UUID uuid) { final List properties = response.properties(); - for (ProfileProperty property : properties) { + for (final ProfileProperty property : properties) { if (!property.getName().equals("textures")) continue; @@ -202,7 +202,7 @@ private static T sendRequestForJSON(final HttpClient client, final URI uri, try { response = client.send(request, BodyHandlers.ofString()); - } catch (Exception e) { + } catch (final Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/response/SkinResponse.java b/src/main/java/pw/kaboom/extras/modules/player/skin/response/SkinResponse.java index 81e3232..77a296e 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/response/SkinResponse.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/response/SkinResponse.java @@ -2,31 +2,5 @@ import com.destroystokyo.paper.profile.ProfileProperty; import java.util.List; -import java.util.Objects; -public final class SkinResponse { - - private final String id; - private final String name; - private final List properties; - - public SkinResponse(String id, String name, List properties) { - this.id = id; - this.name = name; - this.properties = properties; - } - - public String id() { - return id; - } - - public String name() { - return name; - } - - public List properties() { - return properties; - } - - -} +public record SkinResponse(String id, String name, List properties) {} From e360efd4db6274c06e5581f83b2ca39b593215f8 Mon Sep 17 00:00:00 2001 From: amy <144570677+amyavi@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:10:00 -0300 Subject: [PATCH 07/10] refactor: don't rely on Player instance when fetching --- .../modules/player/skin/SkinFillRequest.java | 8 ++--- .../modules/player/skin/SkinManager.java | 29 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java index 1d0f1e4..be08ceb 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java @@ -3,10 +3,10 @@ import org.bukkit.entity.Player; import org.jspecify.annotations.Nullable; -import java.lang.ref.WeakReference; -import java.util.function.Consumer; +import java.util.UUID; +import java.util.function.BiConsumer; -public record SkinFillRequest(WeakReference fromPlayer, - String toUser, Consumer<@Nullable SkinData> resultConsumer) { +public record SkinFillRequest(UUID fromPlayer, String toUser, + BiConsumer resultConsumer) { } diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index b5a415e..424793a 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; import java.lang.InterruptedException; -import java.lang.ref.WeakReference; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -79,8 +78,8 @@ public void run() { Thread.sleep(EXECUTION_INTERVAL - diff); } - final var ply = request.fromPlayer().get(); - if (ply == null || !ply.isConnected()) continue; + var ply = Bukkit.getPlayer(request.fromPlayer()); + if (ply == null) continue; final var toUser = request.toUser(); UUID id = null; @@ -103,9 +102,14 @@ public void run() { lastRequest = System.currentTimeMillis(); // always refetch final var skin = getSkinData(client, id); - resultConsumer.accept(skin); + + // re-get player to ensure it hasn't changed while we were fetching the skin + ply = Bukkit.getPlayer(request.fromPlayer()); + if (ply == null) continue; + + resultConsumer.accept(ply, skin); } catch (final Exception ignored) { - resultConsumer.accept(null); + resultConsumer.accept(ply, null); } } } catch (final InterruptedException ignored) { @@ -140,20 +144,13 @@ public static void requestSkin(final Player player, final String name, return; } - SKIN_REQUEST_QUEUE.removeIf(skinFillRequest -> { - final var requestingPlayer = skinFillRequest.fromPlayer().get(); - if (requestingPlayer == null) return false; - return requestingPlayer.getUniqueId().equals(player.getUniqueId()); - }); - - final var weakRef = new WeakReference<>(player); + final UUID uuid = player.getUniqueId(); + SKIN_REQUEST_QUEUE.removeIf(skinFillRequest -> skinFillRequest.fromPlayer().equals(uuid)); SKIN_REQUEST_QUEUE.add( new SkinFillRequest( - weakRef, + uuid, name, - skinData -> { - final var strongPlayer = weakRef.get(); - if (strongPlayer == null) return; + (strongPlayer, skinData) -> { if (skinData == null) { if (shouldSendMessage) strongPlayer.sendMessage(ERROR_MESSAGE); return; From d7d159a91a7b419e7124d88914b4028fd184fe2c Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 17:21:57 +0000 Subject: [PATCH 08/10] Fix typo --- .../java/pw/kaboom/extras/modules/player/skin/SkinManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index 424793a..0ba4007 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -131,7 +131,7 @@ private static void setSkin(final Player player, final @Nullable SkinData skinDa "textures", skinData.texture(), skinData.signature())); } else { - profile.removeProperty("texture"); + profile.removeProperty("textures"); } Bukkit.getScheduler().runTask(PLUGIN, () -> player.setPlayerProfile(profile)); From 958fbf0c95affbdecfa5fc1f60cabc19dc98643d Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 17:26:23 +0000 Subject: [PATCH 09/10] Purge requests from player if they leave --- src/main/java/pw/kaboom/extras/Main.java | 5 ++++ .../modules/player/skin/SkinManager.java | 23 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/Main.java b/src/main/java/pw/kaboom/extras/Main.java index 5ea63a9..aa0068e 100644 --- a/src/main/java/pw/kaboom/extras/Main.java +++ b/src/main/java/pw/kaboom/extras/Main.java @@ -13,6 +13,7 @@ import pw.kaboom.extras.modules.entity.EntityKnockback; import pw.kaboom.extras.modules.entity.EntitySpawn; import pw.kaboom.extras.modules.player.*; +import pw.kaboom.extras.modules.player.skin.SkinManager; import pw.kaboom.extras.modules.server.ServerCommand; import pw.kaboom.extras.modules.server.ServerGameRule; import pw.kaboom.extras.modules.server.ServerTabComplete; @@ -85,6 +86,10 @@ public void onEnable() { this.getServer().getPluginManager().registerEvents(new PlayerTeleport(), this); this.getServer().getPluginManager().registerEvents(new PlayerPrefix(), this); + final var skinManager = new SkinManager(); + this.getServer().getPluginManager().registerEvents(skinManager, this); + skinManager.start(); + /* Server-related modules */ ServerGameRule.init(this); diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index 0ba4007..43f598c 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -19,13 +19,16 @@ import net.kyori.adventure.text.Component; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; import org.jspecify.annotations.Nullable; import pw.kaboom.extras.modules.player.skin.response.ProfileResponse; import pw.kaboom.extras.modules.player.skin.response.SkinResponse; import static pw.kaboom.extras.Main.PLUGIN; -public final class SkinManager extends Thread { +public final class SkinManager extends Thread implements Listener { private static final Pattern PREMIUM_USERNAME = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); private static final Pattern UNDASHED_UUID = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})"); @@ -53,15 +56,21 @@ public final class SkinManager extends Thread { = new LinkedBlockingQueue<>(); private static final long EXECUTION_INTERVAL = 1000; - static { - new SkinManager().start(); - } - - private SkinManager() { + public SkinManager() { // avoid blocking jvm shutdown this.setDaemon(true); } + @EventHandler + void onPlayerQuit(PlayerQuitEvent event) { + purgePlayer(event.getPlayer().getUniqueId()); + } + + private static void purgePlayer(UUID uuid) { + SKIN_REQUEST_QUEUE.removeIf(skinFillRequest -> + skinFillRequest.fromPlayer().equals(uuid)); + } + @Override public void run() { final Map nameToIdCache = new HashMap<>(); @@ -145,7 +154,7 @@ public static void requestSkin(final Player player, final String name, } final UUID uuid = player.getUniqueId(); - SKIN_REQUEST_QUEUE.removeIf(skinFillRequest -> skinFillRequest.fromPlayer().equals(uuid)); + purgePlayer(uuid); SKIN_REQUEST_QUEUE.add( new SkinFillRequest( uuid, From 9ce99526e699178dcc16327e5155228a3836ff1c Mon Sep 17 00:00:00 2001 From: opt Date: Sun, 22 Mar 2026 17:56:41 +0000 Subject: [PATCH 10/10] Purge player in removeSkin, clean imports & remove unnecessary var --- src/main/java/pw/kaboom/extras/commands/CommandSkin.java | 8 ++------ .../pw/kaboom/extras/modules/player/skin/SkinManager.java | 3 ++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/pw/kaboom/extras/commands/CommandSkin.java b/src/main/java/pw/kaboom/extras/commands/CommandSkin.java index cbe06e7..f17e4ac 100644 --- a/src/main/java/pw/kaboom/extras/commands/CommandSkin.java +++ b/src/main/java/pw/kaboom/extras/commands/CommandSkin.java @@ -9,8 +9,6 @@ import pw.kaboom.extras.modules.player.skin.SkinManager; import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; public final class CommandSkin implements CommandExecutor { @Override @@ -35,7 +33,7 @@ public boolean onCommand(final @Nonnull CommandSender sender, if (name.equalsIgnoreCase("off") || name.equalsIgnoreCase("remove") || name.equalsIgnoreCase("disable")) { - SkinManager.resetSkin(player, true); + SkinManager.removeSkin(player, true); return true; } @@ -45,9 +43,7 @@ public boolean onCommand(final @Nonnull CommandSender sender, return true; } - final boolean shouldSendMessage = true; - - SkinManager.requestSkin(player, name, shouldSendMessage); + SkinManager.requestSkin(player, name, true); return true; } } diff --git a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java index 43f598c..67d29a9 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java +++ b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinManager.java @@ -126,7 +126,8 @@ public void run() { } } - public static void resetSkin(final Player player, final boolean shouldSendMessage) { + public static void removeSkin(final Player player, final boolean shouldSendMessage) { + purgePlayer(player.getUniqueId()); setSkin(player, null); if (shouldSendMessage)