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..853e437 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -6,8 +6,33 @@ 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"
+ permissions:
+ contents: write
+ pull-requests: write
+ 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}}
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:
diff --git a/pom.xml b/pom.xml
index 1655ea6..23edd19 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,25 +7,23 @@
com.github.jikoo
enchantableblocks
EnchantableBlocks
- 4.1.1-SNAPSHOT
+ 5.0.0-SNAPSHOT
jar
UTF-8
UTF-8
- 17
- 17
- 17
+ 21
8.5.18
+
+
sonarcloud
- 17
- 17
- Jikoo_EnchantableBlocks
+ jikoo_enchantableblocks
jikoo
https://sonarcloud.io
java
@@ -61,8 +59,8 @@
- spigot-repo
- https://hub.spigotmc.org/nexus/content/groups/public/
+ papermc
+ https://repo.papermc.io/repository/maven-public/
jitpack.io
@@ -78,9 +76,9 @@
provided
- org.spigotmc
- spigot-api
- 1.21-R0.1-SNAPSHOT
+ io.papermc.paper
+ paper-api
+ 1.21.11-R0.1-SNAPSHOT
provided
@@ -93,7 +91,7 @@
com.github.jikoo
planarenchanting
- 2.0.1
+ fe946163f8
compile
@@ -144,6 +142,17 @@
maven-compiler-plugin
3.14.1
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ properties
+
+
+
+
org.apache.maven.plugins
maven-surefire-plugin
@@ -151,6 +160,7 @@
2
false
+ @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off
@@ -165,11 +175,6 @@
true
-
com.github.jikoo:*
diff --git a/readme.md b/readme.md
index 4673286..80dec60 100644
--- a/readme.md
+++ b/readme.md
@@ -1,8 +1,8 @@
## EnchantableBlocks
[](https://github.com/Jikoo/EnchantableBlocks/actions/workflows/ci.yml)
-[](https://sonarcloud.io/dashboard?id=Jikoo_EnchantableBlocks)
-[](https://sonarcloud.io/dashboard?id=Jikoo_EnchantableBlocks)
-[](https://sonarcloud.io/dashboard?id=Jikoo_EnchantableBlocks)
+[](https://sonarcloud.io/dashboard?id=jikoo_enchantableblocks)
+[](https://sonarcloud.io/dashboard?id=jikoo_enchantableblocks)
+[](https://sonarcloud.io/dashboard?id=jikoo_enchantableblocks)
EnchantableBlocks is a Bukkit plugin adding effects for enchantments on blocks.
diff --git a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java
index 0661479..31b8f32 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);
@@ -41,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.
@@ -85,7 +79,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;
}
@@ -93,7 +87,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/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 34aea12..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
@@ -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.
*/
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/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/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/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java
index 89377ef..51064f4 100644
--- a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java
+++ b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java
@@ -48,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/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/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}
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 89c9346..41990be 100644
--- a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java
+++ b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java
@@ -1,38 +1,40 @@
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.hamcrest.CoreMatchers;
+import org.bukkit.inventory.ItemType;
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.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 {
@@ -77,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());
@@ -195,6 +197,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..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
@@ -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 {
@@ -62,6 +64,7 @@ class EnchantableFurnaceTest {
private Block block;
private ItemStack itemStack;
private ConfigurationSection storage;
+ private ItemStack input;
@BeforeAll
void beforeAll() {
@@ -74,20 +77,22 @@ 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);
+ 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;
@@ -124,8 +129,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 +140,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 +151,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 +160,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;
@@ -340,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);
@@ -355,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(similar(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));
@@ -376,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));
@@ -389,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",
@@ -404,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(
@@ -420,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(
@@ -436,9 +432,9 @@ 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(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));
@@ -452,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",
@@ -563,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));
@@ -716,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();
@@ -734,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/block/impl/furnace/FurnaceListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java
index c3e7abd..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
@@ -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;
@@ -51,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;
@@ -64,11 +44,35 @@
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.isItem;
+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 {
private CookingRecipe> recipe;
+ private ItemStack input;
@BeforeAll
void beforeAll() {
@@ -85,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 -> {
@@ -103,10 +113,6 @@ class FurnaceEventsTest {
private FurnaceListener listener;
private Block block;
- @BeforeAll
- static void beforeAll() {
- }
-
@BeforeEach
void beforeEach() {
var server = Bukkit.getServer();
@@ -152,7 +158,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.
@@ -201,7 +208,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",
@@ -213,7 +220,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",
@@ -225,10 +232,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();
}
@@ -236,10 +243,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();
}
@@ -248,10 +255,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.")
@@ -259,30 +266,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(), similar(recipe.getResult()));
- assertThat("Result must be modified", event.getResult(), is(not(recipe.getResult())));
+ assertThat("Result must be similar", event.getResult(), isSimilar(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));
}
@@ -294,14 +301,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.")
@@ -310,7 +317,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);
@@ -323,11 +330,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);
@@ -341,11 +348,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);
@@ -362,7 +369,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);
@@ -374,7 +381,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());
@@ -384,7 +391,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();
@@ -396,7 +403,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());
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..a6520b2 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,47 @@
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.argThat;
+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 +94,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 +128,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));
}
@@ -142,6 +142,7 @@ class PrepareAnvilTest {
private EnchantableRegistration registration;
private ItemStack itemStack;
private ArgumentCaptor runnableCaptor;
+ private AnvilView view;
@BeforeEach
void beforeEach() {
@@ -155,8 +156,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,23 +169,18 @@ void beforeEach() {
doReturn(config).when(registration).getConfig();
enchanter = new AnvilEnchanter(plugin, registry);
- itemStack = new ItemStack(goodMat);
- }
- @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);
@@ -189,18 +188,28 @@ void afterEach() {
additionItem.setItemMeta(additionMeta);
inventory.setItem(1, additionItem);
- AnvilView view = mock(AnvilView.class);
- when(view.getTopInventory()).thenReturn(inventory);
- when(view.getPlayer()).thenReturn(player);
+ 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(anvilView).setItem(anyInt(), any());
+
+ when(player.getOpenInventory()).thenReturn(anvilView);
- when(player.getOpenInventory()).thenReturn(view);
+ return anvilView;
+ }
- return player;
+ @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));
@@ -210,18 +219,18 @@ 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());
}
@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());
@@ -229,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));
@@ -240,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));
@@ -268,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()));
@@ -281,7 +296,10 @@ void testSuccess() {
assertDoesNotThrow(task::run);
var inventory = view.getTopInventory();
- verify(inventory).setItem(2, result);
+ verify(inventory).setItem(
+ eq(2),
+ argThat(item -> result.isSimilar(item) && result.getAmount() == item.getAmount())
+ );
}
}
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",
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/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 extends Keyed> constantClazz;
- try {
- //noinspection unchecked
- constantClazz = (Class extends Keyed>) 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/enchantments/EnchantmentMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java
index 8958c09..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
@@ -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,114 @@ 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 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);
+ 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/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