From ff007497e68a79a54ea1d072f45a467016e8d09a Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:06:38 -0600 Subject: [PATCH 01/10] Harden reflective Nexo item integration --- .../advancedcore/api/item/NexoItemHandle.java | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java index dcc931f6e7..8f35913359 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java @@ -1,29 +1,81 @@ - package com.bencodez.advancedcore.api.item; import java.lang.reflect.Method; import org.bukkit.inventory.ItemStack; +import com.bencodez.advancedcore.AdvancedCorePlugin; + +/** + * Reflection-backed Nexo integration kept isolated from the rest of ItemBuilder. + */ public class NexoItemHandle { + private final ClassLoader classLoader; + private final String apiClassName; + private final String builderClassName; + private volatile Method itemFromIdMethod; + private volatile Method buildMethod; + private volatile boolean compatibilityFailureLogged; + public NexoItemHandle() { + this(NexoItemHandle.class.getClassLoader(), "com.nexomc.nexo.api.NexoItems", "com.nexomc.nexo.items.ItemBuilder"); + } + + protected NexoItemHandle(ClassLoader classLoader, String apiClassName, String builderClassName) { + this.classLoader = classLoader == null ? NexoItemHandle.class.getClassLoader() : classLoader; + this.apiClassName = apiClassName; + this.builderClassName = builderClassName; } public ItemStack getItem(String item) { + if (item == null || item.isEmpty()) { + return null; + } try { - Class nexoItemsClass = Class.forName("com.nexomc.nexo.api.NexoItems"); - Method itemFromIdMethod = nexoItemsClass.getMethod("itemFromId", String.class); + ensureMethods(); Object itemBuilder = itemFromIdMethod.invoke(null, item); - if (itemBuilder != null) { - Class itemBuilderClass = Class.forName("com.nexomc.nexo.items.ItemBuilder"); - Method buildMethod = itemBuilderClass.getMethod("build"); - Object builtItem = buildMethod.invoke(itemBuilder); + if (itemBuilder == null) { + return null; + } + Object builtItem = buildMethod.invoke(itemBuilder); + if (builtItem instanceof ItemStack) { return (ItemStack) builtItem; } - } catch (Exception e) { - e.printStackTrace(); + logFailureOnce("Nexo build() returned an unexpected type: " + + (builtItem == null ? "null" : builtItem.getClass().getName()), null); + } catch (ReflectiveOperationException | LinkageError | RuntimeException e) { + logFailureOnce("Unable to use the installed Nexo item API", e); } return null; } + private void ensureMethods() throws ReflectiveOperationException { + if (itemFromIdMethod != null && buildMethod != null) { + return; + } + synchronized (this) { + if (itemFromIdMethod == null) { + Class nexoItemsClass = Class.forName(apiClassName, true, classLoader); + itemFromIdMethod = nexoItemsClass.getMethod("itemFromId", String.class); + } + if (buildMethod == null) { + Class itemBuilderClass = Class.forName(builderClassName, true, classLoader); + buildMethod = itemBuilderClass.getMethod("build"); + } + } + } + + private void logFailureOnce(String message, Throwable throwable) { + if (compatibilityFailureLogged) { + return; + } + compatibilityFailureLogged = true; + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (plugin != null) { + plugin.getLogger().warning(message); + if (throwable != null) { + plugin.debug(throwable); + } + } + } } From 57c11029f504f0432fa1a2647402e58ce9b837f2 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:06:46 -0600 Subject: [PATCH 02/10] Centralize effect compatibility parsing --- .../api/misc/effects/EffectCompatibility.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/EffectCompatibility.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/EffectCompatibility.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/EffectCompatibility.java new file mode 100644 index 0000000000..61303ba71e --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/EffectCompatibility.java @@ -0,0 +1,32 @@ +package com.bencodez.advancedcore.api.misc.effects; + +import java.util.Locale; + +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; + +/** + * Compatibility parsing shared by player-facing effects. + */ +public final class EffectCompatibility { + + private EffectCompatibility() { + } + + public static BarColor parseBarColor(String value) { + return BarColor.valueOf(normalizeEnum(value, "BLUE")); + } + + public static BarStyle parseBarStyle(String value) { + return BarStyle.valueOf(normalizeEnum(value, "SOLID")); + } + + public static double clampProgress(double progress) { + return Math.max(0D, Math.min(1D, progress)); + } + + private static String normalizeEnum(String value, String defaultValue) { + String effective = value == null || value.trim().isEmpty() ? defaultValue : value.trim(); + return effective.toUpperCase(Locale.ROOT); + } +} From 1a49cc4ff58c42def8baf7194251df6d6b334987 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:07:03 -0600 Subject: [PATCH 03/10] Normalize boss bar compatibility values --- .../api/misc/effects/BossBar.java | 151 ++++-------------- 1 file changed, 34 insertions(+), 117 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java index 861b92f676..ea43811bd5 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java @@ -4,8 +4,6 @@ import java.util.concurrent.TimeUnit; import org.bukkit.Bukkit; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; import org.bukkit.entity.Player; import com.bencodez.advancedcore.AdvancedCorePlugin; @@ -13,85 +11,54 @@ import lombok.Getter; -// TODO: Auto-generated Javadoc /** - * The Class BossBar. + * Bukkit boss bar wrapper used by rewards and downstream plugins. */ public class BossBar { - /** - * Gets the boss bar. - * - * @return the boss bar - */ @Getter private org.bukkit.boss.BossBar bossBar; - /** - * Instantiates a new boss bar. - * - * @param msg the msg - * @param barColor the bar color - * @param barStyle the bar style - * @param progress the progress - */ public BossBar(String msg, String barColor, String barStyle, double progress) { - bossBar = Bukkit.createBossBar(MessageAPI.colorize(msg), BarColor.valueOf(barColor), - BarStyle.valueOf(barStyle)); - bossBar.setProgress(progress); + bossBar = Bukkit.createBossBar(MessageAPI.colorize(msg), EffectCompatibility.parseBarColor(barColor), + EffectCompatibility.parseBarStyle(barStyle)); + bossBar.setProgress(EffectCompatibility.clampProgress(progress)); } - /** - * Adds a player to the boss bar. - * - * @param player the player to add - */ public void addPlayer(Player player) { - bossBar.addPlayer(player); + if (player != null) { + bossBar.addPlayer(player); + } } - /** - * Adds a player to the boss bar with delay. - * - * @param player the player to add - * @param delay the delay in seconds before removing - */ public void addPlayer(final Player player, int delay) { try { if (player == null) { return; } bossBar.addPlayer(player); - if (delay > 0) { - AdvancedCorePlugin.getInstance().getBukkitScheduler().runTaskLater(AdvancedCorePlugin.getInstance(), - new Runnable() { - - @Override - public void run() { - if (bossBar != null && player != null) { - bossBar.removePlayer(player); - } - } - }, delay * 50 + 60, TimeUnit.MILLISECONDS); + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (plugin != null) { + plugin.getBukkitScheduler().runTaskLater(plugin, () -> { + if (bossBar != null) { + bossBar.removePlayer(player); + } + }, delay * 50L + 60L, TimeUnit.MILLISECONDS); + } } } catch (Exception e) { - AdvancedCorePlugin.getInstance().debug(e); + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (plugin != null) { + plugin.debug(e); + } } } - /** - * Gets the players viewing the boss bar. - * - * @return the list of players - */ public List getPlayers() { return bossBar.getPlayers(); } - /** - * Hides the boss bar. - */ public void hide() { if (bossBar != null) { bossBar.setVisible(false); @@ -100,110 +67,60 @@ public void hide() { } private void hideInDelay(int delay) { - AdvancedCorePlugin.getInstance().getBukkitScheduler().runTaskLater(AdvancedCorePlugin.getInstance(), - new Runnable() { - - @Override - public void run() { - hide(); - } - }, delay * 50, TimeUnit.MILLISECONDS); + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (plugin == null) { + return; + } + plugin.getBukkitScheduler().runTaskLater(plugin, this::hide, delay * 50L, TimeUnit.MILLISECONDS); } - /** - * Removes a player from the boss bar. - * - * @param player the player to remove - */ public void removePlayer(Player player) { - bossBar.removePlayer(player); + if (player != null) { + bossBar.removePlayer(player); + } } - /** - * Sends the boss bar to all players. - */ public void send() { bossBar.setVisible(true); } - /** - * Sends the boss bar with delay. - * - * @param delay the delay in seconds - */ public void send(int delay) { bossBar.setVisible(true); - hideInDelay(delay); } - /** - * Send. - * - * @param player the player - * @param delay the delay - */ public void send(Player player, int delay) { + if (player == null) { + return; + } bossBar.addPlayer(player); bossBar.setVisible(true); hideInDelay(delay); } - /** - * Sets the color of the boss bar. - * - * @param barColor the bar color - */ public void setColor(String barColor) { if (barColor != null) { - bossBar.setColor(BarColor.valueOf(barColor)); + bossBar.setColor(EffectCompatibility.parseBarColor(barColor)); } } - /** - * Sets the progress of the boss bar. - * - * @param progress the progress (0.0 to 1.0) - */ public void setProgress(double progress) { - if (progress > 1) { - progress = 1; - } - if (progress < 0) { - progress = 0; - } - bossBar.setProgress(progress); + bossBar.setProgress(EffectCompatibility.clampProgress(progress)); } - /** - * Sets the style of the boss bar. - * - * @param barStyle the bar style - */ public void setStyle(String barStyle) { if (barStyle != null) { - bossBar.setStyle(BarStyle.valueOf(barStyle)); + bossBar.setStyle(EffectCompatibility.parseBarStyle(barStyle)); } } - /** - * Sets the title of the boss bar. - * - * @param title the title - */ public void setTitle(String title) { if (title != null) { bossBar.setTitle(MessageAPI.colorize(title)); } - } - /** - * Sets the visibility of the boss bar. - * - * @param visible true to show, false to hide - */ public void setVisible(boolean visible) { bossBar.setVisible(visible); } -} \ No newline at end of file +} From c53a15df17f1eff29d13d4f7b3e3a23ebc4541cb Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:07:22 -0600 Subject: [PATCH 04/10] Test custom item and effect compatibility --- .../tests/item/ItemCompatibilityTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java new file mode 100644 index 0000000000..4dc076aca2 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java @@ -0,0 +1,52 @@ +package com.bencodez.advancedcore.tests.item; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.bukkit.Material; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.Test; + +import com.bencodez.advancedcore.api.item.NexoItemHandle; +import com.bencodez.advancedcore.api.misc.effects.EffectCompatibility; + +public class ItemCompatibilityTest { + + @Test + public void nexoReflectionBuildsExpectedItemType() { + NexoItemHandle handle = new TestNexoItemHandle(); + + ItemStack item = handle.getItem("test"); + + assertEquals(Material.STONE, item.getType()); + assertNull(handle.getItem("missing")); + } + + @Test + public void bossBarCompatibilityAcceptsCaseAndClampsProgress() { + assertEquals(BarColor.BLUE, EffectCompatibility.parseBarColor("blue")); + assertEquals(BarStyle.SOLID, EffectCompatibility.parseBarStyle(" solid ")); + assertEquals(1D, EffectCompatibility.clampProgress(2D)); + assertEquals(0D, EffectCompatibility.clampProgress(-1D)); + } + + private static final class TestNexoItemHandle extends NexoItemHandle { + private TestNexoItemHandle() { + super(ItemCompatibilityTest.class.getClassLoader(), FakeNexoItems.class.getName(), FakeBuilder.class.getName()); + } + } + + public static final class FakeNexoItems { + public static FakeBuilder itemFromId(String id) { + return "test".equals(id) ? new FakeBuilder() : null; + } + } + + public static final class FakeBuilder { + public ItemStack build() { + return new ItemStack(Material.STONE); + } + } +} From 8ed4bfb3d7222fdf5851dba0310e38e153052e6e Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:07:54 -0600 Subject: [PATCH 05/10] Share Nexo reflection cache across item lookups --- .../advancedcore/api/item/NexoItemHandle.java | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java index 8f35913359..3c34baf348 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java @@ -1,6 +1,9 @@ package com.bencodez.advancedcore.api.item; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; import org.bukkit.inventory.ItemStack; @@ -10,21 +13,17 @@ * Reflection-backed Nexo integration kept isolated from the rest of ItemBuilder. */ public class NexoItemHandle { - private final ClassLoader classLoader; - private final String apiClassName; - private final String builderClassName; - private volatile Method itemFromIdMethod; - private volatile Method buildMethod; - private volatile boolean compatibilityFailureLogged; + private static final Map> CACHES = new WeakHashMap<>(); + + private final ReflectionCache cache; public NexoItemHandle() { this(NexoItemHandle.class.getClassLoader(), "com.nexomc.nexo.api.NexoItems", "com.nexomc.nexo.items.ItemBuilder"); } protected NexoItemHandle(ClassLoader classLoader, String apiClassName, String builderClassName) { - this.classLoader = classLoader == null ? NexoItemHandle.class.getClassLoader() : classLoader; - this.apiClassName = apiClassName; - this.builderClassName = builderClassName; + ClassLoader effectiveClassLoader = classLoader == null ? NexoItemHandle.class.getClassLoader() : classLoader; + cache = sharedCache(effectiveClassLoader, apiClassName, builderClassName); } public ItemStack getItem(String item) { @@ -33,11 +32,11 @@ public ItemStack getItem(String item) { } try { ensureMethods(); - Object itemBuilder = itemFromIdMethod.invoke(null, item); + Object itemBuilder = cache.itemFromIdMethod.invoke(null, item); if (itemBuilder == null) { return null; } - Object builtItem = buildMethod.invoke(itemBuilder); + Object builtItem = cache.buildMethod.invoke(itemBuilder); if (builtItem instanceof ItemStack) { return (ItemStack) builtItem; } @@ -50,26 +49,28 @@ public ItemStack getItem(String item) { } private void ensureMethods() throws ReflectiveOperationException { - if (itemFromIdMethod != null && buildMethod != null) { + if (cache.itemFromIdMethod != null && cache.buildMethod != null) { return; } - synchronized (this) { - if (itemFromIdMethod == null) { - Class nexoItemsClass = Class.forName(apiClassName, true, classLoader); - itemFromIdMethod = nexoItemsClass.getMethod("itemFromId", String.class); + synchronized (cache) { + if (cache.itemFromIdMethod == null) { + Class nexoItemsClass = Class.forName(cache.apiClassName, true, cache.classLoader); + cache.itemFromIdMethod = nexoItemsClass.getMethod("itemFromId", String.class); } - if (buildMethod == null) { - Class itemBuilderClass = Class.forName(builderClassName, true, classLoader); - buildMethod = itemBuilderClass.getMethod("build"); + if (cache.buildMethod == null) { + Class itemBuilderClass = Class.forName(cache.builderClassName, true, cache.classLoader); + cache.buildMethod = itemBuilderClass.getMethod("build"); } } } private void logFailureOnce(String message, Throwable throwable) { - if (compatibilityFailureLogged) { - return; + synchronized (cache) { + if (cache.compatibilityFailureLogged) { + return; + } + cache.compatibilityFailureLogged = true; } - compatibilityFailureLogged = true; AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); if (plugin != null) { plugin.getLogger().warning(message); @@ -78,4 +79,27 @@ private void logFailureOnce(String message, Throwable throwable) { } } } + + private static synchronized ReflectionCache sharedCache(ClassLoader classLoader, String apiClassName, + String builderClassName) { + Map classLoaderCaches = CACHES.computeIfAbsent(classLoader, ignored -> new HashMap<>()); + String key = apiClassName + '\0' + builderClassName; + return classLoaderCaches.computeIfAbsent(key, + ignored -> new ReflectionCache(classLoader, apiClassName, builderClassName)); + } + + private static final class ReflectionCache { + private final ClassLoader classLoader; + private final String apiClassName; + private final String builderClassName; + private volatile Method itemFromIdMethod; + private volatile Method buildMethod; + private boolean compatibilityFailureLogged; + + private ReflectionCache(ClassLoader classLoader, String apiClassName, String builderClassName) { + this.classLoader = classLoader; + this.apiClassName = apiClassName; + this.builderClassName = builderClassName; + } + } } From 422a74b5570d322ea5fc9c658f5f25b548f9f38c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:08:08 -0600 Subject: [PATCH 06/10] Test shared Nexo reflection cache --- .../tests/item/ItemCompatibilityTest.java | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java index 4dc076aca2..03bcd70bb6 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.concurrent.atomic.AtomicInteger; + import org.bukkit.Material; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; @@ -16,7 +18,7 @@ public class ItemCompatibilityTest { @Test public void nexoReflectionBuildsExpectedItemType() { - NexoItemHandle handle = new TestNexoItemHandle(); + NexoItemHandle handle = new TestNexoItemHandle(ItemCompatibilityTest.class.getClassLoader()); ItemStack item = handle.getItem("test"); @@ -24,6 +26,20 @@ public void nexoReflectionBuildsExpectedItemType() { assertNull(handle.getItem("missing")); } + @Test + public void nexoReflectionCacheIsSharedAcrossHandles() { + CountingClassLoader loader = new CountingClassLoader(ItemCompatibilityTest.class.getClassLoader()); + NexoItemHandle first = new TestNexoItemHandle(loader); + NexoItemHandle second = new TestNexoItemHandle(loader); + + assertEquals(Material.STONE, first.getItem("test").getType()); + int afterFirst = loader.getNexoLoads(); + assertEquals(Material.STONE, second.getItem("test").getType()); + + assertEquals(2, afterFirst, "the API and builder classes should each be resolved once"); + assertEquals(afterFirst, loader.getNexoLoads(), "a second handle must reuse the shared reflection cache"); + } + @Test public void bossBarCompatibilityAcceptsCaseAndClampsProgress() { assertEquals(BarColor.BLUE, EffectCompatibility.parseBarColor("blue")); @@ -33,8 +49,28 @@ public void bossBarCompatibilityAcceptsCaseAndClampsProgress() { } private static final class TestNexoItemHandle extends NexoItemHandle { - private TestNexoItemHandle() { - super(ItemCompatibilityTest.class.getClassLoader(), FakeNexoItems.class.getName(), FakeBuilder.class.getName()); + private TestNexoItemHandle(ClassLoader classLoader) { + super(classLoader, FakeNexoItems.class.getName(), FakeBuilder.class.getName()); + } + } + + private static final class CountingClassLoader extends ClassLoader { + private final AtomicInteger nexoLoads = new AtomicInteger(); + + private CountingClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.equals(FakeNexoItems.class.getName()) || name.equals(FakeBuilder.class.getName())) { + nexoLoads.incrementAndGet(); + } + return super.loadClass(name, resolve); + } + + private int getNexoLoads() { + return nexoLoads.get(); } } From 5a1e040ee4dc6f9be1d6555c97162701b6f4aee1 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:30:19 -0600 Subject: [PATCH 07/10] Avoid retaining Nexo class loaders from weak cache --- .../advancedcore/api/item/NexoItemHandle.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java index 3c34baf348..73c3232a3f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java @@ -1,5 +1,6 @@ package com.bencodez.advancedcore.api.item; +import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -13,7 +14,7 @@ * Reflection-backed Nexo integration kept isolated from the rest of ItemBuilder. */ public class NexoItemHandle { - private static final Map> CACHES = new WeakHashMap<>(); + private static final Map>> CACHES = new WeakHashMap<>(); private final ReflectionCache cache; @@ -82,10 +83,18 @@ private void logFailureOnce(String message, Throwable throwable) { private static synchronized ReflectionCache sharedCache(ClassLoader classLoader, String apiClassName, String builderClassName) { - Map classLoaderCaches = CACHES.computeIfAbsent(classLoader, ignored -> new HashMap<>()); + Map> classLoaderCaches = CACHES.computeIfAbsent(classLoader, + ignored -> new HashMap<>()); String key = apiClassName + '\0' + builderClassName; - return classLoaderCaches.computeIfAbsent(key, - ignored -> new ReflectionCache(classLoader, apiClassName, builderClassName)); + WeakReference reference = classLoaderCaches.get(key); + ReflectionCache existing = reference == null ? null : reference.get(); + if (existing != null) { + return existing; + } + + ReflectionCache created = new ReflectionCache(classLoader, apiClassName, builderClassName); + classLoaderCaches.put(key, new WeakReference<>(created)); + return created; } private static final class ReflectionCache { From 6c808bfd7418935e2cf1170858a77690e85afc24 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:30:37 -0600 Subject: [PATCH 08/10] Test weak Nexo cache values --- .../tests/item/ItemCompatibilityTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java index 03bcd70bb6..dbc8ba4434 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java @@ -2,7 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.bukkit.Material; @@ -40,6 +44,21 @@ public void nexoReflectionCacheIsSharedAcrossHandles() { assertEquals(afterFirst, loader.getNexoLoads(), "a second handle must reuse the shared reflection cache"); } + @Test + @SuppressWarnings("unchecked") + public void sharedNexoCacheDoesNotStronglyRetainReflectionState() throws Exception { + CountingClassLoader loader = new CountingClassLoader(ItemCompatibilityTest.class.getClassLoader()); + new TestNexoItemHandle(loader); + + Field cachesField = NexoItemHandle.class.getDeclaredField("CACHES"); + cachesField.setAccessible(true); + Map> caches = (Map>) cachesField.get(null); + Map loaderCache = caches.get(loader); + + assertTrue(loaderCache.values().stream().allMatch(WeakReference.class::isInstance), + "weak class-loader keys must not have values that strongly retain the reflection cache"); + } + @Test public void bossBarCompatibilityAcceptsCaseAndClampsProgress() { assertEquals(BarColor.BLUE, EffectCompatibility.parseBarColor("blue")); From 088793d41b66a66b08a099b7ef7c49ab8212f51c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:18:54 -0600 Subject: [PATCH 09/10] Keep production Nexo reflection cache stable --- .../bencodez/advancedcore/api/item/NexoItemHandle.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java index 73c3232a3f..fc8ce74e80 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/NexoItemHandle.java @@ -14,6 +14,7 @@ * Reflection-backed Nexo integration kept isolated from the rest of ItemBuilder. */ public class NexoItemHandle { + private static final Map DEFAULT_CACHES = new HashMap<>(); private static final Map>> CACHES = new WeakHashMap<>(); private final ReflectionCache cache; @@ -83,9 +84,14 @@ private void logFailureOnce(String message, Throwable throwable) { private static synchronized ReflectionCache sharedCache(ClassLoader classLoader, String apiClassName, String builderClassName) { + String key = apiClassName + '\0' + builderClassName; + if (classLoader == NexoItemHandle.class.getClassLoader()) { + return DEFAULT_CACHES.computeIfAbsent(key, + ignored -> new ReflectionCache(classLoader, apiClassName, builderClassName)); + } + Map> classLoaderCaches = CACHES.computeIfAbsent(classLoader, ignored -> new HashMap<>()); - String key = apiClassName + '\0' + builderClassName; WeakReference reference = classLoaderCaches.get(key); ReflectionCache existing = reference == null ? null : reference.get(); if (existing != null) { From d4bce69cdbee6b1666d2b4964cfcdc9613290865 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:19:14 -0600 Subject: [PATCH 10/10] Cover stable default Nexo cache lifetime --- .../tests/item/ItemCompatibilityTest.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java index dbc8ba4434..b10a436aa1 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/item/ItemCompatibilityTest.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.tests.item; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -46,7 +47,22 @@ public void nexoReflectionCacheIsSharedAcrossHandles() { @Test @SuppressWarnings("unchecked") - public void sharedNexoCacheDoesNotStronglyRetainReflectionState() throws Exception { + public void defaultNexoCacheRetainsReflectionStateBetweenTransientHandles() throws Exception { + NexoItemHandle handle = new TestNexoItemHandle(NexoItemHandle.class.getClassLoader()); + assertEquals(Material.STONE, handle.getItem("test").getType()); + + Field defaultCachesField = NexoItemHandle.class.getDeclaredField("DEFAULT_CACHES"); + defaultCachesField.setAccessible(true); + Map defaultCaches = (Map) defaultCachesField.get(null); + + assertFalse(defaultCaches.isEmpty()); + assertTrue(defaultCaches.values().stream().noneMatch(WeakReference.class::isInstance), + "the production/default loader must retain resolved reflection state between temporary handles"); + } + + @Test + @SuppressWarnings("unchecked") + public void sharedNexoCacheDoesNotStronglyRetainDisposableClassLoaders() throws Exception { CountingClassLoader loader = new CountingClassLoader(ItemCompatibilityTest.class.getClassLoader()); new TestNexoItemHandle(loader); @@ -56,7 +72,7 @@ public void sharedNexoCacheDoesNotStronglyRetainReflectionState() throws Excepti Map loaderCache = caches.get(loader); assertTrue(loaderCache.values().stream().allMatch(WeakReference.class::isInstance), - "weak class-loader keys must not have values that strongly retain the reflection cache"); + "disposable class-loader keys must not have values that strongly retain reflection state"); } @Test