diff --git a/jitpack.yml b/jitpack.yml index d9a09de62d..d40b777ed9 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,7 +1,7 @@ before_install: - - sdk install java 21.0.2-open - - sdk use java 21.0.2-open + - sdk install java 25-open + - sdk use java 25-open - sdk install maven jdk: - - openjdk21 + - openjdk25 diff --git a/pom.xml b/pom.xml index 65280d4459..3ea5e8391f 100644 --- a/pom.xml +++ b/pom.xml @@ -21,15 +21,15 @@ UTF-8 - - 16 - 16 - - 21 - 21 + + 25 + 25 + + 25 + 25 - 1.21.1 + 26.1.2 https://hub.spigotmc.org/javadocs/spigot/ @@ -221,6 +221,8 @@ META-INF/* + diff --git a/releases/Slimefun-v4.9-UNOFFICIAL-26.1.2.jar b/releases/Slimefun-v4.9-UNOFFICIAL-26.1.2.jar new file mode 100644 index 0000000000..0a02f81cea Binary files /dev/null and b/releases/Slimefun-v4.9-UNOFFICIAL-26.1.2.jar differ diff --git a/src/main/java/io/github/bakedlibs/dough/versions/MinecraftVersion.java b/src/main/java/io/github/bakedlibs/dough/versions/MinecraftVersion.java new file mode 100644 index 0000000000..48869297e1 --- /dev/null +++ b/src/main/java/io/github/bakedlibs/dough/versions/MinecraftVersion.java @@ -0,0 +1,104 @@ +package io.github.bakedlibs.dough.versions; + +import javax.annotation.Nonnull; + +import org.bukkit.Bukkit; +import org.bukkit.Server; + +/** + * Patched override of the shaded dough {@code MinecraftVersion}. + * + * The original class calls {@code SemanticVersion.parse()} on the first + * dash-delimited component of {@code Server.getBukkitVersion()}. For + * Minecraft 26.1.2 that component is {@code "26.1.2.build.12"}, which + * contains a non-numeric fourth segment and therefore throws + * {@link IllegalArgumentException} inside {@code SemanticVersion.parse()}, + * ultimately crashing the static initialiser of {@code ItemUtils}. + * + * This replacement normalises the string to {@code "major.minor.patch"} + * before parsing so {@code "26.1.2.build.12"} becomes {@code "26.1.2"}. + */ +public class MinecraftVersion extends SemanticVersion { + + public MinecraftVersion(int major, int minor, int patch) { + super(major, minor, patch); + } + + private MinecraftVersion(@Nonnull SemanticVersion version) { + this(version.getMajorVersion(), version.getMinorVersion(), version.getPatchVersion()); + } + + @Nonnull + public static MinecraftVersion of(@Nonnull Server server) throws UnknownServerVersionException { + if (server == null) { + throw new UnknownServerVersionException("Server should not be null!", null); + } + + String rawBukkitVersion = server.getBukkitVersion(); + + try { + // Take only the part before the first '-' (e.g. "26.1.2.build.12") + String withoutSuffix = rawBukkitVersion.split("-")[0]; + + // Split into dot-separated components and take only the leading numeric ones. + String[] parts = withoutSuffix.split("\\."); + int major = 1, minor = 0, patch = 0; + if (parts.length >= 1) { + major = Integer.parseInt(parts[0]); + } + // Legacy "1.x.y" format — skip leading "1" and treat parts[1] as major + if (major == 1 && parts.length >= 2) { + minor = Integer.parseInt(parts[1]); + if (parts.length >= 3) { + // parts[2] might be "1" in "1.21.1" or numeric prefix of "2something" + try { + patch = Integer.parseInt(parts[2]); + } catch (NumberFormatException ignored) { + patch = 0; + } + } + return new MinecraftVersion(major, minor, patch); + } + + // Modern "year.drop.hotfix[.extra…]" format — parts[0] is year/major + if (parts.length >= 2) { + minor = Integer.parseInt(parts[1]); + } + if (parts.length >= 3) { + try { + patch = Integer.parseInt(parts[2]); + } catch (NumberFormatException ignored) { + patch = 0; + } + } + return new MinecraftVersion(major, minor, patch); + } catch (Exception x) { + throw new UnknownServerVersionException("Could not recognize version string: " + rawBukkitVersion, x); + } + } + + @Nonnull + public static MinecraftVersion get() throws UnknownServerVersionException { + return of(Bukkit.getServer()); + } + + public static boolean isMocked(@Nonnull Server server) { + Class clazz = server.getClass(); + while (clazz != null) { + if (clazz.getName().endsWith("mockbukkit.ServerMock")) { + return true; + } + clazz = clazz.getSuperclass(); + } + return false; + } + + public static boolean isMocked() { + return isMocked(Bukkit.getServer()); + } + + @Override + public String getAsString() { + return "Minecraft " + super.getAsString(); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/MinecraftVersion.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/MinecraftVersion.java index afad06e3bc..825e9869ae 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/MinecraftVersion.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/MinecraftVersion.java @@ -61,6 +61,12 @@ public enum MinecraftVersion { */ MINECRAFT_1_21(21, 0, "1.21.x"), + /** + * Minecraft (Java Edition) 26.1 — first release under the new + * year.drop.hotfix versioning scheme. Matches 26.1.0 through 26.1.x. + */ + MINECRAFT_26_1(26, 1, "26.1.x"), + /** * This constant represents an exceptional state in which we were unable * to identify the Minecraft Version we are using diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItemStack.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItemStack.java index 8a427b249f..d4d78496d1 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItemStack.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItemStack.java @@ -38,12 +38,11 @@ import io.github.bakedlibs.dough.common.CommonPatterns; import io.github.bakedlibs.dough.items.ItemMetaSnapshot; -import io.github.bakedlibs.dough.skins.PlayerHead; -import io.github.bakedlibs.dough.skins.PlayerSkin; import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.api.exceptions.PrematureCodeException; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.utils.HeadTexture; +import io.github.thebusybiscuit.slimefun4.utils.PlayerSkinUtils; import io.github.thebusybiscuit.slimefun4.utils.compatibility.VersionedItemFlag; /** @@ -284,8 +283,7 @@ public void setAmount(int amount) { return new ItemStack(Material.PLAYER_HEAD); } - PlayerSkin skin = PlayerSkin.fromBase64(getTexture(id, texture)); - return PlayerHead.getItemStack(skin); + return PlayerSkinUtils.getItemStackFromBase64(getTexture(id, texture)); } private static @Nonnull String getTexture(@Nonnull String id, @Nonnull String texture) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java index 34ff1f4a20..2470a4484d 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/VersionsCommand.java @@ -36,13 +36,13 @@ class VersionsCommand extends SubCommand { * This is the Java version we recommend to use. * Bump as necessary and adjust the warning. */ - private static final int RECOMMENDED_JAVA_VERSION = 16; + private static final int RECOMMENDED_JAVA_VERSION = 25; /** * This is the notice that will be displayed when an * older version of Java is detected. */ - private static final String JAVA_VERSION_NOTICE = "As of Minecraft 1.17 Java 16 will be required!"; + private static final String JAVA_VERSION_NOTICE = "As of Minecraft 26.1 Java 25 is required!"; @ParametersAreNonnullByDefault VersionsCommand(Slimefun plugin, SlimefunCommand cmd) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java index 8b065e3ff4..43f4324d2a 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java @@ -1,23 +1,26 @@ package io.github.thebusybiscuit.slimefun4.core.services.github; +import java.io.BufferedReader; import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bukkit.Bukkit; -import io.github.bakedlibs.dough.skins.PlayerSkin; -import io.github.bakedlibs.dough.skins.UUIDLookup; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; /** @@ -33,6 +36,8 @@ class GitHubTask implements Runnable { private static final int MAX_REQUESTS_PER_MINUTE = 16; + private static final Pattern UUID_PATTERN = Pattern.compile("\"id\"\\s*:\\s*\"([0-9a-fA-F]{32})\""); + private static final Pattern TEXTURE_VALUE_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"textures\"\\s*,\\s*\"value\"\\s*:\\s*\"([^\"]+)\""); private final GitHubService gitHubService; GitHubTask(@Nonnull GitHubService github) { @@ -107,9 +112,6 @@ private int requestTexture(@Nonnull Contributor contributor, @Nonnull Map skins) throws InterruptedException, ExecutionException, TimeoutException { + private @Nullable String pullTexture(@Nonnull Contributor contributor, @Nonnull Map skins) throws IOException { Optional uuid = contributor.getUniqueId(); if (!uuid.isPresent()) { - CompletableFuture future = UUIDLookup.getUuidFromUsername(Slimefun.instance(), contributor.getMinecraftName()); - - // Fixes #3241 - Do not wait for more than 30 seconds - uuid = Optional.ofNullable(future.get(30, TimeUnit.SECONDS)); - uuid.ifPresent(contributor::setUniqueId); + UUID resolved = lookupUuid(contributor.getMinecraftName()); + if (resolved != null) { + contributor.setUniqueId(resolved); + uuid = Optional.of(resolved); + } } if (uuid.isPresent()) { - CompletableFuture future = PlayerSkin.fromPlayerUUID(Slimefun.instance(), uuid.get()); - Optional skin = Optional.of(future.get().getProfile().getBase64Texture()); - skins.put(contributor.getMinecraftName(), skin.orElse("")); - return skin.orElse(null); + String texture = lookupTexture(uuid.get()); + skins.put(contributor.getMinecraftName(), texture == null ? "" : texture); + return texture; } else { return null; } } + private @Nullable UUID lookupUuid(@Nonnull String username) throws IOException { + String body = fetch("https://api.mojang.com/users/profiles/minecraft/" + username); + if (body == null) { + return null; + } + Matcher m = UUID_PATTERN.matcher(body); + if (!m.find()) { + return null; + } + String raw = m.group(1); + String formatted = raw.substring(0, 8) + '-' + raw.substring(8, 12) + '-' + + raw.substring(12, 16) + '-' + raw.substring(16, 20) + '-' + + raw.substring(20); + return UUID.fromString(formatted); + } + + private @Nullable String lookupTexture(@Nonnull UUID uuid) throws IOException { + String body = fetch("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid); + if (body == null) { + return null; + } + Matcher m = TEXTURE_VALUE_PATTERN.matcher(body); + return m.find() ? m.group(1) : null; + } + + private @Nullable String fetch(@Nonnull String endpoint) throws IOException { + URL url = URI.create(endpoint).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10_000); + conn.setReadTimeout(10_000); + conn.setRequestProperty("User-Agent", "Slimefun4"); + int status = conn.getResponseCode(); + if (status == 429) { + throw new IOException("429 Too Many Requests"); + } + if (status != 200) { + return null; + } + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } + return sb.toString(); + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/localization/SlimefunLocalization.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/localization/SlimefunLocalization.java index a146a9f620..cf19988fe9 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/localization/SlimefunLocalization.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/localization/SlimefunLocalization.java @@ -32,9 +32,7 @@ import io.github.thebusybiscuit.slimefun4.core.services.LocalizationService; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; -import net.md_5.bungee.api.ChatMessageType; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.TextComponent; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; /** * This is an abstract parent class of {@link LocalizationService}. @@ -362,8 +360,7 @@ public void sendActionbarMessage(@Nonnull Player player, @Nonnull String key, bo String prefix = addPrefix ? getChatPrefix() : ""; String message = ChatColors.color(prefix + getMessage(player, key)); - BaseComponent[] components = TextComponent.fromLegacyText(message); - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, components); + player.sendActionBar(LegacyComponentSerializer.legacySection().deserialize(message)); } public void sendMessage(@Nonnull CommandSender recipient, @Nonnull String key) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java index 1be851ba17..e265094e0e 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java @@ -147,7 +147,7 @@ public class Slimefun extends JavaPlugin implements SlimefunAddon { * This does not necessarily mean that it's the minimum version * required to run Slimefun. */ - private static final int RECOMMENDED_JAVA_VERSION = 17; + private static final int RECOMMENDED_JAVA_VERSION = 25; /** * Our static instance of {@link Slimefun}. @@ -523,9 +523,14 @@ private boolean isVersionUnsupported() { return true; } - // Now check the actual Version of Minecraft - int version = PaperLib.getMinecraftVersion(); - int patchVersion = PaperLib.getMinecraftPatchVersion(); + // Now check the actual Version of Minecraft. + // PaperLib 1.0.8 assumes the legacy "1.x.y" scheme and returns the + // drop number (e.g. 1 for "26.1.2"), so we parse the Bukkit version + // ourselves to also support the year.drop.hotfix scheme introduced + // with Minecraft 26. + int[] parsed = parseBukkitVersion(Bukkit.getBukkitVersion()); + int version = parsed[0]; + int patchVersion = parsed[1]; if (version > 0) { // Check all supported versions of Minecraft @@ -537,7 +542,10 @@ private boolean isVersionUnsupported() { } // Looks like you are using an unsupported Minecraft Version - StartupWarnings.invalidMinecraftVersion(getLogger(), version, getDescription().getVersion()); + String versionDisplay = version >= 26 + ? version + "." + patchVersion + ".x" + : "1." + version + ".x"; + StartupWarnings.invalidMinecraftVersion(getLogger(), versionDisplay, getDescription().getVersion()); return true; } else { getLogger().log(Level.WARNING, "We could not determine the version of Minecraft you were using? ({0})", Bukkit.getVersion()); @@ -558,6 +566,41 @@ private boolean isVersionUnsupported() { } } + /** + * Parses the value returned by {@link Bukkit#getBukkitVersion()} into a + * {@code {major, patch}} pair that maps onto {@link MinecraftVersion}'s + * numeric fields. + *

+ * Handles both the legacy {@code "1.."} format and the new + * {@code ".."} format (Minecraft 26+). Returns + * {@code {-1, -1}} when the string cannot be parsed. + */ + private static int[] parseBukkitVersion(@Nonnull String bukkitVersion) { + try { + String core = bukkitVersion.split("-", 2)[0]; + String[] parts = core.split("\\."); + + if (parts.length < 2) { + return new int[] { -1, -1 }; + } + + int major; + int patch; + if ("1".equals(parts[0])) { + // Legacy scheme: 1.. + major = Integer.parseInt(parts[1]); + patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + } else { + // Year-based scheme: .. + major = Integer.parseInt(parts[0]); + patch = Integer.parseInt(parts[1]); + } + return new int[] { major, patch }; + } catch (NumberFormatException ex) { + return new int[] { -1, -1 }; + } + } + /** * This private method gives us a {@link Collection} of every {@link MinecraftVersion} * that Slimefun is compatible with (as a {@link String} representation). diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/StartupWarnings.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/StartupWarnings.java index 77b3a7479a..2925d56264 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/StartupWarnings.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/StartupWarnings.java @@ -34,12 +34,12 @@ static void discourageCSCoreLib(Logger logger) { } @ParametersAreNonnullByDefault - static void invalidMinecraftVersion(Logger logger, int majorVersion, String slimefunVersion) { + static void invalidMinecraftVersion(Logger logger, String versionDisplay, String slimefunVersion) { logger.log(Level.SEVERE, BORDER); logger.log(Level.SEVERE, PREFIX + "Slimefun was not installed correctly!"); logger.log(Level.SEVERE, PREFIX + "You are using the wrong version of Minecraft!"); logger.log(Level.SEVERE, PREFIX); - logger.log(Level.SEVERE, PREFIX + "You are using Minecraft 1.{0}.x", majorVersion); + logger.log(Level.SEVERE, PREFIX + "You are using Minecraft {0}", versionDisplay); logger.log(Level.SEVERE, PREFIX + "but Slimefun {0} requires you to be using", slimefunVersion); logger.log(Level.SEVERE, PREFIX + "Minecraft {0}", String.join(" / ", Slimefun.getSupportedVersions())); logger.log(Level.SEVERE, BORDER); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java index 669327a9bf..ae14200f34 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java @@ -34,8 +34,6 @@ import io.github.bakedlibs.dough.common.CommonPatterns; import io.github.bakedlibs.dough.items.CustomItemStack; import io.github.bakedlibs.dough.items.ItemUtils; -import io.github.bakedlibs.dough.skins.PlayerHead; -import io.github.bakedlibs.dough.skins.PlayerSkin; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; @@ -49,6 +47,7 @@ import io.github.thebusybiscuit.slimefun4.utils.ChestMenuUtils; import io.github.thebusybiscuit.slimefun4.utils.HeadTexture; import io.github.thebusybiscuit.slimefun4.utils.NumberUtils; +import io.github.thebusybiscuit.slimefun4.utils.PlayerSkinUtils; import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils; import io.papermc.lib.PaperLib; @@ -880,11 +879,10 @@ protected void move(Block b, BlockFace face, Block block) { block.setBlockData(blockData); Slimefun.runSync(() -> { - PlayerSkin skin = PlayerSkin.fromBase64(texture); Material type = block.getType(); // Ensure that this Block is still a Player Head if (type == Material.PLAYER_HEAD || type == Material.PLAYER_WALL_HEAD) { - PlayerHead.setSkin(block, skin, true); + PlayerSkinUtils.setBlockSkinFromBase64(block, texture, true); } }); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/DebugFishListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/DebugFishListener.java index 6bcbffd93b..9a0577e7f9 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/DebugFishListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/DebugFishListener.java @@ -21,7 +21,6 @@ import org.bukkit.inventory.EquipmentSlot; import io.github.bakedlibs.dough.common.ChatColors; -import io.github.bakedlibs.dough.skins.PlayerHead; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.core.attributes.EnergyNetComponent; import io.github.thebusybiscuit.slimefun4.core.attributes.EnergyNetProvider; @@ -29,6 +28,7 @@ import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.SlimefunItems; import io.github.thebusybiscuit.slimefun4.utils.HeadTexture; +import io.github.thebusybiscuit.slimefun4.utils.PlayerSkinUtils; import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils; import io.github.thebusybiscuit.slimefun4.utils.tags.SlimefunTag; @@ -95,7 +95,7 @@ private void onRightClick(Player p, Block b, BlockFace face) { Block block = b.getRelative(face); block.setType(Material.PLAYER_HEAD); - PlayerHead.setSkin(block, HeadTexture.MISSING_TEXTURE.getAsSkin(), true); + PlayerSkinUtils.setBlockSkinFromHash(block, HeadTexture.MISSING_TEXTURE.getUniqueId(), HeadTexture.MISSING_TEXTURE.getTexture(), true); SoundEffect.DEBUG_FISH_CLICK_SOUND.playFor(p); }, 2L); } else if (BlockStorage.hasBlockInfo(b)) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/CapacitorTextureUpdateTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/CapacitorTextureUpdateTask.java index 4f79580831..ac720a9ce5 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/CapacitorTextureUpdateTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/CapacitorTextureUpdateTask.java @@ -8,11 +8,9 @@ import org.bukkit.Server; import org.bukkit.block.Block; -import io.github.bakedlibs.dough.skins.PlayerHead; -import io.github.bakedlibs.dough.skins.PlayerSkin; import io.github.thebusybiscuit.slimefun4.implementation.items.electric.Capacitor; import io.github.thebusybiscuit.slimefun4.utils.HeadTexture; -import io.papermc.lib.PaperLib; +import io.github.thebusybiscuit.slimefun4.utils.PlayerSkinUtils; /** * This task is run whenever a {@link Capacitor} needs to update their texture. @@ -75,10 +73,7 @@ public void run() { } private void setTexture(@Nonnull Block b, @Nonnull HeadTexture texture) { - PlayerSkin skin = PlayerSkin.fromHashCode(texture.getUniqueId(), texture.getTexture()); - PlayerHead.setSkin(b, skin, false); - - PaperLib.getBlockState(b, false).getState().update(true, false); + PlayerSkinUtils.setBlockSkinFromHash(b, texture.getUniqueId(), texture.getTexture(), false); } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java index 85e4c4bfe8..a709d5d397 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java @@ -10,9 +10,7 @@ import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.listeners.RadioactivityListener; import io.github.thebusybiscuit.slimefun4.utils.RadiationUtils; -import net.md_5.bungee.api.ChatMessageType; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.ComponentBuilder; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.GameMode; @@ -92,9 +90,7 @@ protected void onPlayerTick(Player p, PlayerProfile profile) { String msg = Slimefun.getLocalization() .getMessage(p, "actionbar.radiation") .replace("%level%", "" + exposureLevelAfter); - BaseComponent[] components = - new ComponentBuilder().append(ChatColors.color(msg)).create(); - p.spigot().sendMessage(ChatMessageType.ACTION_BAR, components); + p.sendActionBar(LegacyComponentSerializer.legacySection().deserialize(ChatColors.color(msg))); } } else { RadiationUtils.removeExposure(p, 1); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ArmorStandUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ArmorStandUtils.java index e2cf5ae10f..e7b661dfa6 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ArmorStandUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ArmorStandUtils.java @@ -2,7 +2,6 @@ import javax.annotation.Nonnull; -import io.papermc.lib.PaperLib; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; @@ -50,16 +49,6 @@ private ArmorStandUtils() {} * @return The spawned {@link ArmorStand} */ public static @Nonnull ArmorStand spawnArmorStand(@Nonnull Location location) { - // The consumer method was moved from World to RegionAccessor in 1.20.2 - // Due to this, we need to use a rubbish workaround to support 1.20.1 and below - // This causes flicker on these versions which sucks but not sure a better way around this right now. - if (PaperLib.getMinecraftVersion() < 20 || - (PaperLib.getMinecraftVersion() == 20 && PaperLib.getMinecraftPatchVersion() < 2)) { - ArmorStand armorStand = location.getWorld().spawn(location, ArmorStand.class); - setupArmorStand(armorStand); - return armorStand; - } - return location.getWorld().spawn(location, ArmorStand.class, ArmorStandUtils::setupArmorStand); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/HeadTexture.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/HeadTexture.java index a4caf14774..ac8fd59377 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/HeadTexture.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/HeadTexture.java @@ -9,7 +9,6 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.common.CommonPatterns; -import io.github.bakedlibs.dough.skins.PlayerSkin; /** * This enum holds all currently used Head textures in Slimefun. @@ -163,8 +162,4 @@ public enum HeadTexture { return SlimefunUtils.getCustomHead(getTexture()); } - public @Nonnull PlayerSkin getAsSkin() { - return PlayerSkin.fromHashCode(texture); - } - } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PlayerSkinUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PlayerSkinUtils.java new file mode 100644 index 0000000000..e6ab1c7b2a --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PlayerSkinUtils.java @@ -0,0 +1,97 @@ +package io.github.thebusybiscuit.slimefun4.utils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.Skull; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; + +/** + * Drop-in replacement for the shaded dough {@code PlayerSkin} / {@code PlayerHead} + * helpers. The dough library's {@code CustomGameProfile} extends Mojang's + * {@code GameProfile}, which was made {@code final} in Minecraft 26.1.2, + * so any call into dough.skins crashes with {@link IncompatibleClassChangeError}. + * This utility talks to Paper's native {@link PlayerProfile} API instead. + */ +public final class PlayerSkinUtils { + + private static final Pattern URL_PATTERN = Pattern.compile("\"url\"\\s*:\\s*\"([^\"]+)\""); + private static final UUID ZERO_UUID = new UUID(0L, 0L); + private static final String TEXTURE_URL_PREFIX = "http://textures.minecraft.net/texture/"; + + private PlayerSkinUtils() {} + + public static @Nonnull String hashToBase64(@Nonnull String hash) { + String json = "{\"textures\":{\"SKIN\":{\"url\":\"" + TEXTURE_URL_PREFIX + hash + "\"}}}"; + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } + + private static @Nullable URL extractUrl(@Nonnull String base64) { + try { + String json = new String(Base64.getDecoder().decode(base64), StandardCharsets.UTF_8); + Matcher m = URL_PATTERN.matcher(json); + if (!m.find()) { + return null; + } + return new URI(m.group(1)).toURL(); + } catch (IllegalArgumentException | URISyntaxException | java.net.MalformedURLException e) { + return null; + } + } + + private static @Nonnull PlayerProfile buildProfile(@Nonnull UUID uuid, @Nonnull String base64) { + PlayerProfile profile = Bukkit.createPlayerProfile(uuid); + URL url = extractUrl(base64); + if (url != null) { + PlayerTextures textures = profile.getTextures(); + textures.setSkin(url); + profile.setTextures(textures); + } + return profile; + } + + public static @Nonnull ItemStack getItemStackFromBase64(@Nonnull String base64) { + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) head.getItemMeta(); + if (meta != null) { + meta.setOwnerProfile(buildProfile(ZERO_UUID, base64)); + head.setItemMeta(meta); + } + return head; + } + + public static @Nonnull ItemStack getItemStackFromHash(@Nonnull String hash) { + return getItemStackFromBase64(hashToBase64(hash)); + } + + public static void setBlockSkinFromBase64(@Nonnull Block block, @Nonnull String base64, boolean sendUpdate) { + if (!(block.getState() instanceof Skull skull)) { + return; + } + skull.setOwnerProfile(buildProfile(ZERO_UUID, base64)); + skull.update(true, sendUpdate); + } + + public static void setBlockSkinFromHash(@Nonnull Block block, @Nonnull UUID uuid, @Nonnull String hash, boolean sendUpdate) { + if (!(block.getState() instanceof Skull skull)) { + return; + } + skull.setOwnerProfile(buildProfile(uuid, hashToBase64(hash))); + skull.update(true, sendUpdate); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java index ed5bdb9b4d..fbbb970cdb 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java @@ -1,8 +1,6 @@ package io.github.thebusybiscuit.slimefun4.utils; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.OptionalInt; @@ -29,8 +27,6 @@ import io.github.bakedlibs.dough.common.CommonPatterns; import io.github.bakedlibs.dough.items.ItemMetaSnapshot; -import io.github.bakedlibs.dough.skins.PlayerHead; -import io.github.bakedlibs.dough.skins.PlayerSkin; import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.api.events.SlimefunItemSpawnEvent; import io.github.thebusybiscuit.slimefun4.api.exceptions.PrematureCodeException; @@ -238,11 +234,10 @@ public static boolean isRadioactive(@Nullable ItemStack item) { String base64 = texture; if (CommonPatterns.HEXADECIMAL.matcher(texture).matches()) { - base64 = Base64.getEncoder().encodeToString(("{\"textures\":{\"SKIN\":{\"url\":\"http://textures.minecraft.net/texture/" + texture + "\"}}}").getBytes(StandardCharsets.UTF_8)); + base64 = PlayerSkinUtils.hashToBase64(texture); } - PlayerSkin skin = PlayerSkin.fromBase64(base64); - return PlayerHead.getItemStack(skin); + return PlayerSkinUtils.getItemStackFromBase64(base64); } public static boolean containsSimilarItem(Inventory inventory, ItemStack item, boolean checkLore) { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5e5a3adbe5..ed54458981 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -9,7 +9,7 @@ description: Slimefun basically turns your entire Server into a FTB modpack with # Technical settings main: io.github.thebusybiscuit.slimefun4.implementation.Slimefun -api-version: '1.16' +api-version: '1.21' # (Soft) dependencies of Slimefun, we hook into these plugins. softdepend: diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestMinecraftVersion.java b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestMinecraftVersion.java index 74a2622cce..dde7dcd497 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestMinecraftVersion.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestMinecraftVersion.java @@ -100,4 +100,22 @@ void testLowestSupportedVersion() { Assertions.assertThrows(IllegalArgumentException.class, () -> MinecraftVersion.UNIT_TEST.isAtLeast(MinecraftVersion.MINECRAFT_1_16)); } + @Test + @DisplayName("Test MINECRAFT_26_1 matches 26.1.x and is ordered after 1.21") + void testMinecraft26() { + // 26.1 should match major=26, patch=1 + Assertions.assertTrue(MinecraftVersion.MINECRAFT_26_1.isMinecraftVersion(26, 1)); + // 26.1.2 hotfix also falls in the 26.1.x range + Assertions.assertTrue(MinecraftVersion.MINECRAFT_26_1.isMinecraftVersion(26, 2)); + // 26.0.x is before the 26.1 drop — must not match + Assertions.assertFalse(MinecraftVersion.MINECRAFT_26_1.isMinecraftVersion(26, 0)); + // different major must not match + Assertions.assertFalse(MinecraftVersion.MINECRAFT_26_1.isMinecraftVersion(21, 0)); + + // ordinal ordering: 26.1 is newer than 1.21 + Assertions.assertTrue(MinecraftVersion.MINECRAFT_26_1.isAtLeast(MinecraftVersion.MINECRAFT_1_21)); + Assertions.assertFalse(MinecraftVersion.MINECRAFT_26_1.isBefore(MinecraftVersion.MINECRAFT_1_21)); + Assertions.assertTrue(MinecraftVersion.MINECRAFT_1_21.isBefore(MinecraftVersion.MINECRAFT_26_1)); + } + }