diff --git a/src/main/java/pw/kaboom/extras/Main.java b/src/main/java/pw/kaboom/extras/Main.java index 3e23caa9..aa0068ed 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; @@ -20,9 +21,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 */ @@ -79,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/commands/CommandSkin.java b/src/main/java/pw/kaboom/extras/commands/CommandSkin.java index 2afe1598..f17e4ac7 100644 --- a/src/main/java/pw/kaboom/extras/commands/CommandSkin.java +++ b/src/main/java/pw/kaboom/extras/commands/CommandSkin.java @@ -9,12 +9,8 @@ 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 { - private final Map lastUsedMillis = new HashMap<>(); - @Override public boolean onCommand(final @Nonnull CommandSender sender, final @Nonnull Command command, @@ -26,9 +22,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,31 +29,21 @@ 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") || name.equalsIgnoreCase("disable")) { - SkinManager.resetSkin(player, true); + SkinManager.removeSkin(player, true); return true; } 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, true); return true; } } 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 2b239c39..42fa4d9f 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 b2be1747..2b9fb980 100644 --- a/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java +++ b/src/main/java/pw/kaboom/extras/modules/player/PlayerConnection.java @@ -8,15 +8,14 @@ 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; 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; -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; @@ -28,7 +27,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( @@ -83,19 +82,18 @@ 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)); + @EventHandler(priority = EventPriority.LOWEST) + void onEarlyPlayerJoin(final PlayerJoinEvent event) { + final var player = event.getPlayer(); - profile.setId(offlineUUID); + // Must be done in the PlayerJoinEvent instead of PlayerLoginEvent otherwise skins may fetch + // after the player joins + final var serverConfig = Bukkit.getServer().getServerConfig(); - SkinDownloader skinDownloader = new SkinDownloader(); - skinDownloader.fillJoinProfile(profile, event.getName(), event.getUniqueId()); - } catch (Exception ignored) { - }*/ + if (!serverConfig.isProxyOnlineMode()) + SkinManager.requestSkin(player, player.getName(), false); } @EventHandler @@ -144,13 +142,6 @@ void onPlayerLogin(final PlayerLoginEvent event) { if (OP_ON_JOIN && !player.isOp()) { player.setOp(true); } - - final Server server = Bukkit.getServer(); - - - if (!server.getOnlineMode()) { - SkinManager.applySkin(player, player.getName(), false); - } } @EventHandler 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 efae707f..27217176 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/player/skin/SkinData.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinData.java index 17ac8036..e5de1802 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/SkinFillRequest.java b/src/main/java/pw/kaboom/extras/modules/player/skin/SkinFillRequest.java new file mode 100644 index 00000000..be08ceb6 --- /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.util.UUID; +import java.util.function.BiConsumer; + +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 02fec19e..67d29a91 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 @@ -7,36 +7,32 @@ 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.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; -public final class SkinManager { - private static final HttpClient httpClient = HttpClient.newHttpClient(); +import static pw.kaboom.extras.Main.PLUGIN; + +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})"); 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 +51,155 @@ 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 SkinManager() { + // avoid blocking jvm shutdown + this.setDaemon(true); + } - 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); + @EventHandler + void onPlayerQuit(PlayerQuitEvent event) { + purgePlayer(event.getPlayer().getUniqueId()); + } - bukkitScheduler.runTask(plugin, () -> player.setPlayerProfile(playerProfile)); + private static void purgePlayer(UUID uuid) { + SKIN_REQUEST_QUEUE.removeIf(skinFillRequest -> + skinFillRequest.fromPlayer().equals(uuid)); + } - if(!shouldSendMessage) { - return; - } + @Override + public void run() { + final Map nameToIdCache = new HashMap<>(); + long lastRequest = 0; - player.sendMessage(Component.text("Successfully removed your skin")); - }); - } + try(final var client = HttpClient.newHttpClient()) { + for(;;) { + while (SKIN_REQUEST_QUEUE.size() > 32) SKIN_REQUEST_QUEUE.remove(); + final SkinFillRequest request = SKIN_REQUEST_QUEUE.take(); - 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; + final long diff = System.currentTimeMillis() - lastRequest; + if (diff < EXECUTION_INTERVAL) { + //noinspection BusyWait + Thread.sleep(EXECUTION_INTERVAL - diff); } - player.sendMessage(Component.text("Skin fetching was interrupted")); - return; - } catch (TimeoutException e) { - if (!shouldSendMessage) { - return; - } + var ply = Bukkit.getPlayer(request.fromPlayer()); + if (ply == null) continue; - player.sendMessage(Component.text("Took too long to fetch skin")); - return; - } catch (ExecutionException | CompletionException e) { - if(!shouldSendMessage) { - return; - } + final var toUser = request.toUser(); + UUID id = null; - player.sendMessage(Component.text("A player with that username doesn't exist")); - return; - } + if (toUser.equalsIgnoreCase(ply.getName()) + && Bukkit.getServerConfig().isProxyOnlineMode()) + id = ply.getUniqueId(); + + id = nameToIdCache.getOrDefault(toUser, id); - final String texture = skinData.texture(); - final String signature = skinData.signature(); - profile.setProperty(new ProfileProperty("textures", texture, signature)); + final var resultConsumer = request.resultConsumer(); - final BukkitScheduler bukkitScheduler = Bukkit.getScheduler(); - final Main plugin = JavaPlugin.getPlugin(Main.class); + try { + if (id == null) { + lastRequest = System.currentTimeMillis(); + id = getUUID(client, toUser); + nameToIdCache.put(toUser, id); + } - bukkitScheduler.runTask(plugin, - () -> player.setPlayerProfile(profile)); + lastRequest = System.currentTimeMillis(); + // always refetch + final var skin = getSkinData(client, id); + // re-get player to ensure it hasn't changed while we were fetching the skin + ply = Bukkit.getPlayer(request.fromPlayer()); + if (ply == null) continue; - if(!shouldSendMessage) { - return; + resultConsumer.accept(ply, skin); + } catch (final Exception ignored) { + resultConsumer.accept(ply, null); + } } + } catch (final 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 removeSkin(final Player player, final boolean shouldSendMessage) { + purgePlayer(player.getUniqueId()); + 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("textures"); + } - 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()); - } + final UUID uuid = player.getUniqueId(); + purgePlayer(uuid); + SKIN_REQUEST_QUEUE.add( + new SkinFillRequest( + uuid, + name, + (strongPlayer, skinData) -> { + if (skinData == null) { + if (shouldSendMessage) strongPlayer.sendMessage(ERROR_MESSAGE); + return; + } + + setSkin(strongPlayer, skinData); + if (shouldSendMessage) + strongPlayer + .sendMessage(Component.text("Successfully set your skin to ") + .append(Component.text(name)) + .append(Component.text("'s"))); + } + ) + ); + } - throw new RuntimeException("No textures property"); - }, executorService); + 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 (final 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(URI uri, String endpoint, Class clazz) { + 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,27 +208,26 @@ private static T sendRequestForJSON(URI uri, String endpoint, Class clazz final HttpResponse response; try { - response = httpClient.send(request, BodyHandlers.ofString()); - } catch (Exception e) { + response = client.send(request, BodyHandlers.ofString()); + } catch (final Exception e) { throw new RuntimeException(e); } 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); } } 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 81e32326..77a296e3 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) {} 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 a2dac790..20b9134c 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",