From ebc81c6d95cbecbfe3d6e8e972c12ea0ef761845 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Fri, 30 Jan 2026 11:48:25 -0500 Subject: [PATCH 01/20] Update CI --- .github/workflows/auto-merge-dependabot.yml | 15 ---------- .github/workflows/ci.yml | 4 +-- .github/workflows/pull_request.yml | 32 +++++++++++++++++---- 3 files changed, 29 insertions(+), 22 deletions(-) delete mode 100644 .github/workflows/auto-merge-dependabot.yml diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml deleted file mode 100644 index ba6410d..0000000 --- a/.github/workflows/auto-merge-dependabot.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Auto-merge Dependabot PRs - -on: - workflow_run: - workflows: [ "Pull Request" ] - types: [ completed ] - -jobs: - automerge-dependabot-pr: - if: "github.actor == 'dependabot[bot]' - && github.event.workflow_run.event == 'pull_request' - && github.event.workflow_run.conclusion == 'success'" - uses: Jikoo/PlanarActions/.github/workflows/pr_automerge_complete.yml@master - with: - triggering-workflow-run: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbba0de..6dd2f98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: build: runs-on: ubuntu-latest - name: Build Java 17 + name: Build Java 21 steps: - uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'adopt' - java-version: '17' + java-version: '21' cache: 'maven' - name: Cache SonarCloud packages diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 236f785..c702210 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -6,8 +6,30 @@ on: jobs: run-ci: uses: Jikoo/PlanarActions/.github/workflows/ci_maven.yml@master - store-dependabot-pr-data: - if: "github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'" - uses: Jikoo/PlanarActions/.github/workflows/pr_automerge_prep.yml@master - with: - pr-number: ${{ github.event.number }} + approve-and-merge-dependabot: + if: "github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'" + needs: [ "run-ci" ] + runs-on: "ubuntu-latest" + steps: + # Always approve PRs from Dependabot. + - name: Approve + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + # Fetch Dependabot metadata for finer decisionmaking later. + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # Enable auto-merge for the PR. + # Auto-merge is used rather than a direct merge so that any other required checks can pass. + - name: Enable auto-merge for minor/patch updates + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} From 331cfed24a032efcda8f196f85244a1312f32bb8 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Fri, 30 Jan 2026 11:50:46 -0500 Subject: [PATCH 02/20] Swap to Paper --- pom.xml | 17 +- .../enchantableblocks/mock/ServerMocks.java | 81 +++----- .../mock/inventory/ItemFactoryMocks.java | 196 +++++++++++------- ...{ItemMetaHelper.java => ItemMetaBase.java} | 7 +- .../mock/inventory/ItemStackMocks.java | 158 ++++++++++++++ .../mock/matcher/IsSimilarMatcher.java | 41 ---- .../mock/matcher/ItemMatcher.java | 106 ++++++++++ .../mock/server/RegistryHelper.java | 186 +++++++++++++++++ .../mock/server/TestRegistryAccess.java | 93 +++++++++ .../mock/server/TestServerBuildInfo.java | 62 ++++++ .../services/io.papermc.paper.ServerBuildInfo | 1 + .../io.papermc.paper.registry.RegistryAccess | 1 + 12 files changed, 770 insertions(+), 179 deletions(-) rename src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/{ItemMetaHelper.java => ItemMetaBase.java} (87%) create mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java delete mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/IsSimilarMatcher.java create mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/ItemMatcher.java create mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java create mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java create mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java create mode 100644 src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo create mode 100644 src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess diff --git a/pom.xml b/pom.xml index 1655ea6..903e737 100644 --- a/pom.xml +++ b/pom.xml @@ -13,9 +13,7 @@ UTF-8 UTF-8 - 17 - 17 - 17 + 21 8.5.18 @@ -23,8 +21,7 @@ sonarcloud - 17 - 17 + 21 Jikoo_EnchantableBlocks jikoo https://sonarcloud.io @@ -61,8 +58,8 @@ - spigot-repo - https://hub.spigotmc.org/nexus/content/groups/public/ + papermc + https://repo.papermc.io/repository/maven-public/ jitpack.io @@ -78,9 +75,9 @@ provided - org.spigotmc - spigot-api - 1.21-R0.1-SNAPSHOT + io.papermc.paper + paper-api + 1.21.11-R0.1-SNAPSHOT provided diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java index ee9ebc5..5544006 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java @@ -1,71 +1,56 @@ package com.github.jikoo.enchantableblocks.mock; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; +import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import org.bukkit.Bukkit; import org.bukkit.Keyed; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; +import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.Tag; +import org.bukkit.UnsafeValues; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Field; +import java.util.Set; +import java.util.logging.Logger; + +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +// These suppressions are for internals we have to mock to get a usable server for testing. +@SuppressWarnings({"deprecation", "UnstableApiUsage"}) public final class ServerMocks { public static @NotNull Server mockServer() { Server mock = mock(Server.class); + doReturn(ServerMocks.class.getName()).when(mock).getName(); + doReturn("1.2.3").when(mock).getVersion(); + doReturn("1.2.3-SAMPLETEXT").when(mock).getBukkitVersion(); + Logger noOp = mock(Logger.class); when(mock.getLogger()).thenReturn(noOp); when(mock.isPrimaryThread()).thenReturn(true); + ItemFactory itemFactory = ItemFactoryMocks.mockFactory(); + when(mock.getItemFactory()).thenReturn(itemFactory); + doAnswer(invocation -> { + UnsafeValues unsafe = mock(); + + ItemStack empty = mock(); + doReturn(Material.AIR).when(empty).getType(); + when(unsafe.createEmptyStack()).thenReturn(empty); + + return unsafe; + }).when(mock).getUnsafe(); + // Server must be available before tags can be mocked. Bukkit.setServer(mock); - // Bukkit has a lot of static constants referencing registry values. To initialize those, the - // registries must be able to be fetched before the classes are touched. - Map, Object> registers = new HashMap<>(); - - doAnswer(invocationGetRegistry -> - registers.computeIfAbsent(invocationGetRegistry.getArgument(0), clazz -> { - Registry registry = mock(); - Map cache = new HashMap<>(); - doAnswer(invocationGetEntry -> { - NamespacedKey key = invocationGetEntry.getArgument(0); - // Some classes (like BlockType and ItemType) have extra generics that will be - // erased during runtime calls. To ensure accurate typing, grab the constant's field. - // This approach also allows us to return null for unsupported keys. - Class constantClazz; - try { - //noinspection unchecked - constantClazz = (Class) clazz.getField(key.getKey().toUpperCase( - Locale.ROOT).replace('.', '_')).getType(); - } catch (ClassCastException e) { - throw new RuntimeException(e); - } catch (NoSuchFieldException e) { - return null; - } - - return cache.computeIfAbsent(key, key1 -> { - Keyed keyed = mock(constantClazz); - doReturn(key).when(keyed).getKey(); - return keyed; - }); - }).when(registry).get(notNull()); - return registry; - })) - .when(mock).getRegistry(notNull()); - // Tags are dependent on registries, but use a different method. // This will set up blank tags for each constant; all that needs to be done to render them // functional is to re-mock Tag#getValues. diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java index 06e3276..1181e96 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java @@ -1,28 +1,12 @@ package com.github.jikoo.enchantableblocks.mock.inventory; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; +import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ArmorMeta; import org.bukkit.inventory.meta.AxolotlBucketMeta; import org.bukkit.inventory.meta.BannerMeta; import org.bukkit.inventory.meta.BlockStateMeta; @@ -39,8 +23,10 @@ import org.bukkit.inventory.meta.LeatherArmorMeta; import org.bukkit.inventory.meta.MapMeta; import org.bukkit.inventory.meta.MusicInstrumentMeta; +import org.bukkit.inventory.meta.OminousBottleMeta; import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.inventory.meta.Repairable; +import org.bukkit.inventory.meta.ShieldMeta; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.inventory.meta.SpawnEggMeta; import org.bukkit.inventory.meta.SuspiciousStewMeta; @@ -50,6 +36,25 @@ import org.jetbrains.annotations.Nullable; import org.mockito.ArgumentMatchers; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + public final class ItemFactoryMocks { private static final Map, Consumer> CLASS_META_METHODS; @@ -90,75 +95,85 @@ public final class ItemFactoryMocks { if (material == Material.AIR) { return null; } - if (material == Material.WRITTEN_BOOK || material == Material.WRITABLE_BOOK) { - // Note: Internally 2 separate implementations, but the API doesn't differentiate - return copyDetails(createMeta(BookMeta.class), meta); + if (material.name().startsWith("LEATHER_")) { + return copyDetails(createMeta(LeatherArmorMeta.class), meta); + } + if (material == Material.SHIELD) { + return copyDetails(createMeta(ShieldMeta.class), meta); + } + if (material.name().endsWith("_BOOTS") || material.name().endsWith("_CHESTPLATE") || material.name().endsWith("_HELMET") || material.name().endsWith("_LEGGINGS")) { + return copyDetails(createMeta(ArmorMeta.class), meta); + } + if (material == Material.AXOLOTL_BUCKET) { + return copyDetails(createMeta(AxolotlBucketMeta.class), meta); + } + if (material.name().endsWith("_BANNER")) { + return copyDetails(createMeta(BannerMeta.class), meta); } if (material.name().endsWith("_HEAD") || material.name().endsWith("_SKULL")) { return copyDetails(createMeta(SkullMeta.class), meta); } - if (material.name().startsWith("LEATHER_")) { - return copyDetails(createMeta(LeatherArmorMeta.class), meta); - } - if (material.name().endsWith("POTION") || material == Material.TIPPED_ARROW) { - return copyDetails(createMeta(PotionMeta.class), meta); + // Perhaps a generic BlockStateMeta+BlockDataMeta if material.isBlock()? + if (material.data != null && BlockData.class.isAssignableFrom(material.data) + || List.of(Material.SPAWNER, Material.ENCHANTING_TABLE, Material.BEACON).contains(material)) { + return copyDetails(createMeta(BlockStateMeta.class), meta); } - if (material == Material.FILLED_MAP) { - return copyDetails(createMeta(MapMeta.class), meta); + if (material == Material.BUNDLE) { + return copyDetails(createMeta(BundleMeta.class), meta); } - if (material == Material.FIREWORK_ROCKET) { - return copyDetails(createMeta(FireworkMeta.class), meta); + // ColorableArmorMeta only cases are also LeatherArmorMeta, which is a subclass. + if (material == Material.COMPASS) { + return copyDetails(createMeta(CompassMeta.class), meta); } - if (material == Material.FIREWORK_STAR) { - return copyDetails(createMeta(FireworkEffectMeta.class), meta); + if (material == Material.CROSSBOW) { + return copyDetails(createMeta(CrossbowMeta.class), meta); } if (material == Material.ENCHANTED_BOOK) { return copyDetails(createMeta(EnchantmentStorageMeta.class), meta); } - if (material.name().endsWith("_BANNER")) { - return copyDetails(createMeta(BannerMeta.class), meta); + if (material == Material.FIREWORK_STAR) { + return copyDetails(createMeta(FireworkEffectMeta.class), meta); } - if (material.name().endsWith("_SPAWN_EGG")) { - return copyDetails(createMeta(SpawnEggMeta.class), meta); + if (material == Material.FIREWORK_ROCKET) { + return copyDetails(createMeta(FireworkMeta.class), meta); } - // CB has a different meta for Armor Stands and one for things like fish buckets but there's no API for in-inventory entities. if (material == Material.KNOWLEDGE_BOOK) { return copyDetails(createMeta(KnowledgeBookMeta.class), meta); } - if (material.data != null && BlockData.class.isAssignableFrom(material.data) - || List.of(Material.SPAWNER, Material.ENCHANTING_TABLE, Material.BEACON, Material.SHIELD).contains(material)) { - return copyDetails(createMeta(BlockStateMeta.class), meta); + if (material == Material.FILLED_MAP) { + return copyDetails(createMeta(MapMeta.class), meta); } - if (material == Material.TROPICAL_FISH_BUCKET) { - return copyDetails(createMeta(TropicalFishBucketMeta.class), meta); + if (material == Material.GOAT_HORN) { + return copyDetails(createMeta(MusicInstrumentMeta.class), meta); } - if (material == Material.AXOLOTL_BUCKET) { - return copyDetails(createMeta(AxolotlBucketMeta.class), meta); + if (material == Material.OMINOUS_BOTTLE) { + return copyDetails(createMeta(OminousBottleMeta.class), meta); } - if (material == Material.CROSSBOW) { - return copyDetails(createMeta(CrossbowMeta.class), meta); + if (material.name().endsWith("POTION") || material == Material.TIPPED_ARROW) { + return copyDetails(createMeta(PotionMeta.class), meta); + } + if (material.name().endsWith("_SPAWN_EGG")) { + return copyDetails(createMeta(SpawnEggMeta.class), meta); } if (material == Material.SUSPICIOUS_STEW) { return copyDetails(createMeta(SuspiciousStewMeta.class), meta); } - if (material == Material.COMPASS) { - return copyDetails(createMeta(CompassMeta.class), meta); - } - if (material == Material.BUNDLE) { - return copyDetails(createMeta(BundleMeta.class), meta); + if (material == Material.TROPICAL_FISH_BUCKET) { + return copyDetails(createMeta(TropicalFishBucketMeta.class), meta); } - if (material == Material.GOAT_HORN) { - return copyDetails(createMeta(MusicInstrumentMeta.class), meta); + if (material == Material.WRITTEN_BOOK || material == Material.WRITABLE_BOOK) { + // Note: Internally 2 separate implementations, but the API doesn't differentiate + return copyDetails(createMeta(BookMeta.class), meta); } return copyDetails(createMeta(ItemMeta.class), meta); } - public static @NotNull T createMeta(@NotNull Class metaClass) { + public static @NotNull T createMeta(@NotNull Class metaClass) { T meta; - if (metaClass == ItemMetaHelper.class) { + if (metaClass == ItemMetaBase.class) { meta = mock(metaClass); } else { - meta = mock(metaClass, withSettings().extraInterfaces(ItemMetaHelper.class)); + meta = mock(metaClass, withSettings().extraInterfaces(ItemMetaBase.class)); } for (Entry, Consumer> classConsumerEntry : CLASS_META_METHODS.entrySet()) { @@ -168,7 +183,7 @@ public final class ItemFactoryMocks { } // Lazy copy the creation process and details. - doAnswer(invocation -> copyDetails(createMeta(metaClass), meta)).when(meta).clone(); + when(meta.clone()).thenAnswer(invocation -> copyDetails(createMeta(metaClass), meta)); return meta; } @@ -183,34 +198,47 @@ public final class ItemFactoryMocks { // Note that certain things such as enchantments have more specific setting methods; // setters for bulk handling can be added via the ItemMetaCloneHelper interface. for (Method getter : oldMeta.getClass().getMethods()) { - if (isSimpleGetter(getter)) { + Method setter = findSetter(newMeta.getClass(), getter); + if (setter != null) { try { - // Find matching setter. - Method setter = newMeta.getClass() - .getMethod("set" + getter.getName().substring(3), getter.getReturnType()); // Get value. Object oldValue = getter.invoke(oldMeta); if (oldValue != null) { // Only set if non-null; Null should be default for nullables, and this may just be a stub. setter.invoke(newMeta, oldValue); } - } catch (NoSuchMethodException ignored) { - // No matching method, not a getter + setter pair. } catch (InvocationTargetException | IllegalAccessException e) { // Method should be accessible and invokable - must be public and types should match. throw new RuntimeException(e); } + } } return newMeta; } - private static boolean isSimpleGetter(@NotNull Method method) { - // Getter must start with "get". - return method.getName().startsWith("get") - // We only accept getters that don't accept parameters - we have no idea what to provide. - && method.getParameterCount() == 0 + private static @Nullable Method findSetter(@NotNull Class clazz, @NotNull Method method) { + if (!isPossibleGetter(method)) { + return null; + } + + String methodName = method.getName(); + Class returnType = method.getReturnType(); + + // Simple get/set pattern. Setter must accept type returned by getter. + if (methodName.startsWith("get")) { + return findSetter(clazz, "set" + methodName.substring(3), returnType); + } + + // Paper uses a different pattern to prevent signature + // conflicts: getter and setter have matching names. + return findSetter(clazz, methodName, returnType); + } + + private static boolean isPossibleGetter(@NotNull Method method) { + // We only accept getters that don't accept parameters - we have no idea what to provide. + return method.getParameterCount() == 0 // Getter must actually get a value. && method.getReturnType() != void.class // Getter must not be part of Mockito's internals. @@ -219,6 +247,20 @@ private static boolean isSimpleGetter(@NotNull Method method) { && !Modifier.isNative(method.getModifiers()); } + private static @Nullable Method findSetter( + @NotNull Class clazz, + @NotNull String methodName, + @NotNull Class returnType + ) { + try { + // Find corresponding set method accepting return type. + return clazz.getMethod(methodName, returnType); + } catch (NoSuchMethodException ignored) { + // Otherwise, no setter. Return null. + return null; + } + } + private static boolean equals(@Nullable ItemMeta meta, @Nullable ItemMeta other) { if (meta == other) { return true; @@ -239,7 +281,7 @@ private static boolean equals(@Nullable ItemMeta meta, @Nullable ItemMeta other) Map methodValues = new HashMap<>(); for (Method getter : meta.getClass().getMethods()) { - if (!isSimpleGetter(getter)) { + if (findSetter(meta.getClass(), getter) == null) { continue; } @@ -258,13 +300,13 @@ private static boolean equals(@Nullable ItemMeta meta, @Nullable ItemMeta other) private static void meta(@NotNull ItemMeta meta) { // Display name - AtomicReference displayName = new AtomicReference<>(); - when(meta.hasDisplayName()).thenAnswer(invocation -> displayName.get() != null); - when(meta.getDisplayName()).thenAnswer(invocation -> displayName.get()); + AtomicReference customName = new AtomicReference<>(); + when(meta.hasCustomName()).thenAnswer(invocation -> customName.get() != null); + when(meta.customName()).thenAnswer(invocation -> customName.get()); doAnswer(invocation -> { - displayName.set(invocation.getArgument(0)); + customName.set(invocation.getArgument(0)); return null; - }).when(meta).setDisplayName(any()); + }).when(meta).customName(any()); // Enchantments Map enchants = new HashMap<>(); @@ -298,7 +340,7 @@ private static void meta(@NotNull ItemMeta meta) { when(meta.getEnchants()).thenAnswer(invocation -> Map.copyOf(enchants)); when(meta.hasEnchants()).thenAnswer(invocation -> !enchants.isEmpty()); - if (meta instanceof ItemMetaHelper cloneHelper) { + if (meta instanceof ItemMetaBase cloneHelper) { doAnswer(invocation -> { enchants.clear(); Map map = invocation.getArgument(0); @@ -314,7 +356,7 @@ private static void meta(@NotNull ItemMeta meta) { }).when(cloneHelper).setEnchants(any()); } - // TODO lore + // Doesn't set up lore, unnecessary here. } private static void repairable(@NotNull ItemMeta meta) { @@ -382,7 +424,7 @@ private static void enchantableBook(@NotNull ItemMeta meta) { when(storageMeta.getStoredEnchants()).thenAnswer(invocation -> Map.copyOf(stored)); when(storageMeta.hasStoredEnchants()).thenAnswer(invocation -> !stored.isEmpty()); - if (meta instanceof ItemMetaHelper cloneHelper) { + if (meta instanceof ItemMetaBase cloneHelper) { doAnswer(invocation -> { stored.clear(); Map map = invocation.getArgument(0); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemMetaHelper.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemMetaBase.java similarity index 87% rename from src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemMetaHelper.java rename to src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemMetaBase.java index 25e72cd..c468cc0 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemMetaHelper.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemMetaBase.java @@ -1,24 +1,25 @@ package com.github.jikoo.enchantableblocks.mock.inventory; -import java.util.Map; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.Repairable; import org.jetbrains.annotations.NotNull; +import java.util.Map; + /** * An interface used to help perform corresponding get/set operations on ItemMetas while "cloning" * via reflection. Also helps simplify boilerplate in initial mocking because no ItemMeta * implementation exists that does not implement Damageable, Repairable, and BlockDataMeta. * See CraftMetaItem. */ -public interface ItemMetaHelper extends ItemMeta, Repairable, Damageable { +public interface ItemMetaBase extends ItemMeta, Repairable, Damageable { void setEnchants(Map enchantments); void setStoredEnchants(Map enchantments); - @NotNull ItemMetaHelper clone(); + @NotNull ItemMetaBase clone(); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java new file mode 100644 index 0000000..da62402 --- /dev/null +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java @@ -0,0 +1,158 @@ +package com.github.jikoo.enchantableblocks.mock.inventory; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mockito.stubbing.Answer; + +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public enum ItemStackMocks { + ; + + public static ItemStack newItemMock(@NotNull ItemType type, int amount) { + ItemStack stack = mock(); + + Material material = Material.getMaterial(type.getKey().getKey().toUpperCase(Locale.ROOT)); + if (material == null) { + throw new IllegalArgumentException("Unable to locate Material for ItemType " + type.getKey().getKey()); + } + doReturn(material).when(stack).getType(); + + // Amount get/set. + AtomicInteger amt = new AtomicInteger(amount); + doAnswer(invocation -> amt.get()).when(stack).getAmount(); + doAnswer(invocation -> { + amt.set(invocation.getArgument(0)); + return null; + }).when(stack).setAmount(anyInt()); + + // Item meta. + AtomicReference meta = new AtomicReference<>(); + doAnswer(invocation -> { + ItemMeta existing = meta.get(); + if (existing != null) { + return existing.clone(); + } + return Bukkit.getItemFactory().getItemMeta(stack.getType()); + }).when(stack).getItemMeta(); + doAnswer(invocation -> { + ItemMeta newMeta = invocation.getArgument(0); + meta.set(newMeta != null ? newMeta.clone() : null); + return null; + }).when(stack).setItemMeta(any()); + doAnswer(invocation -> meta.get() != null).when(stack).hasItemMeta(); + + // Item cloning. + doAnswer(invocation -> { + ItemStack clone = newItemMock(type, amount); + // Must also clone the meta or methods that manipulate the meta directly will mutate both. + clone.setItemMeta(get(meta, null, false).map(ItemMeta::clone).orElse(null)); + return clone; + }).when(stack).clone(); + + // Item similarity. Note that the only thing we track other the meta is the count. + doAnswer(invocation -> { + ItemStack other = invocation.getArgument(0); + if (other == null || other.getType() != stack.getType()) { + return false; + } + boolean haveMeta = stack.hasItemMeta(); + if (haveMeta != other.hasItemMeta()) { + return false; + } + if (!haveMeta) { + return true; + } + return Bukkit.getItemFactory().equals(meta.get(), other.getItemMeta()); + }).when(stack).isSimilar(any()); + + // Enchantments. + doAnswer(invocation -> { + ItemMeta existing = meta.get(); + return existing != null ? existing.getEnchants() : Map.of(); + }).when(stack).getEnchantments(); + doAnswer(invocation -> { + ItemMeta existing = meta.get(); + return existing == null ? 0 : existing.getEnchantLevel(invocation.getArgument(0)); + }).when(stack).getEnchantmentLevel(any()); + doAnswer(invocation -> { + ItemMeta existing = meta.get(); + return existing != null && existing.hasEnchant(invocation.getArgument(0)); + }).when(stack).containsEnchantment(any(Enchantment.class)); + Answer addEnchant = invocation -> { + get(meta, stack.getType(), true).ifPresent(itemMeta -> { + itemMeta.addEnchant( + invocation.getArgument(0), + invocation.getArgument(1), + // We aren't winning any performance prizes here, a beautiful DRY hack. + invocation.getMethod().getName().contains("Unsafe") + ); + }); + return null; + }; + doAnswer(addEnchant).when(stack).addEnchantment(any(Enchantment.class), anyInt()); + doAnswer(addEnchant).when(stack).addUnsafeEnchantment(any(Enchantment.class), anyInt()); + Answer addEnchants = invocation -> { + get(meta, stack.getType(), true).ifPresent(itemMeta -> { + // DRY hack again + boolean unsafe = invocation.getMethod().getName().contains("Unsafe"); + Map enchants = invocation.getArgument(0); + for (Map.Entry entry : enchants.entrySet()) { + itemMeta.addEnchant(entry.getKey(), entry.getValue(), unsafe); + } + }); + return null; + }; + doAnswer(addEnchants).when(stack).addEnchantments(any()); + doAnswer(addEnchants).when(stack).addUnsafeEnchantments(any()); + doAnswer(invocation -> { + ItemMeta existing = meta.get(); + if (existing != null) { + existing.removeEnchantments(); + } + return null; + }).when(stack).removeEnchantments(); + doAnswer(invocation -> { + ItemMeta existing = meta.get(); + if (existing != null) { + existing.removeEnchant(invocation.getArgument(0)); + } + return null; + }).when(stack).removeEnchantment(any()); + + return stack; + } + + private static @NotNull Optional get( + @NotNull AtomicReference container, + @Nullable Material createFor, + boolean store + ) { + ItemMeta itemMeta = container.get(); + if (itemMeta == null && createFor != null) { + itemMeta = Bukkit.getItemFactory().getItemMeta(createFor); + if (!store) { + return Optional.ofNullable(itemMeta); + } + container.set(itemMeta); + } + return Optional.ofNullable(itemMeta); + } + +} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/IsSimilarMatcher.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/IsSimilarMatcher.java deleted file mode 100644 index bb401ba..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/IsSimilarMatcher.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.matcher; - -import org.bukkit.inventory.ItemStack; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * A {@link org.hamcrest.Matcher Matcher} implementation for comparing {@link ItemStack ItemStacks} - * with {@link ItemStack#isSimilar(ItemStack)}. - */ -public class IsSimilarMatcher extends BaseMatcher { - - private final @NotNull ItemStack other; - - private IsSimilarMatcher(@NotNull ItemStack other) { - this.other = other; - } - - @Override - public boolean matches(@Nullable Object actual) { - return actual instanceof ItemStack actualItem && other.isSimilar(actualItem); - } - - @Override - public void describeTo(Description description) { - description.appendText(other.toString()); - } - - /** - * Construct a new {@code IsSimilarMatcher} for the given {@link ItemStack}. - * - * @param other the matchable item - * @return the resulting matcher - */ - public static @NotNull IsSimilarMatcher similar(@NotNull ItemStack other) { - return new IsSimilarMatcher(other); - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/ItemMatcher.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/ItemMatcher.java new file mode 100644 index 0000000..0685d50 --- /dev/null +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/ItemMatcher.java @@ -0,0 +1,106 @@ +package com.github.jikoo.enchantableblocks.mock.matcher; + +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class ItemMatcher { + + /** + * Construct a new {@code ItemEqualMatcher} for the given {@link ItemStack}. + * + * @param other the matchable item + * @return the resulting matcher + */ + public static BaseMatcher isItem(@NotNull ItemStack other) { + return new ItemEqualMatcher(other); + } + + private static class ItemEqualMatcher extends BaseMatcher { + + private final @NotNull ItemStack other; + + public ItemEqualMatcher(@NotNull ItemStack other) { + this.other = other; + } + + @Override + public boolean matches(Object actual) { + // Cannot use .equals because the backing ItemStack impl is a mock. + // Instead, leverage our isSimilar implementation and compare the one remaining value. + return actual instanceof ItemStack actualItem + && other.isSimilar(actualItem) + && other.getAmount() == actualItem.getAmount(); + } + + @Override + public void describeTo(Description description) { + description.appendText("item ").appendValue(other.toString()); + } + } + + /** + * Construct a new {@code IsSimilarMatcher} for the given {@link ItemStack}. + * + * @param other the matchable item + * @return the resulting matcher + */ + public static BaseMatcher isSimilar(@NotNull ItemStack other) { + return new IsSimilarMatcher(other); + } + + private static class IsSimilarMatcher extends BaseMatcher { + + private final @NotNull ItemStack other; + + private IsSimilarMatcher(@NotNull ItemStack other) { + this.other = other; + } + + @Override + public boolean matches(@Nullable Object actual) { + return actual instanceof ItemStack actualItem && other.isSimilar(actualItem); + } + + @Override + public void describeTo(Description description) { + description.appendText(other.toString()); + } + + } + + /** + * Construct a new {@code MetaIsEqualMatcher} for the given {@link ItemMeta}. + * + * @param other the matchable meta + * @return the resulting matcher + */ + public static BaseMatcher isMetaEqual(@NotNull ItemMeta other) { + return new MetaIsEqualMatcher(other); + } + + private static class MetaIsEqualMatcher extends BaseMatcher { + + private final @NotNull ItemMeta other; + + private MetaIsEqualMatcher(@NotNull ItemMeta other) { + this.other = other; + } + + @Override + public boolean matches(@Nullable Object actual) { + return actual instanceof ItemMeta actualMeta && Bukkit.getItemFactory().equals(other, actualMeta); + } + + @Override + public void describeTo(Description description) { + description.appendText(other.toString()); + } + + } + +} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java new file mode 100644 index 0000000..10e3e66 --- /dev/null +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java @@ -0,0 +1,186 @@ +package com.github.jikoo.enchantableblocks.mock.server; + +import com.github.jikoo.enchantableblocks.mock.inventory.ItemStackMocks; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.TypedKey; +import io.papermc.paper.registry.keys.tags.BannerPatternTagKeys; +import io.papermc.paper.registry.keys.tags.BiomeTagKeys; +import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; +import io.papermc.paper.registry.keys.tags.DamageTypeTagKeys; +import io.papermc.paper.registry.keys.tags.DialogTagKeys; +import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys; +import io.papermc.paper.registry.keys.tags.EntityTypeTagKeys; +import io.papermc.paper.registry.keys.tags.FluidTagKeys; +import io.papermc.paper.registry.keys.tags.GameEventTagKeys; +import io.papermc.paper.registry.keys.tags.InstrumentTagKeys; +import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; +import io.papermc.paper.registry.keys.tags.PaintingVariantTagKeys; +import io.papermc.paper.registry.keys.tags.StructureTagKeys; +import io.papermc.paper.registry.tag.Tag; +import io.papermc.paper.registry.tag.TagKey; +import org.bukkit.Art; +import org.bukkit.Fluid; +import org.bukkit.GameEvent; +import org.bukkit.Instrument; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Biome; +import org.bukkit.block.BlockType; +import org.bukkit.block.Structure; +import org.bukkit.block.banner.PatternType; +import org.bukkit.damage.DamageType; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +/** Helper class used to prevent class loading order issues when creating registry. */ +@SuppressWarnings("unchecked") +enum RegistryHelper { + ; + + private static final Map, RegistryKey> LEGACY_REGISTRIES = new HashMap<>(); + + static { + forEachRegistryClass(((field, clazz) -> { + try { + LEGACY_REGISTRIES.put(clazz, (RegistryKey) field.get(null)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + })); + } + + static RegistryKey getRegistryKey(Class clazz) { + return (RegistryKey) LEGACY_REGISTRIES.get(clazz); + } + + static T getOrThrow(Class clazz, NamespacedKey key) { + // Some classes (like BlockType and ItemType) have extra generics that will be + // erased during runtime calls. To ensure accurate typing, grab the constant's field. + // This approach also allows us to return null for unsupported keys. + Class constantClazz; + try { + constantClazz = (Class) clazz.getField( + key.getKey().toUpperCase(Locale.ROOT).replace('.', '_') + ).getType(); + } catch (ClassCastException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + T keyed = mock(constantClazz); + doReturn(key).when(keyed).getKey(); + doReturn(key).when(keyed).key(); + if (keyed instanceof ItemType itemType) { + mockItemType(itemType); + } + return keyed; + } + + static @Nullable Tag getTag(Class clazz, TagKey tagKey) { + Class source; + if (PatternType.class.isAssignableFrom(clazz)) { + source = BannerPatternTagKeys.class; + } else if (Biome.class.isAssignableFrom(clazz)) { + source = BiomeTagKeys.class; + } else if (BlockType.class.isAssignableFrom(clazz)) { + source = BlockTypeTagKeys.class; + } else if (DamageType.class.isAssignableFrom(clazz)) { + source = DamageTypeTagKeys.class; + } else if (Dialog.class.isAssignableFrom(clazz)) { + source = DialogTagKeys.class; + } else if (Enchantment.class.isAssignableFrom(clazz)) { + source = EnchantmentTagKeys.class; + } else if (EntityType.class.isAssignableFrom(clazz)) { + source = EntityTypeTagKeys.class; + } else if (Fluid.class.isAssignableFrom(clazz)) { + source = FluidTagKeys.class; + } else if (GameEvent.class.isAssignableFrom(clazz)) { + source = GameEventTagKeys.class; + } else if (Instrument.class.isAssignableFrom(clazz)) { + source = InstrumentTagKeys.class; + } else if (ItemType.class.isAssignableFrom(clazz)) { + source = ItemTypeTagKeys.class; + } else if (Art.class.isAssignableFrom(clazz)) { + source = PaintingVariantTagKeys.class; + } else if (Structure.class.isAssignableFrom(clazz)) { + source = StructureTagKeys.class; + } else { + // Throw rather than return null; if we're trying to use a tag that we can't verify + // odds are on that it's a new type rather than an invalid tag. + throw new UnsupportedOperationException(""); + } + + if (!hasField(tagKey.key().value(), source, TagKey.class, clazz)) { + return null; + } + + Tag tag = mock(); + doReturn(tagKey).when(tag).tagKey(); + doReturn(Set.of()).when(tag).values(); + doAnswer(invocationIsTagged -> { + TypedKey keyed = invocationIsTagged.getArgument(0); + // Since these are mocks, the exact instance might not be equal. Consider equal keys equal. + return tag.values().contains(keyed) || tag.values().stream().anyMatch(value -> value.key().equals(keyed.key())); + }).when(tag).contains(notNull()); + return tag; + } + + static void forEachRegistryClass(BiConsumer> consumer) { + for (Field field : RegistryKey.class.getFields()) { + if (field.getType() == RegistryKey.class) { + Class clazz = (Class) getClass(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]); + consumer.accept(field, clazz); + } + } + } + + private static Class getClass(Type type) { + if (type instanceof Class clazz) { + return clazz; + } else if (type instanceof ParameterizedType parameterized) { + return (Class) parameterized.getRawType(); + } else { + throw new UnsupportedOperationException("Unsupported type " + type.getClass()); + } + } + + private static void mockItemType(@NotNull ItemType itemType) { + // ItemStack creation. + doAnswer(invocation -> itemType.createItemStack(1) + ).when(itemType).createItemStack(); + doAnswer(invocation -> ItemStackMocks.newItemMock(itemType, invocation.getArgument(0))) + .when(itemType).createItemStack(anyInt()); + } + + private static boolean hasField(String fieldName, Class clazz, Class type, Class generic) { + fieldName = fieldName.toUpperCase().replaceAll("\\W", "_"); + try { + Field field = clazz.getDeclaredField(fieldName); + if (!type.equals(field.getType())) { + return false; + } + Class genericClazz = getClass(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]); + return generic.equals(genericClazz); + } catch (NoSuchFieldException e) { + return false; + } + } + +} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java new file mode 100644 index 0000000..b35681e --- /dev/null +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java @@ -0,0 +1,93 @@ +package com.github.jikoo.enchantableblocks.mock.server; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.tag.Tag; +import io.papermc.paper.registry.tag.TagKey; +import net.kyori.adventure.key.Key; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mockito.stubbing.Answer; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class TestRegistryAccess implements RegistryAccess { + + private static final Map, Registry> REGISTERS = new HashMap<>(); + + @Override + @Deprecated(since = "1.20.6", forRemoval = true) + public @Nullable Registry<@NotNull T> getRegistry(@NotNull Class type) { + return getRegistry(RegistryHelper.getRegistryKey(type)); + } + + @SuppressWarnings("unchecked") + @Override + public @NotNull Registry getRegistry(@NotNull RegistryKey registryKey) { + return (Registry) REGISTERS.computeIfAbsent(registryKey, regKey -> { + Registry registry = mock(); + + AtomicReference> clazzRef = new AtomicReference<>(); + RegistryHelper.forEachRegistryClass((BiConsumer>) (field, clazz) -> { + if (RegistryHelper.getRegistryKey(clazz).equals(regKey)) { + clazzRef.set(clazz); + } + }); + + Map values = new HashMap<>(); + Class clazz = clazzRef.get(); + + // Paper added several helpers and then moved ItemType to them. + // Mock these first so Mockito doesn't try to clobber the original overload + // because NamespacedKey extends Key. + doAnswer(invocation -> { + Key key = invocation.getArgument(0); + return registry.get(Objects.requireNonNull(NamespacedKey.fromString(key.asString()))); + }).when(registry).get(any(Key.class)); + doAnswer(invocation -> { + Key key = invocation.getArgument(0); + return registry.getOrThrow(Objects.requireNonNull(NamespacedKey.fromString(key.asString()))); + }).when(registry).getOrThrow(any(Key.class)); + + Answer getOrThrow = invocationGetEntry -> { + NamespacedKey key = invocationGetEntry.getArgument(0); + return values.computeIfAbsent(key, key1 -> RegistryHelper.getOrThrow(clazz, key1)); + }; + + doAnswer(getOrThrow).when(registry).getOrThrow(any(NamespacedKey.class)); + // For get, return null for nonexistant constants. + doAnswer(invocation -> { + try { + return getOrThrow.answer(invocation); + } catch (RuntimeException e) { + if (e.getCause() instanceof NoSuchFieldException) { + return null; + } + throw e; + } + }).when(registry).get(any(NamespacedKey.class)); + + Map, Tag> tags = new HashMap<>(); + + doAnswer(invocation -> { + TagKey tagKey = invocation.getArgument(0); + return tags.computeIfAbsent(tagKey, key -> RegistryHelper.getTag(clazz, key)); + }).when(registry).getTag(any(TagKey.class)); + + return registry; + }); + } + +} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java new file mode 100644 index 0000000..8bc1fcc --- /dev/null +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java @@ -0,0 +1,62 @@ +package com.github.jikoo.enchantableblocks.mock.server; + +import io.papermc.paper.ServerBuildInfo; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.util.Optional; +import java.util.OptionalInt; + +public class TestServerBuildInfo implements ServerBuildInfo { + + @Override + public @NotNull Key brandId() { + return Key.key("dummyserver:dummy"); + } + + @Override + public boolean isBrandCompatible(@NotNull Key brandId) { + return true; + } + + @Override + public @NotNull String brandName() { + return "dummyserver"; + } + + @Override + public @NotNull String minecraftVersionId() { + return "1"; + } + + @Override + public @NotNull String minecraftVersionName() { + return "1.2.3"; + } + + @Override + public @NotNull OptionalInt buildNumber() { + return OptionalInt.empty(); + } + + @Override + public @NotNull Instant buildTime() { + return Instant.now(); + } + + @Override + public @NotNull Optional gitBranch() { + return Optional.empty(); + } + + @Override + public @NotNull Optional gitCommit() { + return Optional.empty(); + } + + @Override + public @NotNull String asString(@NotNull StringRepresentation representation) { + return ""; + } +} diff --git a/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo new file mode 100644 index 0000000..655b0c3 --- /dev/null +++ b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo @@ -0,0 +1 @@ +com.github.jikoo.enchantableblocks.mock.server.TestServerBuildInfo diff --git a/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess b/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess new file mode 100644 index 0000000..c210470 --- /dev/null +++ b/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess @@ -0,0 +1 @@ +com.github.jikoo.enchantableblocks.mock.server.TestRegistryAccess From eff491af2986e36293aabe370d9971b4634dc4ce Mon Sep 17 00:00:00 2001 From: Jikoo Date: Fri, 30 Jan 2026 11:51:48 -0500 Subject: [PATCH 03/20] Update PlanarEnchanting --- pom.xml | 7 +-- .../listener/AnvilEnchanter.java | 14 ++--- .../util/enchant/BlockAnvil.java | 49 +++++++++++++++ .../util/enchant/BlockAnvilBehavior.java | 54 ++++++++++++++++ .../util/enchant/BlockAnvilOperation.java | 63 ------------------- ...nTest.java => BlockAnvilBehaviorTest.java} | 56 ++++++++++------- 6 files changed, 142 insertions(+), 101 deletions(-) create mode 100644 src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java create mode 100644 src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java delete mode 100644 src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperation.java rename src/test/java/com/github/jikoo/enchantableblocks/util/enchant/{BlockAnvilOperationTest.java => BlockAnvilBehaviorTest.java} (77%) diff --git a/pom.xml b/pom.xml index 903e737..6b0b61b 100644 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ com.github.jikoo planarenchanting - 2.0.1 + fe946163f8 compile @@ -162,11 +162,6 @@ true - com.github.jikoo:* diff --git a/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java b/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java index 7f7c5f1..94e61d2 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java @@ -1,7 +1,7 @@ package com.github.jikoo.enchantableblocks.listener; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockRegistry; -import com.github.jikoo.enchantableblocks.util.enchant.BlockAnvilOperation; +import com.github.jikoo.enchantableblocks.util.enchant.BlockAnvil; import com.github.jikoo.planarenchanting.anvil.AnvilResult; import com.github.jikoo.planarenchanting.util.ItemUtil; import org.bukkit.Material; @@ -9,7 +9,6 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.inventory.PrepareAnvilEvent; -import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Contract; @@ -39,7 +38,8 @@ public AnvilEnchanter(@NotNull Plugin plugin, @NotNull EnchantableBlockRegistry @EventHandler(priority = EventPriority.HIGH) @VisibleForTesting void onPrepareAnvil(@NotNull PrepareAnvilEvent event) { - var clicker = event.getView().getPlayer(); + var view = event.getView(); + var clicker = view.getPlayer(); var inventory = event.getInventory(); var base = inventory.getItem(0); var addition = inventory.getItem(1); @@ -55,8 +55,8 @@ void onPrepareAnvil(@NotNull PrepareAnvilEvent event) { return; } - var operation = new BlockAnvilOperation(registration, clicker.getWorld().getName()); - final var result = operation.apply(inventory); + var operation = new BlockAnvil(registration, clicker.getWorld().getName()); + final var result = operation.getResult(view); if (result == AnvilResult.EMPTY) { return; @@ -78,9 +78,7 @@ void onPrepareAnvil(@NotNull PrepareAnvilEvent event) { inventory.setItem(2, resultItem); // Set repair cost. As vanilla has no result for our combinations, this is always set to 0 // after the event has completed and needs to be set again. - inventory.setRepairCost(result.levelCost()); - // Update level cost window property again just to be safe. - clicker.setWindowProperty(InventoryView.Property.REPAIR_COST, result.levelCost()); + view.setRepairCost(result.levelCost()); }); } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java new file mode 100644 index 0000000..e39bda5 --- /dev/null +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java @@ -0,0 +1,49 @@ +package com.github.jikoo.enchantableblocks.util.enchant; + +import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; +import com.github.jikoo.planarenchanting.anvil.Anvil; +import com.github.jikoo.planarenchanting.anvil.AnvilFunctions; +import com.github.jikoo.planarenchanting.anvil.AnvilResult; +import com.github.jikoo.planarenchanting.anvil.AnvilState; +import org.bukkit.inventory.view.AnvilView; +import org.jetbrains.annotations.NotNull; + +/** + * A simplified {@link Anvil} designed for use with + * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock} implementations. Unlike a + * normal operation, this operation does not validate items! + */ +public class BlockAnvil extends Anvil { + + /** + * A simplified {@link Anvil} designed for use with + * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock} implementations. + * + * @param registration the {@link EnchantableRegistration} for the block + * @param worldName the name of the world the operation is applied in + */ + public BlockAnvil( + @NotNull EnchantableRegistration registration, + @NotNull String worldName) { + super(new BlockAnvilBehavior(registration, worldName)); + } + + @Override + public @NotNull AnvilResult getResult(@NotNull AnvilView view) { + var state = new AnvilState(view); + // Base and addition have already been validated. + + // Apply base cost. + apply(state, AnvilFunctions.PRIOR_WORK_LEVEL_COST); + + // Apply the rename function first, then update prior work cost - both update prior work. + apply(state, AnvilFunctions.RENAME); + apply(state, AnvilFunctions.UPDATE_PRIOR_WORK_COST); + + // Combine enchantments. + apply(state, AnvilFunctions.COMBINE_ENCHANTMENTS_JAVA_EDITION); + + return forge(state); + } + +} diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java new file mode 100644 index 0000000..a5f9094 --- /dev/null +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java @@ -0,0 +1,54 @@ +package com.github.jikoo.enchantableblocks.util.enchant; + +import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; +import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; +import com.github.jikoo.planarenchanting.anvil.AnvilBehavior; +import com.github.jikoo.planarenchanting.util.MetaCachedStack; +import com.google.common.collect.Multimap; +import org.bukkit.enchantments.Enchantment; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +public class BlockAnvilBehavior implements AnvilBehavior { + + private final @NotNull EnchantableBlockConfig config; + private final @NotNull String worldName; + private final @NotNull Collection enchants; + + public BlockAnvilBehavior( + @NotNull EnchantableRegistration registration, + @NotNull String worldName + ) { + this.config = registration.getConfig(); + this.worldName = worldName; + this.enchants = registration.getEnchants(); + } + + @Override + public boolean enchantApplies(@NotNull Enchantment enchantment, @NotNull MetaCachedStack base) { + return enchants.contains(enchantment) && !config.anvilDisabledEnchants().get(worldName).contains(enchantment); + } + + @Override + public boolean enchantsConflict(@NotNull Enchantment enchant1, @NotNull Enchantment enchant2) { + Multimap conflicts = config.anvilEnchantmentConflicts().get(worldName); + return conflicts.containsEntry(enchant1, enchant2) || conflicts.containsEntry(enchant2, enchant1); + } + + @Override + public int getEnchantMaxLevel(@NotNull Enchantment enchantment) { + return config.anvilEnchantmentMax().get(worldName, enchantment); + } + + @Override + public boolean itemsCombineEnchants(@NotNull MetaCachedStack base, @NotNull MetaCachedStack addition) { + return true; + } + + @Override + public boolean itemRepairedBy(@NotNull MetaCachedStack repaired, @NotNull MetaCachedStack repairMat) { + return false; + } + +} diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperation.java b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperation.java deleted file mode 100644 index 441a1df..0000000 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperation.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.jikoo.enchantableblocks.util.enchant; - -import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; -import com.github.jikoo.planarenchanting.anvil.AnvilFunction; -import com.github.jikoo.planarenchanting.anvil.AnvilOperation; -import com.github.jikoo.planarenchanting.anvil.AnvilOperationState; -import com.github.jikoo.planarenchanting.anvil.AnvilResult; -import java.util.ArrayList; -import org.bukkit.inventory.AnvilInventory; -import org.jetbrains.annotations.NotNull; - -/** - * A simplified {@link AnvilOperation} designed for use with - * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock} implementations. Unlike a - * normal operation, this operation does not validate items! - */ -public class BlockAnvilOperation extends AnvilOperation { - - /** - * A simplified {@link AnvilOperation} designed for use with - * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock} implementations. - * - * @param registration the {@link EnchantableRegistration} for the block - * @param worldName the name of the world the operation is applied in - */ - public BlockAnvilOperation( - @NotNull EnchantableRegistration registration, - @NotNull String worldName) { - // Set world allowed enchantments. - var enchantments = new ArrayList<>(registration.getEnchants()); - var config = registration.getConfig(); - enchantments.removeAll(config.anvilDisabledEnchants().get(worldName)); - this.setEnchantApplies((enchantment, itemStack) -> enchantments.contains(enchantment)); - // Set world enchantment conflicts. - var enchantConflicts = config.anvilEnchantmentConflicts().get(worldName); - this.setEnchantsConflict((enchantment, enchantment2) -> - enchantConflicts.get(enchantment).contains(enchantment2) - || enchantConflicts.get(enchantment2).contains(enchantment)); - // Set world max levels. - this.setEnchantMaxLevel(enchantment -> config.anvilEnchantmentMax().get(worldName, enchantment)); - // Combination validation is handled before calling #apply. - this.setItemsCombineEnchants((a, b) -> true); - } - - @Override - public @NotNull AnvilResult apply(@NotNull AnvilInventory inventory) { - var state = new AnvilOperationState(this, inventory); - // Base and addition have already been validated. - - // Apply base cost. - state.apply(AnvilFunction.PRIOR_WORK_LEVEL_COST); - - // Apply the rename function first, then update prior work cost - both update prior work. - state.apply(AnvilFunction.RENAME); - state.apply(AnvilFunction.UPDATE_PRIOR_WORK_COST); - - // Combine enchantments. - state.apply(AnvilFunction.COMBINE_ENCHANTMENTS_JAVA_EDITION); - - return state.forge(); - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperationTest.java b/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java similarity index 77% rename from src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperationTest.java rename to src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java index a958d94..37c5580 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilOperationTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java @@ -1,22 +1,14 @@ package com.github.jikoo.enchantableblocks.util.enchant; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; +import com.github.jikoo.planarenchanting.util.MetaCachedStack; import com.github.jikoo.planarwrappers.config.Mapping; import com.github.jikoo.planarwrappers.config.Setting; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import java.util.Set; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; @@ -26,9 +18,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -@DisplayName("Feature: Configurable AnvilOperation for EnchantableBlocks.") +import java.util.Set; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@DisplayName("Feature: Configurable AnvilBehavior for EnchantableBlocks.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class BlockAnvilOperationTest { +public class BlockAnvilBehaviorTest { private EnchantableRegistration registration; @@ -54,30 +56,30 @@ void beforeEach() { } @Test - void testEnchantApplies() { + void enchantApplies() { doReturn(Set.of(Enchantment.UNBREAKING, Enchantment.EFFICIENCY)).when(registration).getEnchants(); var anvilDisabledEnchants = registration.getConfig().anvilDisabledEnchants(); doReturn(Set.of(Enchantment.UNBREAKING)).when(anvilDisabledEnchants).get(anyString()); - var op = new BlockAnvilOperation(registration, "sample_text"); + var op = new BlockAnvilBehavior(registration, "sample_text"); assertThat( "Enabled enchantment applies", - op.enchantApplies(Enchantment.EFFICIENCY, new ItemStack(Material.AIR))); + op.enchantApplies(Enchantment.EFFICIENCY, new MetaCachedStack(new ItemStack(Material.AIR)))); assertThat( "Disabled enchantment does not apply", - op.enchantApplies(Enchantment.UNBREAKING, new ItemStack(Material.AIR)), + op.enchantApplies(Enchantment.UNBREAKING, new MetaCachedStack(new ItemStack(Material.AIR))), is(false)); } @Test - void testEnchantsConflict() { + void enchantsConflict() { var anvilEnchantmentConflicts = registration.getConfig().anvilEnchantmentConflicts(); Multimap conflicts = HashMultimap.create(); conflicts.put(Enchantment.UNBREAKING, Enchantment.EFFICIENCY); doReturn(conflicts).when(anvilEnchantmentConflicts).get(anyString()); - var op = new BlockAnvilOperation(registration, "sample_text"); + var op = new BlockAnvilBehavior(registration, "sample_text"); assertThat( "Enchantments conflict", @@ -96,7 +98,7 @@ void testEnchantsConflict() { } @Test - void testEnchantMaxLevel() { + void enchantMaxLevel() { var anvilEnchantmentMax = registration.getConfig().anvilEnchantmentMax(); int genericEnchantLevel = 0; doReturn(genericEnchantLevel).when(anvilEnchantmentMax).get(anyString(), any()); @@ -104,7 +106,7 @@ void testEnchantMaxLevel() { Enchantment specifiedEnchant = Enchantment.UNBREAKING; doReturn(specificEnchantLevel).when(anvilEnchantmentMax).get(anyString(), eq(specifiedEnchant)); - var op = new BlockAnvilOperation(registration, "sample_text"); + var op = new BlockAnvilBehavior(registration, "sample_text"); assertThat( "Specified enchantment max is provided", @@ -117,13 +119,19 @@ void testEnchantMaxLevel() { } @Test - void testItemsCombineEnchants() { - var op = new BlockAnvilOperation(registration, "sample_text"); + void itemsCombineEnchants() { + var op = new BlockAnvilBehavior(registration, "sample_text"); assertThat( "Items always combine", - op.itemsCombineEnchants(new ItemStack(Material.AIR), new ItemStack(Material.AIR))); + op.itemsCombineEnchants(new MetaCachedStack(new ItemStack(Material.AIR)), new MetaCachedStack(new ItemStack(Material.AIR)))); } - // Application handled by AnvilEnchanterTest. + @Test + void itemRepairedBy() { + var op = new BlockAnvilBehavior(registration, "sample_text"); + assertThat( + "Item is never repairable", + !op.itemRepairedBy(new MetaCachedStack(new ItemStack(Material.AIR)), new MetaCachedStack(new ItemStack(Material.AIR)))); + } -} \ No newline at end of file +} From 1b4f7e0ff46f52b01d82f493342b70f6f8214f4c Mon Sep 17 00:00:00 2001 From: Jikoo Date: Fri, 30 Jan 2026 12:02:04 -0500 Subject: [PATCH 04/20] Fix dynamic agent loading warning --- pom.xml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b0b61b..7c91e15 100644 --- a/pom.xml +++ b/pom.xml @@ -15,13 +15,14 @@ UTF-8 21 8.5.18 + + sonarcloud - 21 Jikoo_EnchantableBlocks jikoo https://sonarcloud.io @@ -141,6 +142,17 @@ maven-compiler-plugin 3.14.1 + + org.apache.maven.plugins + maven-dependency-plugin + + + + properties + + + + org.apache.maven.plugins maven-surefire-plugin @@ -148,6 +160,7 @@ 2 false + @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off From f171bae4e8e06a0bd454f4a664b7a372ecbf4e33 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Fri, 30 Jan 2026 12:02:47 -0500 Subject: [PATCH 05/20] Fix most erroring tests Still failures, and overall not happy with the actual test layouts, but that'll come later. --- .../block/EnchantableBlockTest.java | 36 +- .../EnchantableFurnaceRegistrationTest.java | 57 +- .../impl/furnace/EnchantableFurnaceTest.java | 55 +- .../impl/furnace/FurnaceListenerTest.java | 48 +- .../listener/WorldListenerTest.java | 36 +- .../mock/enchantments/EnchantmentMocks.java | 512 ++++++++++++++---- .../registry/EnchantableBlockManagerTest.java | 53 +- 7 files changed, 553 insertions(+), 244 deletions(-) diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java index 89c9346..e845f05 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java @@ -1,31 +1,17 @@ package com.github.jikoo.enchantableblocks.block; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; +import com.github.jikoo.enchantableblocks.mock.inventory.ItemStackMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import com.jparams.verifier.tostring.ToStringVerifier; import com.jparams.verifier.tostring.preset.Presets; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Set; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -33,6 +19,23 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Enchantable blocks.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableBlockTest { @@ -195,6 +198,7 @@ void testToString() { }) .withPreset(Presets.INTELLI_J) .withIgnoredFields("registration", "storage", "dirty", "updating") + .withValueProvider(ItemStack.class, path -> ItemStackMocks.newItemMock(ItemType.AIR, 1)) .withFailOnExcludedFields(true).verify(); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java index 80dd569..9d51b65 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java @@ -1,30 +1,9 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; -import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.CoreMatchers.both; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Stream; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -46,6 +25,7 @@ import org.bukkit.inventory.SmokingRecipe; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -54,6 +34,29 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.both; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Registration for EnchantableFurnace.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableFurnaceRegistrationTest { @@ -73,6 +76,7 @@ void beforeAll() { plugin = mock(Plugin.class); when(plugin.getName()).thenReturn(getClass().getSimpleName()); + doReturn(plugin.getName().toLowerCase()).when(plugin).namespace(); when(plugin.getConfig()).thenReturn(new YamlConfiguration()); when(plugin.getServer()).thenReturn(server); var logger = mock(Logger.class); @@ -208,7 +212,16 @@ void testGetFurnaceRecipe(ItemStack item) { static Stream getModernItems() { return Stream.of(Material.values()) .filter(material -> !material.name().startsWith("LEGACY_") && material != Material.AIR && material.isItem()) - .map(ItemStack::new); + .map(material -> new ItemStack(material) { + @Override + public @NotNull ItemStack clone() { + super.clone(); // Shh, IDE. + // Because these are backed by mocks, each clone results in a new backing mock. + // As mocks override equals, toString, and hashCode, we cannot mock those. + // Instead, we bypass the attempt to not alter the original when getting the cache key. + return this; + } + }); } @DisplayName("Recipe lookup ignores null tile.") diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java index 3953abf..e08d975 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java @@ -1,29 +1,11 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; -import static com.github.jikoo.enchantableblocks.mock.matcher.IsSimilarMatcher.similar; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyShort; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.block.EnchantableBlock; import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import com.github.jikoo.planarwrappers.util.StringConverters; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; @@ -36,6 +18,7 @@ import org.bukkit.inventory.FurnaceInventory; import org.bukkit.inventory.FurnaceRecipe; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; import org.bukkit.inventory.RecipeChoice; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; @@ -53,6 +36,25 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.github.jikoo.enchantableblocks.mock.matcher.ItemMatcher.isSimilar; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyShort; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Enchantable furnaces.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableFurnaceTest { @@ -74,14 +76,14 @@ void beforeAll() { recipe = new FurnaceRecipe( Objects.requireNonNull(StringConverters.toNamespacedKey("sample:text")), - new ItemStack(Material.COARSE_DIRT), Material.DIRT, 0, 200); + ItemType.COARSE_DIRT.createItemStack(), Material.DIRT, 0, 200); } @BeforeEach void beforeEach() { reg = mock(EnchantableFurnaceRegistration.class); block = mock(Block.class); - itemStack = new ItemStack(Material.FURNACE); + itemStack = ItemType.FURNACE.createItemStack(); storage = mock(ConfigurationSection.class); // Set up matching recipe @@ -124,8 +126,6 @@ void beforeEach() { void testConstructorLegacyData() { short legacyFrozenTicks = 200; itemStack.addUnsafeEnchantment(Enchantment.SILK_TOUCH, legacyFrozenTicks); - itemStack = spy(itemStack); - when(itemStack.clone()).thenAnswer(invocation -> spy(invocation.callRealMethod())); var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage); assertThat("Data needs saving", enchantableFurnace.isDirty()); @@ -137,8 +137,6 @@ void testConstructorLegacyData() { @Test void testConstructorNewData() { itemStack.addUnsafeEnchantment(Enchantment.SILK_TOUCH, 1); - itemStack = spy(itemStack); - when(itemStack.clone()).thenAnswer(invocation -> spy(invocation.callRealMethod())); var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage); assertThat("Data needs saving", enchantableFurnace.isDirty()); @@ -150,9 +148,6 @@ void testConstructorNewData() { @DisplayName("New data does not fetch silk level") @Test void testConstructorNewNonSilk() { - itemStack = spy(itemStack); - when(itemStack.clone()).thenAnswer(invocation -> spy(invocation.callRealMethod())); - var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage); assertThat("Data needs saving", enchantableFurnace.isDirty()); assertThat("Non-silk cannot pause", enchantableFurnace.canPause(), is(false)); @@ -162,8 +157,6 @@ void testConstructorNewNonSilk() { @DisplayName("Existing data is used") @Test void testConstructorExistingData() { - itemStack = spy(itemStack); - when(itemStack.clone()).thenAnswer(invocation -> spy(invocation.callRealMethod())); storage = new YamlConfiguration(); storage.set("silk.enabled", true); short frozenTicks = 200; @@ -356,7 +349,7 @@ void testShouldPauseNonmatching() { var tile = setUpTile(); var inv = tile.getInventory(); ItemStack input = new ItemStack(Material.FURNACE); - assertThat("Input is not similar to recipe input", input, not(similar(recipe.getInput()))); + assertThat("Input is not similar to recipe input", input, not(isSimilar(recipe.getInput()))); inv.setSmelting(input); inv.setResult(recipe.getResult()); @@ -438,7 +431,7 @@ void testShouldPauseDissimilarResult() { var inv = tile.getInventory(); inv.setSmelting(recipe.getInput()); ItemStack result = new ItemStack(Material.DIAMOND); - assertThat("Result must be dissimilar", result, not(similar(recipe.getResult()))); + assertThat("Result must be dissimilar", result, not(isSimilar(recipe.getResult()))); inv.setResult(result); assertThat("Furnace with dissimilar result should pause", enchantableFurnace.shouldPause(null)); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java index c3e7abd..12efb9e 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java @@ -1,21 +1,5 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; -import static com.github.jikoo.enchantableblocks.mock.matcher.IsSimilarMatcher.similar; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyDouble; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; @@ -24,11 +8,6 @@ import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockRegistry; import com.github.jikoo.planarwrappers.util.StringConverters; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.IntSupplier; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; @@ -64,6 +43,28 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntSupplier; + +import static com.github.jikoo.enchantableblocks.mock.matcher.ItemMatcher.isSimilar; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Event handlers for enchantable furnaces.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class FurnaceListenerTest { @@ -152,7 +153,8 @@ void beforeEach() { when(player.getOpenInventory()).thenReturn(viewMock); var playerInventory = InventoryMocks.newMock(PlayerInventory.class, InventoryType.PLAYER, 41); - when(playerInventory.getItemInMainHand()).thenReturn(new ItemStack(Material.AIR)); + ItemStack air = new ItemStack(Material.AIR); + when(playerInventory.getItemInMainHand()).thenReturn(air); when(viewMock.getTopInventory()).thenReturn(playerInventory); // Set up open/close for modifications. @@ -281,7 +283,7 @@ void testApplyFortuneBelowOne(int value) { void testApplyFortunePositive(int value) { var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); listener.applyFortune(event, () -> value); - assertThat("Result must be similar", event.getResult(), similar(recipe.getResult())); + assertThat("Result must be similar", event.getResult(), isSimilar(recipe.getResult())); assertThat("Result must be modified", event.getResult(), is(not(recipe.getResult()))); assertThat("Result amount must be increased as expected", event.getResult().getAmount(), is(value + 1)); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java index 0f1799d..e9c53c7 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java @@ -1,26 +1,10 @@ package com.github.jikoo.enchantableblocks.listener; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import com.google.common.base.Preconditions; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; @@ -52,6 +36,23 @@ import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentCaptor; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Listen for world events.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WorldListenerTest { @@ -86,7 +87,8 @@ void setUp() { when(player.getWorld()).thenReturn(world); var inventory = mock(PlayerInventory.class); - when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.DIAMOND_PICKAXE)); + ItemStack stack = new ItemStack(Material.DIAMOND_PICKAXE); + when(inventory.getItemInMainHand()).thenReturn(stack); when(player.getInventory()).thenReturn(inventory); var scheduler = mock(BukkitScheduler.class); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java index 8958c09..005777d 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java @@ -1,113 +1,335 @@ package com.github.jikoo.enchantableblocks.mock.enchantments; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import org.bukkit.Material; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.TypedKey; +import io.papermc.paper.registry.keys.EnchantmentKeys; +import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys; +import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; +import io.papermc.paper.registry.tag.Tag; +import io.papermc.paper.registry.tag.TagKey; import org.bukkit.NamespacedKey; -import org.bukkit.Tag; +import org.bukkit.Registry; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; +import org.mockito.ArgumentMatchers; -public class EnchantmentMocks { +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.IntUnaryOperator; - private static final Tag TAG_EMPTY = new Tag<>() { - @Override - public boolean isTagged(@NotNull Material item) { - return false; - } +import static org.bukkit.enchantments.Enchantment.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; - @NotNull - @Override - public Set getValues() { - return Set.of(); - } +public class EnchantmentMocks { - @NotNull - @Override - public NamespacedKey getKey() { - return Objects.requireNonNull(NamespacedKey.fromString("mock:empty")); - } - }; + private static final Map KEYS_TO_ENCHANTS = new HashMap<>(); + private static final Set> ENCHANTING_TABLE_TAGS = new HashSet<>(); public static void init() { - List protections = List.of(Enchantment.PROTECTION, Enchantment.FIRE_PROTECTION, Enchantment.BLAST_PROTECTION, Enchantment.PROJECTILE_PROTECTION); - setUpEnchant(Enchantment.PROTECTION, 4, Tag.ITEMS_ENCHANTABLE_ARMOR, protections); - setUpEnchant(Enchantment.FIRE_PROTECTION, 4, Tag.ITEMS_ENCHANTABLE_ARMOR, protections); - setUpEnchant(Enchantment.FEATHER_FALLING, 4, Tag.ITEMS_ENCHANTABLE_FOOT_ARMOR); - setUpEnchant(Enchantment.BLAST_PROTECTION, 4, Tag.ITEMS_ENCHANTABLE_ARMOR, protections); - setUpEnchant(Enchantment.PROJECTILE_PROTECTION, 4, Tag.ITEMS_ENCHANTABLE_ARMOR, protections); + // See net.minecraft.world.item.enchantment.Enchantments + config(PROTECTION) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) + .maxLevel(4) + .minModCost(perLvl(1, 11)) + .maxModCost(perLvl(12, 11)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); + config(FIRE_PROTECTION) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) + .weight(5) + .maxLevel(4) + .minModCost(perLvl(10, 8)) + .maxModCost(perLvl(18, 8)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); + config(FEATHER_FALLING) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) + .weight(5) + .maxLevel(4) + .minModCost(perLvl(5, 6)) + .maxModCost(perLvl(11, 6)); + config(BLAST_PROTECTION) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) + .weight(2) + .maxLevel(4) + .minModCost(perLvl(5, 8)) + .maxModCost(perLvl(13, 8)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); + config(PROJECTILE_PROTECTION) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) + .weight(5) + .maxLevel(4) + .minModCost(perLvl(3, 6)) + .maxModCost(perLvl(9, 6)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); - setUpEnchant(Enchantment.RESPIRATION, 3, Tag.ITEMS_ENCHANTABLE_HEAD_ARMOR); - setUpEnchant(Enchantment.AQUA_AFFINITY, 1, Tag.ITEMS_ENCHANTABLE_HEAD_ARMOR); + config(RESPIRATION) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_HEAD_ARMOR) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(10, 10)) + .maxModCost(perLvl(40, 10)); + config(AQUA_AFFINITY) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_HEAD_ARMOR) + .weight(2) + .maxLevel(1) + .minModCost(flat(1)) + .maxModCost(flat(41)); - setUpEnchant(Enchantment.THORNS, 3, Tag.ITEMS_ENCHANTABLE_ARMOR); + config(THORNS) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CHEST_ARMOR) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) + .weight(1) + .maxLevel(3) + .minModCost(perLvl(10, 20)) + .maxModCost(perLvl(60, 20)); - setUpEnchant(Enchantment.DEPTH_STRIDER, 3, Tag.ITEMS_ENCHANTABLE_FOOT_ARMOR, List.of(Enchantment.FROST_WALKER)); - setUpEnchant(Enchantment.FROST_WALKER, 3, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_FOOT_ARMOR, List.of(Enchantment.DEPTH_STRIDER)); - setUpEnchant(Enchantment.SOUL_SPEED, 3, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_FOOT_ARMOR, List.of()); - setUpEnchant(Enchantment.SWIFT_SNEAK, 3, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_LEG_ARMOR, List.of()); + config(DEPTH_STRIDER) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(10, 10)) + .maxModCost(perLvl(25, 10)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_BOOTS); + config(FROST_WALKER) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) + .weight(2) + .maxLevel(2) + .minModCost(perLvl(10, 10)) + .maxModCost(perLvl(25, 10)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_BOOTS); - setUpEnchant(Enchantment.BINDING_CURSE, 1, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_EQUIPPABLE, List.of()); + config(BINDING_CURSE) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_EQUIPPABLE) + .weight(1) + .minModCost(flat(25)) + .maxModCost(flat(50)); - setUpEnchant(Enchantment.SHARPNESS, 5, Tag.ITEMS_ENCHANTABLE_SWORD, Tag.ITEMS_ENCHANTABLE_SHARP_WEAPON, List.of(Enchantment.BANE_OF_ARTHROPODS, Enchantment.SMITE)); - setUpEnchant(Enchantment.SMITE, 5, Tag.ITEMS_ENCHANTABLE_SWORD, Tag.ITEMS_ENCHANTABLE_WEAPON, List.of(Enchantment.SHARPNESS, Enchantment.BANE_OF_ARTHROPODS)); - setUpEnchant(Enchantment.BANE_OF_ARTHROPODS, 5, Tag.ITEMS_ENCHANTABLE_SWORD, Tag.ITEMS_ENCHANTABLE_WEAPON, List.of(Enchantment.SHARPNESS, Enchantment.SMITE)); - setUpEnchant(Enchantment.KNOCKBACK, 2, Tag.ITEMS_ENCHANTABLE_SWORD); - setUpEnchant(Enchantment.FIRE_ASPECT, 2, Tag.ITEMS_ENCHANTABLE_FIRE_ASPECT); - setUpEnchant(Enchantment.LOOTING, 3, Tag.ITEMS_ENCHANTABLE_SWORD); - setUpEnchant(Enchantment.SWEEPING_EDGE, 3, Tag.ITEMS_ENCHANTABLE_SWORD); + config(SOUL_SPEED) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) + .weight(1) + .maxLevel(3) + .minModCost(perLvl(10, 10)) + .maxModCost(perLvl(25, 10)); + config(SWIFT_SNEAK) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_LEG_ARMOR) + .weight(1) + .maxLevel(3) + .minModCost(perLvl(25, 25)) + .maxModCost(perLvl(75, 25)); - setUpEnchant(Enchantment.EFFICIENCY, 5, Tag.ITEMS_ENCHANTABLE_MINING); - setUpEnchant(Enchantment.SILK_TOUCH, 1, Tag.ITEMS_ENCHANTABLE_MINING_LOOT, List.of(Enchantment.FORTUNE)); + config(SHARPNESS) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_SHARP_WEAPON) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) + .maxLevel(5) + .minModCost(perLvl(1, 11)) + .maxModCost(perLvl(21, 11)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); + config(SMITE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_WEAPON) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) + .weight(5) + .maxLevel(5) + .minModCost(perLvl(5, 8)) + .maxModCost(perLvl(25, 8)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); + config(BANE_OF_ARTHROPODS) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_WEAPON) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) + .weight(5) + .maxLevel(5) + .minModCost(perLvl(5, 8)) + .maxModCost(perLvl(25, 8)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); + config(KNOCKBACK) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) + .weight(5) + .maxLevel(2) + .minModCost(perLvl(5, 20)) + .maxModCost(perLvl(55, 20)); + config(FIRE_ASPECT) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FIRE_ASPECT) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) + .weight(2) + .maxLevel(2) + .minModCost(perLvl(10, 20)) + .maxModCost(perLvl(60, 20)); + config(LOOTING) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(15, 9)) + .maxModCost(perLvl(65, 9)); + config(SWEEPING_EDGE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_SWEEPING) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(5, 9)) + .maxModCost(perLvl(20, 9)); - setUpEnchant(Enchantment.UNBREAKING, 3, Tag.ITEMS_ENCHANTABLE_DURABILITY); + config(EFFICIENCY) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MINING) + .maxLevel(5) + .minModCost(perLvl(1, 10)) + .maxModCost(perLvl(51, 10)); + config(SILK_TOUCH) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MINING_LOOT) + .weight(1) + .minModCost(flat(15)) + .maxModCost(flat(65)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_MINING); + config(UNBREAKING) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_DURABILITY) + .weight(5) + .maxLevel(3) + .minModCost(perLvl(5, 8)) + .maxModCost(perLvl(55, 8)); + config(FORTUNE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MINING_LOOT) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(15, 9)) + .maxModCost(perLvl(65, 9)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_MINING); - setUpEnchant(Enchantment.FORTUNE, 3, Tag.ITEMS_ENCHANTABLE_MINING_LOOT, List.of(Enchantment.SILK_TOUCH)); + config(POWER) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) + .maxLevel(5) + .minModCost(perLvl(1, 10)) + .maxModCost(perLvl(16, 10)); + config(PUNCH) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) + .weight(2) + .maxLevel(2) + .minModCost(perLvl(12, 20)) + .maxModCost(perLvl(37, 20)); + config(FLAME) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) + .weight(2) + .minModCost(flat(20)) + .maxModCost(flat(50)); + config(INFINITY) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) + .weight(1) + .minModCost(flat(20)) + .maxModCost(flat(50)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_BOW); - setUpEnchant(Enchantment.POWER, 5, Tag.ITEMS_ENCHANTABLE_BOW); - setUpEnchant(Enchantment.PUNCH, 2, Tag.ITEMS_ENCHANTABLE_BOW); - setUpEnchant(Enchantment.FLAME, 1, Tag.ITEMS_ENCHANTABLE_BOW); - setUpEnchant(Enchantment.INFINITY, 1, Tag.ITEMS_ENCHANTABLE_BOW, List.of(Enchantment.MENDING)); + config(LUCK_OF_THE_SEA) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FISHING) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(15, 9)) + .maxModCost(perLvl(65, 9)); + config(LURE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FISHING) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(15, 9)) + .maxModCost(perLvl(65, 9)); - setUpEnchant(Enchantment.LUCK_OF_THE_SEA, 3, Tag.ITEMS_ENCHANTABLE_FISHING); - setUpEnchant(Enchantment.LURE, 3, Tag.ITEMS_ENCHANTABLE_FISHING); + config(LOYALTY) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) + .weight(5) + .maxLevel(3) + .minModCost(perLvl(12, 7)) + .maxModCost(flat(50)); + config(IMPALING) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) + .weight(2) + .maxLevel(5) + .minModCost(perLvl(1, 8)) + .maxModCost(perLvl(21, 8)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); + config(RIPTIDE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(17, 7)) + .maxModCost(flat(50)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_RIPTIDE); - setUpEnchant(Enchantment.LOYALTY, 3, Tag.ITEMS_ENCHANTABLE_TRIDENT, List.of(Enchantment.RIPTIDE)); - setUpEnchant(Enchantment.IMPALING, 5, Tag.ITEMS_ENCHANTABLE_TRIDENT); - setUpEnchant(Enchantment.RIPTIDE, 3, Tag.ITEMS_ENCHANTABLE_TRIDENT, List.of(Enchantment.CHANNELING, Enchantment.LOYALTY)); - setUpEnchant(Enchantment.CHANNELING, 1, Tag.ITEMS_ENCHANTABLE_TRIDENT, List.of(Enchantment.RIPTIDE)); + config(LUNGE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_LUNGE) + .weight(5) + .maxLevel(3) + .minModCost(perLvl(5, 8)) + .maxModCost(perLvl(25, 8)); - setUpEnchant(Enchantment.MULTISHOT, 1, Tag.ITEMS_ENCHANTABLE_CROSSBOW, List.of(Enchantment.PIERCING)); - setUpEnchant(Enchantment.QUICK_CHARGE, 3, Tag.ITEMS_ENCHANTABLE_CROSSBOW); - setUpEnchant(Enchantment.PIERCING, 4, Tag.ITEMS_ENCHANTABLE_CROSSBOW, List.of(Enchantment.MULTISHOT)); - setUpEnchant(Enchantment.WIND_BURST, 3, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_MACE, List.of()); - setUpEnchant(Enchantment.BREACH, 4, Tag.ITEMS_ENCHANTABLE_MACE); - setUpEnchant(Enchantment.DENSITY, 5, Tag.ITEMS_ENCHANTABLE_MACE); + config(CHANNELING) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) + .weight(1) + .minModCost(flat(25)) + .maxModCost(flat(50)); - setUpEnchant(Enchantment.MENDING, 1, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_DURABILITY, List.of(Enchantment.INFINITY)); - setUpEnchant(Enchantment.VANISHING_CURSE, 1, TAG_EMPTY, Tag.ITEMS_ENCHANTABLE_VANISHING, List.of()); + config(MULTISHOT) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW) + .weight(2) + .maxLevel(1) + .minModCost(flat(20)) + .maxModCost(flat(50)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_CROSSBOW); + config(QUICK_CHARGE) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW) + .weight(5) + .maxLevel(3) + .minModCost(perLvl(12, 20)) + .maxModCost(flat(50)); + config(PIERCING) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW) + .maxLevel(4) + .minModCost(perLvl(1, 10)) + .maxModCost(flat(50)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_CROSSBOW); - checkMissingEnchantments(); - } + config(DENSITY) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MACE) + .weight(5) + .maxLevel(5) + .minModCost(perLvl(5, 8)) + .maxModCost(perLvl(25, 8)) + .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); + config(BREACH) + .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MACE) + .weight(2) + .maxLevel(4) + .minModCost(perLvl(15, 9)) + .maxModCost(perLvl(65, 9)); + config(WIND_BURST) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MACE) + .weight(2) + .maxLevel(3) + .minModCost(perLvl(15, 9)) + .maxModCost(perLvl(65, 9)); + + config(MENDING) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_DURABILITY) + .weight(2) + .minModCost(perLvl(25, 25)) + .maxModCost(perLvl(75, 25)); + config(VANISHING_CURSE) + .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_VANISHING) + .weight(1) + .minModCost(flat(25)) + .maxModCost(flat(50)); - private static void checkMissingEnchantments() { Set missingInternalEnchants = new HashSet<>(); try { for (Field field : Enchantment.class.getFields()) { if (Modifier.isStatic(field.getModifiers()) && Enchantment.class.equals(field.getType())) { Enchantment declaredEnchant = (Enchantment) field.get(null); - // If max leveel is 0, enchantment was not set up. - if (declaredEnchant.getMaxLevel() == 0) { + Enchantment stored = KEYS_TO_ENCHANTS.get(declaredEnchant.getKey()); + if (stored == null) { missingInternalEnchants.add(declaredEnchant.getKey().toString()); } } @@ -119,43 +341,115 @@ private static void checkMissingEnchantments() { if (!missingInternalEnchants.isEmpty()) { throw new IllegalStateException("Missing enchantment declarations for " + missingInternalEnchants); } + + Registry registry = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT); + // When all enchantments are initialized, redirect registry to our map. + // This allows us to add and test custom enchantments much more easily. + doAnswer(invocation -> KEYS_TO_ENCHANTS.get(invocation.getArgument(0, NamespacedKey.class))) + .when(registry).get((NamespacedKey) ArgumentMatchers.notNull()); + doAnswer(invocation -> KEYS_TO_ENCHANTS.values().stream()).when(registry).stream(); + doAnswer(invocation -> Collections.unmodifiableCollection(KEYS_TO_ENCHANTS.values()).iterator()).when(registry).iterator(); + } + + public static void putEnchant(@NotNull Enchantment enchantment) { + KEYS_TO_ENCHANTS.put(enchantment.getKey(), enchantment); } - private static void setUpEnchant( - @NotNull Enchantment enchantment, - int maxLevel, - @NotNull Tag target) { - setUpEnchant(enchantment, maxLevel, target, target, List.of()); + public static @NotNull @UnmodifiableView Set> getEnchantingTableTags() { + return Collections.unmodifiableSet(ENCHANTING_TABLE_TAGS); } - private static void setUpEnchant( - @NotNull Enchantment enchantment, - int maxLevel, - @NotNull Tag target, - @NotNull Collection conflicts) { - setUpEnchant(enchantment, maxLevel, target, target, conflicts); + private static @NotNull IntUnaryOperator perLvl(int base, int perLevel) { + return level -> base + (level - 1) * perLevel; } - private static void setUpEnchant( - @NotNull Enchantment enchantment, - int maxLevel, - @NotNull Tag tableTarget, - @NotNull Tag anvilTarget, - @NotNull Collection conflicts) { - doReturn(1).when(enchantment).getStartLevel(); - doReturn(maxLevel).when(enchantment).getMaxLevel(); - // Hopefully in the future the enchantment API gets expanded, making separate table+anvil targets available - doAnswer(invocation -> { - ItemStack item = invocation.getArgument(0); - return item != null && anvilTarget.isTagged(item.getType()); - }).when(enchantment).canEnchantItem(any()); - doReturn(tableTarget.getValues().isEmpty()).when(enchantment).isTreasure(); - // Note: Usual implementation allows contains check, but as these are - // mocks that cannot be relied on. - doAnswer(invocation -> { - NamespacedKey otherKey = invocation.getArgument(0, Enchantment.class).getKey(); - return otherKey.equals(enchantment.getKey()) || conflicts.stream().anyMatch(conflict -> conflict.getKey().equals(otherKey)); - }).when(enchantment).conflictsWith(any()); + private static @NotNull IntUnaryOperator flat(int value) { + return integer -> value; + } + + private static EnchantConfig config(Enchantment enchantment) { + return new EnchantConfig(enchantment); + } + + private static record EnchantConfig(Enchantment enchantment) { + + EnchantConfig(Enchantment enchantment) { + this.enchantment = enchantment; + KEYS_TO_ENCHANTS.put(enchantment.getKey(), enchantment); + weight(10); + doReturn(1).when(enchantment).getStartLevel(); + doReturn(1).when(enchantment).getMaxLevel(); + doAnswer(invocation -> { + NamespacedKey otherKey = invocation.getArgument(0, Enchantment.class).getKey(); + return otherKey.equals(enchantment.getKey()); + }).when(enchantment).conflictsWith(any()); + } + + EnchantConfig weight(int weight) { + doReturn(weight).when(enchantment).getWeight(); + + // Anvil cost is technically separate, but in practice is based on enchanting table rarity. + // For known rarities, set it here. + return switch (weight) { + case 10 -> anvilCost(1); + case 5 -> anvilCost(2); + case 2 -> anvilCost(4); + case 1 -> anvilCost(8); + default -> this; + }; + } + + EnchantConfig maxLevel(int maxLevel) { + doReturn(maxLevel).when(enchantment).getMaxLevel(); + return this; + } + + EnchantConfig anvilTarget(TagKey targetKey) { + // Hopefully in the future the enchantment API gets expanded, making separate table+anvil targets available + Tag target = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(targetKey); + doAnswer(invocation -> { + ItemStack item = invocation.getArgument(0); + return item != null && target.contains(TypedKey.create(RegistryKey.ITEM, item.getType().getKey())); + }).when(enchantment).canEnchantItem(any()); + doReturn(target).when(enchantment).getSupportedItems(); + return this; + } + + EnchantConfig tableTarget(TagKey targetKey) { + Tag target = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(targetKey); + doReturn(false).when(enchantment).isTreasure(); + ENCHANTING_TABLE_TAGS.add(target); + return anvilTarget(targetKey); + } + + EnchantConfig minModCost(IntUnaryOperator cost) { + doAnswer(invocation -> cost.applyAsInt(invocation.getArgument(0, Integer.class))) + .when(enchantment).getMinModifiedCost(anyInt()); + return this; + } + + EnchantConfig maxModCost(IntUnaryOperator cost) { + doAnswer(invocation -> cost.applyAsInt(invocation.getArgument(0, Integer.class))) + .when(enchantment).getMaxModifiedCost(anyInt()); + return this; + } + + EnchantConfig anvilCost(int cost) { + doReturn(cost).when(enchantment).getAnvilCost(); + return this; + } + + EnchantConfig exclusive(TagKey conflict) { + Registry registry = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT); + var conflicts = registry.getTag(conflict); + doAnswer(invocation -> { + // Apparently no way to map Enchantment -> TypeKey directly? Seems odd. + TypedKey otherKey = EnchantmentKeys.create(invocation.getArgument(0, Enchantment.class).key()); + return conflicts.contains(otherKey); + }).when(enchantment).conflictsWith(any()); + return this; + } + } } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java index 2300f3f..bec2610 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java @@ -1,21 +1,5 @@ package com.github.jikoo.enchantableblocks.registry; -import static com.github.jikoo.enchantableblocks.mock.matcher.IsSimilarMatcher.similar; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.block.EnchantableBlock; import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; import com.github.jikoo.enchantableblocks.mock.ServerMocks; @@ -27,13 +11,6 @@ import com.github.jikoo.enchantableblocks.util.Region; import com.github.jikoo.enchantableblocks.util.RegionStorage; import com.github.jikoo.planarwrappers.util.Coords; -import java.lang.reflect.Field; -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; -import java.util.logging.Logger; import org.bukkit.Chunk; import org.bukkit.Material; import org.bukkit.block.Block; @@ -55,6 +32,30 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import static com.github.jikoo.enchantableblocks.mock.matcher.ItemMatcher.isSimilar; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Manage enchantable blocks.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableBlockManagerTest { @@ -227,7 +228,7 @@ void testCreate() { assertThat( "Item should match creation stack", enchantableBlock.getItemStack(), - similar(item)); + isSimilar(item)); } @DisplayName("Unset block returns null.") @@ -348,7 +349,7 @@ void testInvalidData() { assertThat( "Invalid save data should still return in-memory item if available", manager.destroyBlock(block), - similar(stack)); + isSimilar(stack)); } @DisplayName("Destroying valid blocks should return creation item.") @@ -361,7 +362,7 @@ void testValidBlock() { assertThat( "Valid block should return creation item", manager.destroyBlock(block), - similar(stack)); + isSimilar(stack)); } private void setUpChunks() { From 513b273068d3157c03ed6c6ee207c3322e2b9453 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sat, 31 Jan 2026 19:54:44 -0500 Subject: [PATCH 06/20] Clean up FurnaceListenerTest --- .../impl/furnace/FurnaceListenerTest.java | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java index 12efb9e..5dd49e9 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java @@ -30,6 +30,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.Recipe; import org.bukkit.plugin.Plugin; @@ -49,6 +50,7 @@ import java.util.Set; import java.util.function.IntSupplier; +import static com.github.jikoo.enchantableblocks.mock.matcher.ItemMatcher.isItem; import static com.github.jikoo.enchantableblocks.mock.matcher.ItemMatcher.isSimilar; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -70,6 +72,7 @@ class FurnaceListenerTest { private CookingRecipe recipe; + private ItemStack input; @BeforeAll void beforeAll() { @@ -86,6 +89,12 @@ void beforeAll() { // Set up recipe iterator when(server.recipeIterator()).thenAnswer(invocation -> Set.of((Recipe) recipe).iterator()); + // Set up max stack size for result. + doReturn(64).when(ItemType.COARSE_DIRT).getMaxStackSize(); + + // Create input stack. + input = ItemType.DIRT.createItemStack(); + // Set up scheduler to run tasks immediately. var scheduler = mock(BukkitScheduler.class); when(scheduler.runTask(any(Plugin.class), any(Runnable.class))).thenAnswer(invocation -> { @@ -203,7 +212,7 @@ void testFurnaceBurnUnbreaking() { @Test void testFurnaceStartSmeltInvalid() { when(manager.getBlock(block)).thenReturn(null); - var event = new FurnaceStartSmeltEvent(block, recipe.getInput(), recipe); + var event = new FurnaceStartSmeltEvent(block, input, recipe, recipe.getCookingTime()); assertDoesNotThrow(() -> listener.onFurnaceStartSmelt(event)); assertThat( "Cook time must not be modified", @@ -215,7 +224,7 @@ void testFurnaceStartSmeltInvalid() { @Test void testFurnaceStartSmeltModifier() { when(enchantableFurnace.applyCookTimeModifiers(anyDouble())).thenAnswer(invocation -> (short) (invocation.getArgument(0, Double.class) + 10)); - var event = new FurnaceStartSmeltEvent(block, recipe.getInput(), recipe); + var event = new FurnaceStartSmeltEvent(block, input, recipe, recipe.getCookingTime()); assertDoesNotThrow(() -> listener.onFurnaceStartSmelt(event)); assertThat( "Cook time must be modified", @@ -227,10 +236,10 @@ void testFurnaceStartSmeltModifier() { @Test void testFurnaceSmeltInvalid() { when(manager.getBlock(block)).thenReturn(null); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); assertThat("Event is never cancelled", !event.isCancelled()); - assertThat("Result must not be modified", event.getResult(), is(recipe.getResult())); + assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); verify(enchantableFurnace, times(0)).getFortune(); } @@ -238,10 +247,10 @@ void testFurnaceSmeltInvalid() { @Test void testFurnaceSmeltInvalidTile() { when(enchantableFurnace.getFurnaceTile()).thenReturn(null); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); assertThat("Event is never cancelled", !event.isCancelled()); - assertThat("Result must not be modified", event.getResult(), is(recipe.getResult())); + assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); verify(enchantableFurnace, times(0)).getFortune(); } @@ -250,10 +259,10 @@ void testFurnaceSmeltInvalidTile() { void testFurnaceSmeltUnbreakingEfficiency() { when(enchantableFurnace.getCookModifier()).thenReturn(10); when(enchantableFurnace.getBurnModifier()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); assertThat("Event is never cancelled", !event.isCancelled()); - assertThat("Result must not be modified", event.getResult(), is(recipe.getResult())); + assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); } @DisplayName("Fortune result is not calculated if there is no space.") @@ -261,30 +270,30 @@ void testFurnaceSmeltUnbreakingEfficiency() { void testApplyFortuneFull() { var result = recipe.getResult(); result.setAmount(result.getType().getMaxStackSize()); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), result); + var event = new FurnaceSmeltEvent(block, input, result, recipe); var supplier = mock(IntSupplier.class); listener.applyFortune(event, supplier); verify(supplier, times(0)).getAsInt(); - assertThat("Full result must not be modified", event.getResult(), is(result)); + assertThat("Full result must not be modified", event.getResult(), isItem(result)); } @DisplayName("Fortune result is ignored if less than one.") @ParameterizedTest @ValueSource(ints = { -1, 0 }) void testApplyFortuneBelowOne(int value) { - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); listener.applyFortune(event, () -> value); - assertThat("Result must not be modified", event.getResult(), is(recipe.getResult())); + assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); } @DisplayName("Fortune result is included if there is space.") @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testApplyFortunePositive(int value) { - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); listener.applyFortune(event, () -> value); assertThat("Result must be similar", event.getResult(), isSimilar(recipe.getResult())); - assertThat("Result must be modified", event.getResult(), is(not(recipe.getResult()))); + assertThat("Result must be modified", event.getResult(), not(isItem(recipe.getResult()))); assertThat("Result amount must be increased as expected", event.getResult().getAmount(), is(value + 1)); } @@ -296,14 +305,14 @@ void testApplyFortuneReducedSpace(int value) { // Set up event for stack with 1 free slot. var result = recipe.getResult(); result.setAmount(result.getType().getMaxStackSize() - 1); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), result); + var event = new FurnaceSmeltEvent(block, input, result, recipe); listener.applyFortune(event, () -> value); // Expect max stack. var expected = recipe.getResult(); expected.setAmount(result.getType().getMaxStackSize()); - assertThat("Result must be full", event.getResult(), is(expected)); + assertThat("Result must be full", event.getResult(), isItem(expected)); } @DisplayName("Furnace smelt applies fortune if not in blacklist.") @@ -312,7 +321,7 @@ void testFurnaceSmeltFortuneNotBlacklist() { var config = new EnchantableFurnaceConfig(new YamlConfiguration()); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -325,11 +334,11 @@ void testFurnaceSmeltFortuneNotBlacklist() { @Test void testFurnaceSmeltFortuneBlacklist() { var yaml = new YamlConfiguration(); - yaml.set("fortuneList", List.of(recipe.getInput().getType().name())); + yaml.set("fortuneList", List.of(input.getType().name())); var config = new EnchantableFurnaceConfig(yaml); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -343,11 +352,11 @@ void testFurnaceSmeltFortuneBlacklist() { void testFurnaceSmeltFortuneWhitelisted() { var yaml = new YamlConfiguration(); yaml.set("fortuneListIsBlacklist", false); - yaml.set("fortuneList", List.of(recipe.getInput().getType().name())); + yaml.set("fortuneList", List.of(input.getType().name())); var config = new EnchantableFurnaceConfig(yaml); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -364,7 +373,7 @@ void testFurnaceSmeltNotWhitelistedFortune() { var config = new EnchantableFurnaceConfig(yaml); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -376,7 +385,7 @@ void testFurnaceSmeltNotWhitelistedFortune() { @DisplayName("Furnaces that cannot pause do not attempt to.") @Test void testFurnaceSmeltNoPause() { - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); verify(enchantableFurnace, times(0)).shouldPause(any()); assertThat("Event is never cancelled", !event.isCancelled()); @@ -386,7 +395,7 @@ void testFurnaceSmeltNoPause() { @Test void testFurnaceSmeltTryPause() { when(enchantableFurnace.canPause()).thenReturn(true); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); verify(enchantableFurnace).shouldPause(any()); verify(enchantableFurnace, times(0)).pause(); @@ -398,7 +407,7 @@ void testFurnaceSmeltTryPause() { void testFurnaceSmeltDoPause() { when(enchantableFurnace.canPause()).thenReturn(true); when(enchantableFurnace.shouldPause(any())).thenReturn(true); - var event = new FurnaceSmeltEvent(block, recipe.getInput(), recipe.getResult()); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); verify(enchantableFurnace).pause(); assertThat("Event is never cancelled", !event.isCancelled()); From 4535a860760c1ad3813133f9b32b15d5267fc969 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sat, 31 Jan 2026 20:07:57 -0500 Subject: [PATCH 07/20] Avoid nullity annotation Easier to just edit the registry for that test anyway. --- .../listener/TableEnchanterTest.java | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java index b714bcc..f355a72 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java @@ -1,23 +1,5 @@ package com.github.jikoo.enchantableblocks.listener; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasItemInArray; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; @@ -26,8 +8,6 @@ import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import java.util.Map; -import java.util.Set; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; @@ -42,18 +22,38 @@ import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItemInArray; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Enchant blocks in enchanting tables.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TableEnchanterTest { - private @Nullable EnchantableRegistration registration; + private EnchantableBlockRegistry registry; + private EnchantableRegistration registration; private Player player; private TableEnchanter listener; private ItemStack itemStack; @@ -79,9 +79,8 @@ void setUp() { Plugin plugin = mock(Plugin.class); when(plugin.getName()).thenReturn(getClass().getSimpleName()); - var registry = mock(EnchantableBlockRegistry.class); - // Use doAnswer so that we can test with a null registration despite instantiating by default. - doAnswer(invocation -> registration).when(registry).get(any()); + registry = mock(EnchantableBlockRegistry.class); + doReturn(registration).when(registry).get(any()); listener = new TableEnchanter(plugin, registry); @@ -99,7 +98,7 @@ void setUp() { @DisplayName("Materials with no corresponding registration cannot enchant.") @Test void testUnregistered() { - registration = null; + doReturn(null).when(registry).get(any()); assertThat("Never ineligible", listener.isIneligible(player, itemStack), is(false)); assertThat( "Unregistered material cannot be enchanted", From 0912fd18ff6b46c0eeafc136284437324f6297de Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sat, 31 Jan 2026 21:11:04 -0500 Subject: [PATCH 08/20] Improve main plugin tests Still requires some kludge to spy the created objects because of how Paper constructs plugins, but such is life. --- .../EnchantableBlocksPlugin.java | 15 ---- .../EnchantableBlocksPluginTest.java | 78 ++++++++----------- .../block/EnchantableBlockTest.java | 3 +- .../server/TestDummyPluginLoaderHolder.java | 49 ++++++++++++ .../services/org.bukkit.plugin.PluginLoader | 1 + 5 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java create mode 100644 src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader diff --git a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java index 0661479..14d3429 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java @@ -5,14 +5,11 @@ import com.github.jikoo.enchantableblocks.listener.TableEnchanter; import com.github.jikoo.enchantableblocks.listener.WorldListener; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; -import java.io.File; import org.bukkit.Chunk; import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; -import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.plugin.java.JavaPluginLoader; import org.jetbrains.annotations.NotNull; /** @@ -22,18 +19,6 @@ public class EnchantableBlocksPlugin extends JavaPlugin { private EnchantableBlockManager blockManager; - public EnchantableBlocksPlugin() { - super(); - } - - public EnchantableBlocksPlugin( - @NotNull JavaPluginLoader loader, - @NotNull PluginDescriptionFile description, - @NotNull File dataFolder, - @NotNull File file) { - super(loader, description, dataFolder, file); - } - @Override public void onLoad() { this.blockManager = new EnchantableBlockManager(this); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java b/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java index 97632fe..00aecfe 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java @@ -1,29 +1,8 @@ package com.github.jikoo.enchantableblocks; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.nio.file.Path; -import java.util.List; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; import org.bukkit.Chunk; import org.bukkit.command.Command; import org.bukkit.entity.Player; @@ -32,14 +11,31 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.java.JavaPluginLoader; import org.bukkit.scheduler.BukkitScheduler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.invocation.InvocationOnMock; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; @DisplayName("Feature: Plugin should load and enable features.") class EnchantableBlocksPluginTest { @@ -56,26 +52,19 @@ void beforeEach() throws FileNotFoundException, InvalidDescriptionException { var factory = ItemFactoryMocks.mockFactory(); when(server.getItemFactory()).thenReturn(factory); - // JavaPlugin constructs a PluginLogger during initialization, which copies certain - // fields out of the parent server logger directly. As a result, the server logger cannot be - // a mock or the process will result in a NPE. - Logger logger = new Logger(this.getClass().getCanonicalName(), null) { - @Override - public boolean isLoggable(Level level) { - // Instead, turn our real logger into a faux-dummy logger by blocking all logging. - return false; - } - }; - when(server.getLogger()).thenReturn(logger); - - var loader = new JavaPluginLoader(server); var description = new PluginDescriptionFile(new BufferedReader(new FileReader( Path.of(".", "src", "main", "resources", "plugin.yml").toFile()))); var dataFolder = Path.of(".", "src", "test", "resources", description.getName()).toFile(); - plugin = spy(new EnchantableBlocksPlugin(loader, description, dataFolder, new File("jarfile"))); - - logger = mock(Logger.class); - when(plugin.getLogger()).thenReturn(logger); + plugin = mock(EnchantableBlocksPlugin.class, withSettings().defaultAnswer(InvocationOnMock::callRealMethod)); + + plugin.init( + mock(), + server, + description, + dataFolder, + mock(), + EnchantableBlocksPlugin.class.getClassLoader() + ); } @AfterEach @@ -119,15 +108,12 @@ void testPluginLoad() { plugin.onLoad(); - var logger = plugin.getLogger(); - ArgumentCaptor> captor = ArgumentCaptor.forClass(Supplier.class); - doNothing().when(logger).info(captor.capture()); + Logger logger = mock(); + doReturn(logger).when(plugin).getLogger(); plugin.onEnable(); - assertThat("Line is logged", captor.getAllValues().size(), is(1)); - String line = captor.getValue().get(); - assertThat("Correct line is logged", line, startsWith("Loaded all active blocks")); + verify(plugin.getLogger()).info(any(Supplier.class)); } @DisplayName("Reload command functions as expected.") diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java index e845f05..41990be 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java @@ -12,7 +12,6 @@ import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemType; -import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -80,7 +79,7 @@ void testGetItemStack() { var enchantableBlock = new EnchantableBlock(registration, block, itemStack, storage) {}; verify(itemStack).clone(); ItemStack internalStack = enchantableBlock.getItemStack(); - assertThat("Item is clone", internalStack, CoreMatchers.is(itemStackClone)); + assertThat("Item is clone", internalStack, is(itemStackClone)); verify(itemStackClone).setAmount(1); // Directly returning the internal ItemStack instance allows subclasses to manipulate it. assertThat("Same item is returned", internalStack == enchantableBlock.getItemStack()); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java new file mode 100644 index 0000000..5ca89af --- /dev/null +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java @@ -0,0 +1,49 @@ +package com.github.jikoo.enchantableblocks.mock.server; + +import org.bukkit.event.Event; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.RegisteredListener; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +public class TestDummyPluginLoaderHolder implements PluginLoader { + + @Override + public @NotNull Plugin loadPlugin(@NotNull File file) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public @NonNull @NotNull Pattern[] getPluginFileFilters() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public @NotNull Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public void enablePlugin(@NotNull Plugin plugin) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public void disablePlugin(@NotNull Plugin plugin) { + throw new IllegalStateException("Not implemented"); + } + +} diff --git a/src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader b/src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader new file mode 100644 index 0000000..b8b2fa5 --- /dev/null +++ b/src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader @@ -0,0 +1 @@ +com.github.jikoo.enchantableblocks.mock.server.TestDummyPluginLoaderHolder \ No newline at end of file From 2a45a2bbd9b9cfb08a1d4580cb6a6a2fb33e47ac Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sat, 31 Jan 2026 21:12:48 -0500 Subject: [PATCH 09/20] Just test methods directly Don't need a sketchy generic test casting a function to a bifunction --- .../util/EmptyCookingRecipe.java | 7 +- .../util/EmptyCookingRecipeTest.java | 73 ++++++------------- 2 files changed, 25 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java index 89377ef..6319fc5 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java @@ -28,14 +28,11 @@ public ItemStack getItemStack() { return new ItemStack(Material.AIR); } + @SuppressWarnings("MethodDoesntCallSuperMethod") @NotNull @Override public RecipeChoice clone() { - try { - return (RecipeChoice) super.clone(); - } catch (CloneNotSupportedException e) { - throw new IllegalStateException(e); - } + return this; } @Override diff --git a/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java b/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java index aa2ad6a..5b538ce 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java @@ -1,30 +1,26 @@ package com.github.jikoo.enchantableblocks.util; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Stream; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.RecipeChoice; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Placeholder empty unmodifiable cooking recipe") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EmptyCookingRecipeTest { @@ -67,45 +63,22 @@ void testEquality() { } @DisplayName("Empty recipe is not modified by setters.") - @ParameterizedTest - @MethodSource("getMethods") - void testMethods( - @NotNull Function getter, - @NotNull BiConsumer setter, - Object setValue) { - Object value = getter.apply(recipe); - boolean useSetterReturn = false; - Object setterReturn = null; - if (setter instanceof BiFunction biFunction) { - setterReturn = biFunction.apply(recipe, setValue); - useSetterReturn = true; - } else { - setter.accept(recipe, setValue); - } - assertThat("Value must not change", getter.apply(recipe), is(value)); - if (useSetterReturn) { - assertThat("Setter must return self", setterReturn, is(recipe)); - } - } + @Test + void testMethods() { + RecipeChoice inputChoice = recipe.getInputChoice(); + recipe.setInput(Material.COAL); + assertThat("Input modification has no effect", recipe.getInputChoice(), is(inputChoice)); + assertThat("Modified input is not accepted", !inputChoice.test(new ItemStack(Material.COAL))); + + recipe.setInputChoice(new RecipeChoice.MaterialChoice(Material.COAL)); + assertThat("InputChoice modification has no effect", recipe.getInputChoice(), is(inputChoice)); + + int cookingTime = recipe.getCookingTime(); + recipe.setCookingTime(cookingTime + 1); + assertThat("CookingTime modification has no effect", recipe.getCookingTime(), is(cookingTime)); - static Stream getMethods() { - return Stream.of( - Arguments.of( - (Function) EmptyCookingRecipe::getInput, - (BiConsumer) EmptyCookingRecipe::setInput, - Material.COAL), - Arguments.of( - (Function) EmptyCookingRecipe::getInputChoice, - (BiConsumer) EmptyCookingRecipe::setInputChoice, - new RecipeChoice.MaterialChoice(Material.COAL)), - Arguments.of( - (Function) EmptyCookingRecipe::getCookingTime, - (BiConsumer) EmptyCookingRecipe::setCookingTime, - 500), - Arguments.of( - (Function) EmptyCookingRecipe::getExperience, - (BiConsumer) EmptyCookingRecipe::setExperience, - 500F) - ); + float experience = recipe.getExperience(); + recipe.setExperience(experience + 1); + assertThat("Experience modification has no effect", recipe.getExperience(), is(experience)); } } \ No newline at end of file From 747b04b614dc88798f73d603e802dd1581275a96 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sun, 1 Feb 2026 09:14:41 -0500 Subject: [PATCH 10/20] Add permission for dependabot approval --- .github/workflows/pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c702210..853e437 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,6 +10,9 @@ jobs: if: "github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'" needs: [ "run-ci" ] runs-on: "ubuntu-latest" + permissions: + contents: write + pull-requests: write steps: # Always approve PRs from Dependabot. - name: Approve From 30a57925ed50a4ca6da86c1a51bd7ff796ce5c41 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sun, 1 Feb 2026 12:40:22 -0500 Subject: [PATCH 11/20] Code health Clean up some inspection issues Bump version to 5.0.0-SNAPSHOT because things will break --- pom.xml | 2 +- .../EnchantableBlocksPlugin.java | 4 +-- .../block/impl/furnace/FurnaceListener.java | 6 ++-- .../config/EnchantableBlockConfig.java | 31 ++++++------------- .../registry/RegionLoadFunction.java | 9 +++--- .../impl/furnace/FurnaceListenerTest.java | 4 --- .../mock/enchantments/EnchantmentMocks.java | 2 +- .../mock/inventory/ItemStackMocks.java | 8 ++--- 8 files changed, 26 insertions(+), 40 deletions(-) diff --git a/pom.xml b/pom.xml index 7c91e15..739df5b 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.github.jikoo enchantableblocks EnchantableBlocks - 4.1.1-SNAPSHOT + 5.0.0-SNAPSHOT jar diff --git a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java index 14d3429..e5c1691 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java @@ -70,7 +70,7 @@ public boolean onCommand( @NotNull String label, @NotNull String @NotNull [] args) { if (args.length < 1 || !args[0].equalsIgnoreCase("reload")) { - sender.sendMessage("EnchantableBlocks v" + getDescription().getVersion()); + sender.sendMessage("EnchantableBlocks v" + getPluginMeta().getVersion()); return false; } @@ -78,7 +78,7 @@ public boolean onCommand( this.blockManager.getRegistry().reload(); sender.sendMessage( "[EnchantableBlocks v" - + getDescription().getVersion() + + getPluginMeta().getVersion() + "] Reloaded config and registry cache."); return true; } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java index 34aea12..0af3ad2 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java @@ -1,8 +1,6 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.IntSupplier; import org.bukkit.block.Furnace; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -19,6 +17,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.IntSupplier; + /** * Listener for furnace-specific events. */ @@ -92,6 +93,7 @@ void onFurnaceSmelt(final @NotNull FurnaceSmeltEvent event) { } if (enchantableFurnace.shouldPause(event)) { + // TODO can use Paper's addition of recipe to skip cache? plugin.getServer().getScheduler().runTask(plugin, enchantableFurnace::pause); } } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java b/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java index 8f06f78..d026ad8 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java @@ -10,11 +10,12 @@ import com.github.jikoo.planarwrappers.config.impl.BooleanSetting; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import java.util.Set; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.enchantments.Enchantment; import org.jetbrains.annotations.NotNull; +import java.util.Set; + /** * The base settings for all * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock EnchantableBlocks}. @@ -22,27 +23,13 @@ public abstract class EnchantableBlockConfig { protected final @NotNull ConfigurationSection section; - /** @deprecated use {@link #enabled()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Setting enabled; - /** @deprecated use {@link #tableEnchantability()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Setting tableEnchantability; - /** @deprecated use {@link #tableDisabledEnchants()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Setting> tableDisabledEnchants; - /** @deprecated use {@link #tableEnchantmentConflicts()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Setting> tableEnchantmentConflicts; - /** @deprecated use {@link #anvilDisabledEnchants()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Setting> anvilDisabledEnchants; - /** @deprecated use {@link #anvilEnchantmentConflicts()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Setting> anvilEnchantmentConflicts; - /** @deprecated use {@link #anvilEnchantmentMax()} */ - @Deprecated(forRemoval = true, since = "4.1.0") - public final @NotNull Mapping anvilEnchantmentMax; + private final @NotNull Setting enabled; + private final @NotNull Setting tableEnchantability; + private final @NotNull Setting> tableDisabledEnchants; + private final @NotNull Setting> tableEnchantmentConflicts; + private final @NotNull Setting> anvilDisabledEnchants; + private final @NotNull Setting> anvilEnchantmentConflicts; + private final @NotNull Mapping anvilEnchantmentMax; protected EnchantableBlockConfig(@NotNull ConfigurationSection configurationSection) { this.section = configurationSection; diff --git a/src/main/java/com/github/jikoo/enchantableblocks/registry/RegionLoadFunction.java b/src/main/java/com/github/jikoo/enchantableblocks/registry/RegionLoadFunction.java index 2568cba..c78e15d 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/registry/RegionLoadFunction.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/registry/RegionLoadFunction.java @@ -3,14 +3,15 @@ import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager.RegionStorageData; import com.github.jikoo.enchantableblocks.util.Region; import com.github.jikoo.enchantableblocks.util.RegionStorage; +import org.bukkit.configuration.InvalidConfigurationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.IOException; import java.nio.file.Path; import java.util.function.BiFunction; import java.util.logging.Level; import java.util.logging.Logger; -import org.bukkit.configuration.InvalidConfigurationException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * A {@link BiFunction} used to load data from disk. @@ -25,7 +26,7 @@ record RegionLoadFunction( public @Nullable RegionStorageData apply(@NotNull Region region, @NotNull Boolean create) { RegionStorage storage = new RegionStorage(dataDir(), region); - if (!storage.getDataFile().exists() && Boolean.FALSE.equals(create)) { + if (!storage.getDataFile().exists() && !create) { return null; } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java index 5dd49e9..38b707e 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java @@ -113,10 +113,6 @@ class FurnaceEventsTest { private FurnaceListener listener; private Block block; - @BeforeAll - static void beforeAll() { - } - @BeforeEach void beforeEach() { var server = Bukkit.getServer(); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java index 005777d..5f384f7 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java @@ -371,7 +371,7 @@ private static EnchantConfig config(Enchantment enchantment) { return new EnchantConfig(enchantment); } - private static record EnchantConfig(Enchantment enchantment) { + private record EnchantConfig(Enchantment enchantment) { EnchantConfig(Enchantment enchantment) { this.enchantment = enchantment; diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java index da62402..0e9e0b9 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java @@ -96,14 +96,14 @@ public static ItemStack newItemMock(@NotNull ItemType type, int amount) { return existing != null && existing.hasEnchant(invocation.getArgument(0)); }).when(stack).containsEnchantment(any(Enchantment.class)); Answer addEnchant = invocation -> { - get(meta, stack.getType(), true).ifPresent(itemMeta -> { - itemMeta.addEnchant( + get(meta, stack.getType(), true).ifPresent(itemMeta -> + itemMeta.addEnchant( invocation.getArgument(0), invocation.getArgument(1), // We aren't winning any performance prizes here, a beautiful DRY hack. invocation.getMethod().getName().contains("Unsafe") - ); - }); + ) + ); return null; }; doAnswer(addEnchant).when(stack).addEnchantment(any(Enchantment.class), anyInt()); From e1976d2aa7bc766bff96a1ed9a23e1b687f1c095 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sun, 1 Feb 2026 16:36:47 -0500 Subject: [PATCH 12/20] Fix last few tests Not sure what to do about runnable - can't mock .equals, unfortunately. --- .../impl/furnace/EnchantableFurnaceTest.java | 33 ++++--- .../listener/AnvilEnchanterTest.java | 94 ++++++++++--------- 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java index e08d975..0058922 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java @@ -64,6 +64,7 @@ class EnchantableFurnaceTest { private Block block; private ItemStack itemStack; private ConfigurationSection storage; + private ItemStack input; @BeforeAll void beforeAll() { @@ -85,11 +86,13 @@ void beforeEach() { block = mock(Block.class); itemStack = ItemType.FURNACE.createItemStack(); storage = mock(ConfigurationSection.class); + input = ItemType.DIRT.createItemStack(); + doReturn(64).when(ItemType.COARSE_DIRT).getMaxStackSize(); // Set up matching recipe when(reg.getFurnaceRecipe(any())).thenAnswer(invocation -> { FurnaceInventory inventory = invocation.getArgument(0); - if (recipe.getInput().isSimilar(inventory.getSmelting())) { + if (input.isSimilar(inventory.getSmelting())) { return recipe; } return null; @@ -333,7 +336,7 @@ void testShouldPauseFullResult() { when(enchantableFurnace.canPause()).thenReturn(true); var tile = setUpTile(); var inv = tile.getInventory(); - inv.setSmelting(recipe.getInput()); + inv.setSmelting(input); var result = recipe.getResult(); result.setAmount(result.getType().getMaxStackSize()); inv.setResult(result); @@ -348,9 +351,9 @@ void testShouldPauseNonmatching() { when(enchantableFurnace.canPause()).thenReturn(true); var tile = setUpTile(); var inv = tile.getInventory(); - ItemStack input = new ItemStack(Material.FURNACE); - assertThat("Input is not similar to recipe input", input, not(isSimilar(recipe.getInput()))); - inv.setSmelting(input); + ItemStack otherInput = new ItemStack(Material.FURNACE); + assertThat("Input is not similar to recipe input", otherInput, not(isSimilar(input))); + inv.setSmelting(otherInput); inv.setResult(recipe.getResult()); assertThat("Furnace with no matching recipe should pause", enchantableFurnace.shouldPause(null)); @@ -369,7 +372,7 @@ void testShouldPauseInputNotYieldResult() { var tile = setUpTile(); var inv = tile.getInventory(); - inv.setSmelting(recipe.getInput()); + inv.setSmelting(input); inv.setResult(recipe.getResult()); assertThat("Furnace with input not matching recipe input should pause", enchantableFurnace.shouldPause(null)); @@ -382,7 +385,7 @@ void testShouldPauseNullResult() { when(enchantableFurnace.canPause()).thenReturn(true); var tile = setUpTile(); var inv = tile.getInventory(); - inv.setSmelting(recipe.getInput()); + inv.setSmelting(input); assertThat( "Furnace with null result should not pause", @@ -397,7 +400,7 @@ void testShouldPauseAirResult() { when(enchantableFurnace.canPause()).thenReturn(true); var tile = setUpTile(); var inv = tile.getInventory(); - inv.setSmelting(recipe.getInput()); + inv.setSmelting(input); inv.setResult(new ItemStack(Material.AIR)); assertThat( @@ -413,7 +416,7 @@ void testShouldPauseSimilarResult() { when(enchantableFurnace.canPause()).thenReturn(true); var tile = setUpTile(); var inv = tile.getInventory(); - inv.setSmelting(recipe.getInput()); + inv.setSmelting(input); inv.setResult(recipe.getResult()); assertThat( @@ -429,7 +432,7 @@ void testShouldPauseDissimilarResult() { when(enchantableFurnace.canPause()).thenReturn(true); var tile = setUpTile(); var inv = tile.getInventory(); - inv.setSmelting(recipe.getInput()); + inv.setSmelting(input); ItemStack result = new ItemStack(Material.DIAMOND); assertThat("Result must be dissimilar", result, not(isSimilar(recipe.getResult()))); inv.setResult(result); @@ -445,10 +448,10 @@ void testShouldPausePostSmelt() { var furnace = setUpTile(); var inv = furnace.getInventory(); - var smelting = recipe.getInput(); + var smelting = input; inv.setSmelting(smelting); inv.setResult(null); - var event = new FurnaceSmeltEvent(block, smelting, recipe.getResult()); + var event = new FurnaceSmeltEvent(block, smelting, recipe.getResult(), recipe); assertThat( "Situation does not result in pausing without event context", @@ -556,7 +559,7 @@ void testResume() { short frozenTicks = 200; enchantableFurnace.setFrozenTicks(frozenTicks); var tile = setUpTile(); - tile.getInventory().setSmelting(recipe.getInput()); + tile.getInventory().setSmelting(input); assertThat("Furnace resumes as needed", enchantableFurnace.resume()); assertThat("Tile is burning", tile.getBurnTime(), is(frozenTicks)); @@ -709,7 +712,7 @@ void testUpdateNoPauseChange() { verify(enchantableFurnace, times(0)).resume(); when(enchantableFurnace.isPaused()).thenReturn(false); - inventory.setSmelting(recipe.getInput()); + inventory.setSmelting(input); task.run(); verify(enchantableFurnace, times(0)).pause(); verify(enchantableFurnace, times(0)).resume(); @@ -727,7 +730,7 @@ void testUpdateResume() { Runnable task = taskCaptor.getValue(); when(enchantableFurnace.isPaused()).thenReturn(true); - inventory.setSmelting(recipe.getInput()); + inventory.setSmelting(input); task.run(); verify(enchantableFurnace, times(0)).pause(); verify(enchantableFurnace).resume(); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java index c23cf6e..3e2a9e9 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java @@ -1,33 +1,12 @@ package com.github.jikoo.enchantableblocks.listener; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; -import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockRegistry; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; -import java.util.Collection; -import java.util.List; -import java.util.Set; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.configuration.file.YamlConfiguration; @@ -35,6 +14,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.inventory.PrepareAnvilEvent; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.view.AnvilView; import org.bukkit.plugin.Plugin; @@ -52,27 +32,46 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @DisplayName("Feature: Enchant and combine blocks in anvils.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AnvilEnchanterTest { private Enchantment enchantment; - private Material goodMat; - private Material badMat; + private ItemType goodType; + private ItemType badType; private AnvilEnchanter enchanter; @BeforeAll void setUpAll() { - var server = ServerMocks.mockServer(); + ServerMocks.mockServer(); EnchantmentMocks.init(); enchantment = Enchantment.EFFICIENCY; - goodMat = Material.COAL_ORE; - badMat = Material.REDSTONE_ORE; - - var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + goodType = ItemType.COAL_ORE; + badType = ItemType.REDSTONE_ORE; } @Nested @@ -94,32 +93,32 @@ void testNullBaseInvalid() { @DisplayName("Items are invalid if addition is empty.") @Test void testNullAdditionInvalid() { - var base = new ItemStack(goodMat); + var base = goodType.createItemStack(); assertThat("Items are invalid", enchanter.areItemsInvalid(base, null)); } @DisplayName("Items are invalid if base is stacked.") @Test void testStackedBaseInvalid() { - var base = new ItemStack(goodMat); + var base = goodType.createItemStack(); base.setAmount(64); - var addition = new ItemStack(goodMat); + var addition = goodType.createItemStack(); assertThat("Items are invalid", enchanter.areItemsInvalid(base, addition)); } @DisplayName("Items are invalid base and addition do not match.") @Test void testDifferentAddition() { - var base = new ItemStack(goodMat); - var addition = new ItemStack(badMat); + var base = goodType.createItemStack(); + var addition = badType.createItemStack(); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition)); } @DisplayName("Items are valid if base and addition match.") @Test void testSame() { - var base = new ItemStack(goodMat); - var addition = new ItemStack(goodMat); + var base = goodType.createItemStack(); + var addition = goodType.createItemStack(); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition), is(false)); addition.setAmount(64); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition), is(false)); @@ -128,7 +127,7 @@ void testSame() { @DisplayName("Items are valid if addition is enchanted book.") @Test void testEnchantedBookAddition() { - var base = new ItemStack(goodMat); + var base = goodType.createItemStack(); var addition = new ItemStack(Material.ENCHANTED_BOOK); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition), is(false)); } @@ -155,8 +154,11 @@ void beforeEach() { var plugin = mock(Plugin.class); when(plugin.getServer()).thenReturn(server); + itemStack = goodType.createItemStack(); + registry = mock(EnchantableBlockRegistry.class); registration = mock(EnchantableRegistration.class); + Material goodMat = itemStack.getType(); doAnswer(invocation -> registration).when(registry).get(goodMat); doReturn(Set.of(enchantment)).when(registration).getEnchants(); doReturn(true).when(registration).hasEnchantPermission(notNull(), anyString()); @@ -165,7 +167,6 @@ void beforeEach() { doReturn(config).when(registration).getConfig(); enchanter = new AnvilEnchanter(plugin, registry); - itemStack = new ItemStack(goodMat); } @AfterEach @@ -192,6 +193,11 @@ void afterEach() { AnvilView view = mock(AnvilView.class); when(view.getTopInventory()).thenReturn(inventory); when(view.getPlayer()).thenReturn(player); + doAnswer(invocation -> inventory.getItem(invocation.getArgument(0))).when(view).getItem(anyInt()); + doAnswer(invocation -> { + inventory.setItem(invocation.getArgument(0), invocation.getArgument(1)); + return null; + }).when(view).setItem(anyInt(), any()); when(player.getOpenInventory()).thenReturn(view); @@ -211,10 +217,11 @@ void testInvalidItem() { @Test void testUnregisteredMaterial() { var view = (AnvilView) prepareEventPlayer().getOpenInventory(); - view.getTopInventory().setItem(0, new ItemStack(badMat)); + ItemStack badStack = badType.createItemStack(); + view.getTopInventory().setItem(0, badStack); var event = spy(new PrepareAnvilEvent(view, null)); assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); - verify(registry).get(badMat); + verify(registry).get(badStack.getType()); verify(event, times(0)).setResult(any()); } @@ -280,8 +287,11 @@ void testSuccess() { Runnable task = runnableCaptor.getValue(); assertDoesNotThrow(task::run); - var inventory = view.getTopInventory(); - verify(inventory).setItem(2, result); +// var inventory = view.getTopInventory() TODO equals workaround for runnable? +// verify(inventory).setItem( +// eq(2), +// argThat(item -> result.isSimilar(item) && result.getAmount() == item.getAmount()) +// ) } } From b446b5ff4833844416b0f51f1aa5356606b27b5d Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sun, 1 Feb 2026 17:12:20 -0500 Subject: [PATCH 13/20] Fix Appveyor Need to expand property before tests can run --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 895320a..db0f7c9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,7 @@ install: build_script: - mvn clean package -B -DskipTests test_script: - - mvn surefire:test -B + - mvn dependency:properties surefire:test -B cache: - C:\Users\appveyor\.m2 artifacts: From 0f099d8c5121dbd8620dc3d5986a0f9c0fc2d3a4 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sun, 1 Feb 2026 17:13:25 -0500 Subject: [PATCH 14/20] Case-sensitive project ID I guess when I recreated the project after it got deleted the ID generated in lower case. Whoops. --- pom.xml | 2 +- readme.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 739df5b..23edd19 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ sonarcloud - Jikoo_EnchantableBlocks + jikoo_enchantableblocks jikoo https://sonarcloud.io java diff --git a/readme.md b/readme.md index 4673286..80dec60 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,8 @@ ## EnchantableBlocks [![Build](https://github.com/Jikoo/EnchantableBlocks/actions/workflows/ci.yml/badge.svg)](https://github.com/Jikoo/EnchantableBlocks/actions/workflows/ci.yml) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Jikoo_EnchantableBlocks&metric=alert_status)](https://sonarcloud.io/dashboard?id=Jikoo_EnchantableBlocks) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Jikoo_EnchantableBlocks&metric=coverage)](https://sonarcloud.io/dashboard?id=Jikoo_EnchantableBlocks) -[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jikoo_EnchantableBlocks&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=Jikoo_EnchantableBlocks) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jikoo_enchantableblocks&metric=alert_status)](https://sonarcloud.io/dashboard?id=jikoo_enchantableblocks) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=jikoo_enchantableblocks&metric=coverage)](https://sonarcloud.io/dashboard?id=jikoo_enchantableblocks) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=jikoo_enchantableblocks&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=jikoo_enchantableblocks) EnchantableBlocks is a Bukkit plugin adding effects for enchantments on blocks. From eb3434d97144f8925fd802944dfb36172f61e852 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Mon, 2 Feb 2026 09:24:38 -0500 Subject: [PATCH 15/20] Clean up some code smells --- .../enchantableblocks/util/EmptyCookingRecipe.java | 13 +++++++++++-- .../mock/enchantments/EnchantmentMocks.java | 1 - .../util/enchant/BlockAnvilBehaviorTest.java | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java index 6319fc5..51064f4 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java @@ -28,11 +28,14 @@ public ItemStack getItemStack() { return new ItemStack(Material.AIR); } - @SuppressWarnings("MethodDoesntCallSuperMethod") @NotNull @Override public RecipeChoice clone() { - return this; + try { + return (RecipeChoice) super.clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException(e); + } } @Override @@ -45,6 +48,12 @@ public boolean equals(Object obj) { // Since this is an unmodifiable object, all instances are equal. return obj != null && obj.getClass().equals(this.getClass()); } + + @Override + public int hashCode() { + // As above - all instances are equal. + return 0; + } }, 0, 0); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java index 5f384f7..97a9720 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java @@ -417,7 +417,6 @@ EnchantConfig anvilTarget(TagKey targetKey) { EnchantConfig tableTarget(TagKey targetKey) { Tag target = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(targetKey); - doReturn(false).when(enchantment).isTreasure(); ENCHANTING_TABLE_TAGS.add(target); return anvilTarget(targetKey); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java b/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java index 37c5580..1859a5f 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java @@ -30,7 +30,7 @@ @DisplayName("Feature: Configurable AnvilBehavior for EnchantableBlocks.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class BlockAnvilBehaviorTest { +class BlockAnvilBehaviorTest { private EnchantableRegistration registration; From dbf1fb32901963eae0130480dbf7b473b2a13e5c Mon Sep 17 00:00:00 2001 From: Jikoo Date: Mon, 2 Feb 2026 09:56:54 -0500 Subject: [PATCH 16/20] Fix test ensuring result is set --- .../listener/AnvilEnchanterTest.java | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java index 3e2a9e9..0009821 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java @@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.doAnswer; @@ -141,6 +142,7 @@ class PrepareAnvilTest { private EnchantableRegistration registration; private ItemStack itemStack; private ArgumentCaptor runnableCaptor; + private AnvilView view; @BeforeEach void beforeEach() { @@ -167,22 +169,18 @@ void beforeEach() { doReturn(config).when(registration).getConfig(); enchanter = new AnvilEnchanter(plugin, registry); - } - @AfterEach - void afterEach() { - var server = Bukkit.getServer(); - when(server.getScheduler()).thenReturn(null); + view = prepareView(); } - private @NotNull Player prepareEventPlayer() { + private @NotNull AnvilView prepareView() { var player = mock(Player.class); var world = WorldMocks.newWorld("world"); when(player.getWorld()).thenReturn(world); var inventory = InventoryMocks.newAnvilMock(); inventory.setItem(0, itemStack.clone()); - var additionItem = new ItemStack(Material.ENCHANTED_BOOK); + var additionItem = ItemType.ENCHANTED_BOOK.createItemStack(); var additionMeta = additionItem.getItemMeta(); if (additionMeta instanceof EnchantmentStorageMeta storageMeta) { storageMeta.addStoredEnchant(enchantment, enchantment.getMaxLevel(), true); @@ -201,12 +199,17 @@ void afterEach() { when(player.getOpenInventory()).thenReturn(view); - return player; + return view; + } + + @AfterEach + void afterEach() { + var server = Bukkit.getServer(); + when(server.getScheduler()).thenReturn(null); } @Test void testInvalidItem() { - var view = (AnvilView) prepareEventPlayer().getOpenInventory(); view.getTopInventory().setItem(0, null); var event = spy(new PrepareAnvilEvent(view, null)); assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); @@ -216,7 +219,6 @@ void testInvalidItem() { @Test void testUnregisteredMaterial() { - var view = (AnvilView) prepareEventPlayer().getOpenInventory(); ItemStack badStack = badType.createItemStack(); view.getTopInventory().setItem(0, badStack); var event = spy(new PrepareAnvilEvent(view, null)); @@ -228,7 +230,7 @@ void testUnregisteredMaterial() { @Test void testNoPermission() { doReturn(false).when(registration).hasEnchantPermission(notNull(), anyString()); - var event = spy(new PrepareAnvilEvent((AnvilView) prepareEventPlayer().getOpenInventory(), null)); + var event = spy(new PrepareAnvilEvent(view, null)); assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); verify(registration).hasEnchantPermission(notNull(), anyString()); verify(event, times(0)).setResult(any()); @@ -236,7 +238,6 @@ void testNoPermission() { @Test void testNoChange() { - var view = (AnvilView) prepareEventPlayer().getOpenInventory(); view.getTopInventory().setItem(1, itemStack.clone()); var event = spy(new PrepareAnvilEvent(view, null)); @@ -247,7 +248,6 @@ void testNoChange() { @ParameterizedTest @MethodSource("getSlots") void testChangePostCalculate(int... slots) { - var view = (AnvilView) prepareEventPlayer().getOpenInventory(); var event = spy(new PrepareAnvilEvent(view, null)); assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); @@ -275,9 +275,17 @@ static Collection getSlots() { @Test void testSuccess() { - var view = (AnvilView) prepareEventPlayer().getOpenInventory(); var event = spy(new PrepareAnvilEvent(view, null)); + // Because we can't override .equals for mocks and we need to verify that items + // are unchanged before setting the result, we instead want to ensure that "copy" + // is actually the same object. + // This does cause the original item to be manipulated as a side effect when producing the result. + ItemStack base = view.getTopInventory().getItem(0); + doReturn(base).when(base).clone(); + ItemStack addition = view.getTopInventory().getItem(1); + doReturn(addition).when(addition).clone(); + assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); verify(event).setResult(notNull()); assertThat("Result is set", event.getResult(), is(notNullValue())); @@ -287,11 +295,11 @@ void testSuccess() { Runnable task = runnableCaptor.getValue(); assertDoesNotThrow(task::run); -// var inventory = view.getTopInventory() TODO equals workaround for runnable? -// verify(inventory).setItem( -// eq(2), -// argThat(item -> result.isSimilar(item) && result.getAmount() == item.getAmount()) -// ) + var inventory = view.getTopInventory(); + verify(inventory).setItem( + eq(2), + argThat(item -> result.isSimilar(item) && result.getAmount() == item.getAmount()) + ); } } From bca919ea02b16eb6f1a271a1dee4ca3a89746b17 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Mon, 2 Feb 2026 10:07:45 -0500 Subject: [PATCH 17/20] Use provided recipe to skip cache in FurnaceSmeltEvent --- .../impl/furnace/EnchantableFurnace.java | 25 +++++++++++++------ .../block/impl/furnace/FurnaceListener.java | 1 - 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java index cbaa825..d43f833 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java @@ -143,6 +143,7 @@ public boolean shouldPause(final @Nullable Event event) { ItemStack input; ItemStack result; + CookingRecipe recipe; if (event instanceof FurnaceSmeltEvent smeltEvent) { // Special case FurnaceSmeltEvent: smelt has not completed, input and result are different. // Decrease input for post-smelt @@ -150,14 +151,17 @@ public boolean shouldPause(final @Nullable Event event) { input.setAmount(input.getAmount() - 1); // Use post-smelt result result = smeltEvent.getResult(); + // FurnaceSmeltEvent is the only pause event that provides the active recipe. + recipe = smeltEvent.getRecipe(); } else { // In all other cases use current contents of furnace. FurnaceInventory inventory = furnace.getInventory(); input = inventory.getSmelting(); result = inventory.getResult(); + recipe = null; } - return shouldPause(furnace, input, result); + return shouldPause(furnace, input, result, recipe); } /** @@ -172,7 +176,9 @@ public boolean shouldPause(final @Nullable Event event) { private boolean shouldPause( final @NotNull Furnace furnace, final @Nullable ItemStack input, - final @Nullable ItemStack result) { + final @Nullable ItemStack result, + final @Nullable CookingRecipe recipe + ) { if (!this.canPause()) { return false; } @@ -182,13 +188,15 @@ private boolean shouldPause( return false; } - return isFreezableState(furnace.getInventory(), input, result); + return isFreezableState(furnace.getInventory(), input, result, recipe); } private boolean isFreezableState( final @NotNull FurnaceInventory inventory, final @Nullable ItemStack input, - final @Nullable ItemStack result) { + final @Nullable ItemStack result, + @Nullable CookingRecipe recipe + ) { // Is there no input? if (ItemUtil.isEmpty(input)) { return true; @@ -202,7 +210,10 @@ private boolean isFreezableState( } } - CookingRecipe recipe = getRegistration().getFurnaceRecipe(inventory); + // If the recipe wasn't provided, look it up. + if (recipe == null) { + recipe = getRegistration().getFurnaceRecipe(inventory); + } // Does the current smelting item not have a recipe? if (recipe == null) { @@ -263,7 +274,7 @@ public boolean resume(boolean checkState) { } FurnaceInventory inventory = furnace.getInventory(); - if (checkState && isFreezableState(inventory, inventory.getSmelting(), inventory.getResult())) { + if (checkState && isFreezableState(inventory, inventory.getSmelting(), inventory.getResult(), null)) { return false; } @@ -377,7 +388,7 @@ static void update( plugin.getServer().getScheduler().runTask(plugin, () -> { boolean shouldPause = - enchantableFurnace.shouldPause(furnace, inventory.getSmelting(), inventory.getResult()); + enchantableFurnace.shouldPause(furnace, inventory.getSmelting(), inventory.getResult(), null); if (enchantableFurnace.isPaused() == shouldPause) { enchantableFurnace.updating = false; return; diff --git a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java index 0af3ad2..39341e5 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java @@ -93,7 +93,6 @@ void onFurnaceSmelt(final @NotNull FurnaceSmeltEvent event) { } if (enchantableFurnace.shouldPause(event)) { - // TODO can use Paper's addition of recipe to skip cache? plugin.getServer().getScheduler().runTask(plugin, enchantableFurnace::pause); } } From e3a47e29f9478b8923fa5f38652c85e1c2402c6f Mon Sep 17 00:00:00 2001 From: Jikoo Date: Mon, 2 Feb 2026 10:09:53 -0500 Subject: [PATCH 18/20] Last Sonar nag --- .../listener/AnvilEnchanterTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java index 0009821..a6520b2 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java @@ -188,18 +188,18 @@ void beforeEach() { additionItem.setItemMeta(additionMeta); inventory.setItem(1, additionItem); - AnvilView view = mock(AnvilView.class); - when(view.getTopInventory()).thenReturn(inventory); - when(view.getPlayer()).thenReturn(player); - doAnswer(invocation -> inventory.getItem(invocation.getArgument(0))).when(view).getItem(anyInt()); + AnvilView anvilView = mock(AnvilView.class); + when(anvilView.getTopInventory()).thenReturn(inventory); + when(anvilView.getPlayer()).thenReturn(player); + doAnswer(invocation -> inventory.getItem(invocation.getArgument(0))).when(anvilView).getItem(anyInt()); doAnswer(invocation -> { inventory.setItem(invocation.getArgument(0), invocation.getArgument(1)); return null; - }).when(view).setItem(anyInt(), any()); + }).when(anvilView).setItem(anyInt(), any()); - when(player.getOpenInventory()).thenReturn(view); + when(player.getOpenInventory()).thenReturn(anvilView); - return view; + return anvilView; } @AfterEach From 5c51cf91c382cd1d0ad09e27d73417ec2bd2e0c8 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Mon, 2 Feb 2026 10:58:48 -0500 Subject: [PATCH 19/20] Enforce Paper requirement --- .../jikoo/enchantableblocks/EnchantableBlocksPlugin.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java index e5c1691..31b8f32 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java @@ -26,6 +26,15 @@ public void onLoad() { @Override public void onEnable() { + try { + Class.forName("io.papermc.paper.configuration.ServerConfiguration"); + } catch (ClassNotFoundException e) { + getLogger().severe("EnchantableBlocks requires Paper; Spigot's enchantment API is missing features."); + getLogger().severe("Please vote for https://hub.spigotmc.org/jira/browse/SPIGOT-7838 for Spigot support."); + getServer().getPluginManager().disablePlugin(this); + return; + } + this.saveDefaultConfig(); // Register generic listeners for block management. From 0705a84cbaf6b560704420262dae455e6394e8ed Mon Sep 17 00:00:00 2001 From: Jikoo Date: Mon, 2 Feb 2026 10:59:22 -0500 Subject: [PATCH 20/20] Update api-version --- src/main/resources/plugin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5edbafa..e0ccc37 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,7 +3,7 @@ name: EnchantableBlocks main: com.github.jikoo.enchantableblocks.EnchantableBlocksPlugin version: ${project.version} author: Jikoo -api-version: "1.20.6" +api-version: "1.21.11" libraries: - it.unimi.dsi:fastutil:${versions.fastutil}