From 1dcadc35b8f38ea0cfae49018bfe15b33097d3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=ADan=20Errity?= Date: Sat, 24 Oct 2026 19:12:27 +0000 Subject: [PATCH] Bundle private Adventure NBT for legacy disc reads --- build.gradle.kts | 1 + plugin/build.gradle.kts | 10 +++- .../utils/nbt/PackagedDiscDataTest.java | 56 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 plugin/src/test/java/com/minecraftcitiesnetwork/musicbox/utils/nbt/PackagedDiscDataTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 46be7d3..515f9cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,7 @@ tasks.shadowJar { archiveClassifier.set("") relocate("org.incendo", "com.minecraftcitiesnetwork.musicbox.internal.cloud") relocate("org.bstats", "com.minecraftcitiesnetwork.musicbox.internal.bstats") + relocate("net.kyori.adventure.nbt", "com.minecraftcitiesnetwork.musicbox.internal.nbt") mergeServiceFiles() } tasks.jar { archiveClassifier.set("thin") } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 9a137bb..95dd74a 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { compileOnly("org.projectlombok:lombok:1.18.42") annotationProcessor("org.projectlombok:lombok:1.18.42") implementation("org.incendo:cloud-paper:2.0.1") - compileOnly("net.kyori:adventure-nbt:4.26.1") + implementation("net.kyori:adventure-nbt:4.26.1") testImplementation("net.kyori:adventure-nbt:4.26.1") testImplementation("com.github.MilkBowl:VaultAPI:1.7") { isTransitive = false } implementation("org.bstats:bstats-bukkit:3.0.2") @@ -42,3 +42,11 @@ tasks.register("paperRuntimeTest") { doFirst { probeDirectory.mkdirs() } maxHeapSize = "2g" } + +// Exercise the published artifact, not the test classpath's unshaded NBT dependency. +val packagedPlugin = rootProject.tasks.named("shadowJar") +tasks.test { + dependsOn(packagedPlugin) + inputs.file(packagedPlugin.flatMap { it.archiveFile }) + doFirst { systemProperty("musicbox.jar", packagedPlugin.get().archiveFile.get().asFile.absolutePath) } +} diff --git a/plugin/src/test/java/com/minecraftcitiesnetwork/musicbox/utils/nbt/PackagedDiscDataTest.java b/plugin/src/test/java/com/minecraftcitiesnetwork/musicbox/utils/nbt/PackagedDiscDataTest.java new file mode 100644 index 0000000..9ffa261 --- /dev/null +++ b/plugin/src/test/java/com/minecraftcitiesnetwork/musicbox/utils/nbt/PackagedDiscDataTest.java @@ -0,0 +1,56 @@ +package com.minecraftcitiesnetwork.musicbox.utils.nbt; + +import java.net.*; +import java.nio.file.*; +import java.io.*; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.*; +import org.bukkit.persistence.*; +import org.bukkit.NamespacedKey; +import net.kyori.adventure.nbt.*; +import static org.mockito.Mockito.*; + +/** Runs the real JAR handler with no external Adventure NBT visibility. */ +class PackagedDiscDataTest { + static ItemStack item(CompoundBinaryTag root) throws Exception { + var bytes = new ByteArrayOutputStream(); + BinaryTagIO.writer().write(root, bytes, BinaryTagIO.Compression.GZIP); + var item = mock(ItemStack.class); var meta = mock(ItemMeta.class); + when(item.hasItemMeta()).thenReturn(true); when(item.getItemMeta()).thenReturn(meta); + when(meta.getPersistentDataContainer()).thenReturn(mock(PersistentDataContainer.class)); + when(item.serializeAsBytes()).thenReturn(bytes.toByteArray()); + return item; + } + @org.junit.jupiter.api.Test + void packagedHandlerReadsDiscsWithoutServerNbtClasses() throws Exception { + try (var loader = new URLClassLoader(new URL[]{Path.of(System.getProperty("musicbox.jar")).toUri().toURL()}, PackagedDiscDataTest.class.getClassLoader()) { + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("net.kyori.adventure.nbt.")) throw new ClassNotFoundException("External NBT intentionally unavailable: " + name); + if (name.startsWith("com.minecraftcitiesnetwork.musicbox.")) { + Class c = findLoadedClass(name); if(c == null) c = findClass(name); if(resolve) resolveClass(c); return c; + } + return super.loadClass(name,resolve); + } + }) { + var type = loader.loadClass("com.minecraftcitiesnetwork.musicbox.utils.nbt.BukkitNbtHandler"); + var handler = type.getConstructor().newInstance(); + var get = type.getMethod("getNbt",ItemStack.class,String.class); + var pdc = item(CompoundBinaryTag.empty()); + when(pdc.getItemMeta().getPersistentDataContainer().get(NamespacedKey.fromString("musicbox:song_hash"),PersistentDataType.INTEGER)).thenReturn(-197); + check(-197,get.invoke(handler,pdc,"song_hash")); verify(pdc,never()).serializeAsBytes(); + var tag = CompoundBinaryTag.builder().putInt("song_hash",-123456).build(); + var modern = item(CompoundBinaryTag.builder().put("components",CompoundBinaryTag.builder().put("minecraft:custom_data",tag).build()).build()); + var legacy = item(CompoundBinaryTag.builder().put("tag",tag).build()); + check(-123456,get.invoke(handler,modern,"song_hash")); check(-123456,get.invoke(handler,legacy,"song_hash")); + verify(modern,never()).setItemMeta(any()); verify(legacy,never()).setItemMeta(any()); + var empty = item(CompoundBinaryTag.empty()); check(0,get.invoke(handler,empty,"song_hash")); + when(empty.serializeAsBytes()).thenReturn(new byte[]{1,2,3}); check(0,get.invoke(handler,empty,"song_hash")); + check(0,get.invoke(handler,null,"song_hash")); + var clone=mock(ItemStack.class); var meta=mock(ItemMeta.class); var data=mock(PersistentDataContainer.class); + when(pdc.clone()).thenReturn(clone); when(clone.getItemMeta()).thenReturn(meta); when(meta.getPersistentDataContainer()).thenReturn(data); + check(clone,type.getMethod("setNbt",ItemStack.class,String.class,Integer.class).invoke(handler,pdc,"song_hash",123)); + verify(data).set(NamespacedKey.fromString("musicbox:song_hash"),PersistentDataType.INTEGER,123); verify(pdc,never()).setItemMeta(any()); + } + } + static void check(Object expected,Object actual) { if(!java.util.Objects.equals(expected,actual)) throw new AssertionError(expected+" != "+actual); } +}