diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3c64295..fe3f41b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,6 @@ updates: interval: monthly time: "10:00" open-pull-requests-limit: 10 + ignore: + # Spigot-API is pinned to the point when the hard fork occurred. + - dependency-name: "org.spigotmc:*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b1e4e7..d44d4de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,5 +7,6 @@ on: jobs: build: - uses: Jikoo/PlanarActions/.github/workflows/ci_gradle_sonar.yml@master - secrets: inherit + uses: Jikoo/PlanarActions/.github/workflows/ci_gradle.yml@master + with: + include-artifacts: "./enchanting-bundler/build/libs/*.jar" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a08b587..379a409 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -6,7 +6,8 @@ on: jobs: run-ci: uses: Jikoo/PlanarActions/.github/workflows/ci_gradle_sonar.yml@master - secrets: inherit + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} approve-and-merge-dependabot: if: "github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'" needs: [ "run-ci" ] diff --git a/.gitignore b/.gitignore index 8e65e4c..7804587 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ bin/ out/ *.iml +# VS Code +.vscode/ + # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* diff --git a/README.MD b/README.MD index 92b0d83..67ecb4a 100644 --- a/README.MD +++ b/README.MD @@ -17,14 +17,14 @@ systems and divide them up into readable, manageable, customizable segments. ### Anvil Enchanting Anvil enchanting is primarily accessed via the -[`Anvil`](enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java). -This is a class containing several simple ways to modify anvil behaviors. +[`AnvilCreator`](enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilCreator.java). +This is a wrapper allowing instantiation of implementation-specific anvil data. For basic vanilla-style combination, all you need is an ordinary `Anvil`. ```java class MyAnvilHandler implements Listener { - private final Anvil anvil = new Anvil(); + private final Anvil anvil = AnvilCreator.create(); @EventHandler private void onPrepareAnvil(PrepareAnvilEvent event) { AnvilView view = event.getView(); @@ -32,50 +32,80 @@ class MyAnvilHandler implements Listener { event.setResult(result.item()); - // Note: depending on server implementation, may need to update costs on a 0-tick delay. + // Note: Depending on the server implementation, you may need to + // update costs on a 0-tick delay. It is also advisable to ensure + // that the result item has not been changed when doing so. view.setRepairItemCostAmount(result.materialCost()); view.setRepairCost(result.levelCost()); } } ``` -For specific tweaks (i.e. removing or changing enchantment level cap) you can provide the -`Anvil` with a different `AnvilBehavior` implementation. +For specific tweaks (i.e. removing or changing enchantment level cap) you can provide +a different `AnvilBehavior` implementation. + +> [!IMPORTANT] +> For better compatibility, `Anvil` has two main implementations: component-based +> (using Paper's `DataComponentType`) and meta-based (using the Spigot API and `ItemMeta`). +> For detecting compatibility, PlanarEnchanting uses `ServerCapabilities.DATA_COMPONENT`. Allowing conflicting enchantments to be added: ```java -Anvil anvil = new Anvil(new AnvilBehavior() { - @Override - public boolean getEnchantsConflict(Enchantment enchant1, Enchantment enchant2) { - return false; - } -}); +// PlanarForge is the default Anvil implementation, wrapping a WorkPiece provider, +// an AnvilBehavior, and an AnvilFunctionsProvider. +Anvil anvil = new PlanarForge( + AnvilCreator::createComponentPiece, + new ComponentVanillaBehavior() { + @Override + public boolean getEnchantsConflict(Enchantment enchant1, Enchantment enchant2) { + return false; + } + }, + ComponentAnvilFunctions.INSTANCE +); ``` If you have even more specific needs but still want to leverage certain vanilla-style functionality, -you can write your own anvil functionality. +you can write your own Anvil implementation. You can also implement your own +[AnvilFunctions](enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java) +if you need additional behavior changes. An implementation that only allows the input to be renamed: ```java -class RenameOnlyAnvil extends Anvil { +@NullMarked +class RenameOnlyAnvil implements Anvil { + + protected final Function> createPiece; + protected final AnvilBehavior behavior; + protected final AnvilFunctionsProvider functions; + + RenameOnlyAnvil( + Function> createPiece, + AnvilBehavior behavior, + AnvilFunctionsProvider functions + ) { + this.createPiece = createPiece; + this.behavior = behavior; + this.functions = functions; + } + @Override - public void getResult(@NotNull AnvilView view) { - AnvilState state = new AnvilState(view); + public AnvilResult getResult(AnvilView view) { + WorkPiece piece = createPiece.apply(view); // Require first item to be set, second item to be unset. - if (ItemUtil.isEmpty(state.getBase().getItem()) - || !ItemUtil.isEmpty(state.getAddition().getItem())) { + if (piece.getBase().isEmpty() || !piece.getAddition().isEmpty()) { return AnvilResult.EMPTY; } // Apply base cost. - apply(state, AnvilFunctions.PRIOR_WORK_LEVEL_COST); + piece.apply(behavior, functions.setPriorWorkCost()); // Apply rename. - apply(state, AnvilFunctions.RENAME); + piece.apply(behavior, functions.rename()); - return forge(state); + return piece.forge(); } } ``` @@ -96,37 +126,34 @@ the surface. You may manipulate incompatibility between enchantments and enchant Allow enchanting `Material.STONE` with enchantments usually available for tools: ```java +@NullMarked class StoneEnchantListener extends TableEnchantListener { - // Set up table. private final EnchantingTable table = new EnchantingTable( List.of( Enchantment.DIG_SPEED, Enchantment.DURABILITY, Enchantment.LOOT_BONUS_BLOCKS, - Enchantment.SILK_TOUCH), - Enchantability.forMaterial(Material.STONE_PICKAXE)); + Enchantment.SILK_TOUCH + ), + Enchantabilies.forMaterial(Material.STONE_PICKAXE) + ); - StoneEnchantListener(@NotNull Plugin plugin) { + StoneEnchantListener(Plugin plugin) { super(plugin); } @Override - protected @Nullable EnchantingTable getTable( - @NotNull Player player, - @NotNull ItemStack enchanted) { + protected EnchantingTable getTable(Player player, ItemStack enchanted) { // Use stored instance. return table; } @Override - protected boolean isIneligible( - @NotNull Player player, - @NotNull ItemStack enchanted) { + protected boolean isIneligible(Player player, ItemStack enchanted) { // Only allow enchanting stone. return itemStack.getType() != Material.STONE; } - } ``` @@ -139,4 +166,6 @@ JitPack supports Gradle, Maven, SBT, and Leiningen. ## For Developers +The project is compiled using Gradle via the `build` task, i.e. `./gradlew build`. Generated source files can be recreated from Minecraft internals with the `generate` task. +The generator module is not built by default so as to not require the Paper server to be compiled. diff --git a/build.gradle.kts b/build.gradle.kts index 4b66666..f647d5f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,12 +2,23 @@ import java.lang.Runtime import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent +plugins { + `java-library` + jacoco + alias(libs.plugins.shadow) apply false +} + +repositories { + mavenCentral() +} + subprojects { apply(plugin = "java-library") repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") + maven("https://jitpack.io/") } tasks.withType().configureEach { @@ -22,14 +33,49 @@ subprojects { filteringCharset = Charsets.UTF_8.name() } - tasks.withType().configureEach { - // Use as many cores as possible to run tests. - maxParallelForks = Runtime.getRuntime().availableProcessors() - jvmArgs("-Xshare:off") - testLogging { - showStackTraces = true - exceptionFormat = TestExceptionFormat.FULL - events(TestLogEvent.STANDARD_OUT) + if ("enchanting-generator" != this.name) { + apply(plugin = "jacoco") + + val mockitoAgent: Configuration = configurations.create("mockitoAgent") + dependencies { + compileOnly(rootProject.libs.org.jspecify.jspecify) + compileOnly(rootProject.libs.org.jetbrains.annotations) + + testCompileOnly(rootProject.libs.org.jspecify.jspecify) + testImplementation(rootProject.libs.org.hamcrest.hamcrest) + testImplementation(rootProject.libs.org.mockito.mockito.core) + mockitoAgent(rootProject.libs.org.mockito.mockito.core) { isTransitive = false } + testImplementation(rootProject.libs.com.jparams.to.string.verifier) + } + + testing { + suites { + named("test") { + useJUnitJupiter("6.0.2") + } + } + } + + tasks.withType().configureEach { + // Use as many cores as possible to run tests. + maxParallelForks = Runtime.getRuntime().availableProcessors() + // As Bukkit is very heavily statically initialized, don't reuse forks. + forkEvery = 1 + jvmArgs("-Xshare:off", "-javaagent:${mockitoAgent.asPath}") + testLogging { + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + events(TestLogEvent.STANDARD_OUT) + } + finalizedBy(tasks.jacocoTestReport) + } + + tasks.jacocoTestReport { + enabled = true + reports { + // Produce XML report for Sonar + xml.required = true + } } } } diff --git a/enchanting-bundler/build.gradle.kts b/enchanting-bundler/build.gradle.kts new file mode 100644 index 0000000..8bc2251 --- /dev/null +++ b/enchanting-bundler/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + alias(libs.plugins.shadow) +} + +dependencies { + compileOnly(libs.org.spigotmc.spigot.api) + implementation(project(":enchanting-common", configuration = "shadowRuntimeElements")) { + exclude(group = "com.github.jikoo", module = "planarwrappers") + } + implementation(project(":enchanting-components")) { + exclude(group = "io.papermc.paper", module = "paper-api") + } + implementation(project(":enchanting-meta")) + + testImplementation(libs.org.spigotmc.spigot.api) +} + +tasks.jar { + enabled = false +} + +tasks.shadowJar { + dependsOn("classes") + archiveClassifier.set("") + + archiveBaseName = "${project.name}" +} + +tasks.assemble { + dependsOn(tasks.shadowJar) +} + +artifacts { + add("default", tasks.shadowJar) +} diff --git a/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilCreator.java b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilCreator.java new file mode 100644 index 0000000..0bb39b1 --- /dev/null +++ b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilCreator.java @@ -0,0 +1,60 @@ +package com.github.jikoo.planarenchanting.anvil; + +import com.github.jikoo.planarenchanting.util.ServerCapabilities; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; + +/** + * Accessor for creating an appropriate {@link Anvil} for the platform. + */ +@NullMarked +public final class AnvilCreator { + + /** + * Create a new platform-dependent {@link Anvil}. It will use vanilla-style behavior to produce + * results. + * + * @return the anvil implementation + */ + public static Anvil create() { + if (ServerCapabilities.DATA_COMPONENT) { + return new PlanarForge<>(AnvilCreator::createComponentPiece, new ComponentVanillaBehavior(), ComponentAnvilFunctions.INSTANCE); + } else { + return new PlanarForge<>(AnvilCreator::createMetaPiece, new MetaVanillaBehavior(), MetaAnvilFunctions.INSTANCE); + } + } + + /** + * Create a new anvil {@link WorkPiece} based on Paper's {@code DataComponent}. + * + * @param view the AnvilView to operate on + * @return the resulting work piece + * @see ServerCapabilities#DATA_COMPONENT + */ + public static WorkPiece createComponentPiece(AnvilView view) { + return new WorkPiece<>(new ComponentViewState(view), ComponentTemperer.INSTANCE); + } + + /** + * Create a new anvil {@link WorkPiece} based on the Bukkit API. + * + *

Note that this relies on methods which are now deprecated in Paper. It is advisable to use + * {@link #createComponentPiece(AnvilView)} instead if possible.

+ * + *

To prevent creation of numerous duplicate copies of the item's + * {@link org.bukkit.inventory.meta.ItemMeta}, operations are performed on a + * {@link MetaCachedStack}. Changes are only applied to the result when the piece is tempered.

+ * + * @param view the AnvilView to operate on + * @return the resulting work piece + * @see #createComponentPiece(AnvilView) + * @see ServerCapabilities#DATA_COMPONENT + */ + public static WorkPiece createMetaPiece(AnvilView view) { + return new WorkPiece<>(new MetaViewState(view), MetaTemperer.INSTANCE); + } + + private AnvilCreator() {} + +} diff --git a/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/table/Enchantabilities.java b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/table/Enchantabilities.java new file mode 100644 index 0000000..810f8ed --- /dev/null +++ b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/table/Enchantabilities.java @@ -0,0 +1,61 @@ +package com.github.jikoo.planarenchanting.table; + +import com.github.jikoo.planarenchanting.util.ServerCapabilities; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.Nullable; + +/** + * A wrapper allowing access to the {@link Enchantability} of an item or item type. + */ +public class Enchantabilities { + + /** + * Get the {@code Enchantability} of a {@link Material}. Will return {@code null} if not + * enchantable in an enchanting table. + * + * @param material the {@code Material} + * @return the {@code Enchantability} if enchantable + */ + public static @Nullable Enchantability of(Material material) { + return DELEGATE.of(material); + } + + /** + * Get the {@code Enchantability} of an {@link ItemType}. Will return {@code null} if not + * enchantable in an enchanting table. + * + * @param type the {@code ItemType} + * @return the {@code Enchantability} if enchantable + */ + public static @Nullable Enchantability of(ItemType type) { + return DELEGATE.of(type); + } + + /** + * Get the {@code Enchantability} of an {@link ItemStack}. Will return {@code null} if not + * enchantable in an enchanting table. + * + * @param item the {@code ItemStack} + * @return the {@code Enchantability} if enchantable + */ + public static @Nullable Enchantability of(ItemStack item) { + return DELEGATE.of(item); + } + + private static final EnchantabilityProvider DELEGATE; + + static { + if (ServerCapabilities.DATA_COMPONENT) { + DELEGATE = new ComponentEnchantabilities(); + } else { + DELEGATE = new MetaEnchantabilities(); + } + } + + private Enchantabilities() { + throw new IllegalStateException("Cannot instantiate static helper method container."); + } + +} diff --git a/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/util/DelegateEnchantProvider.java b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/util/DelegateEnchantProvider.java new file mode 100644 index 0000000..a778e25 --- /dev/null +++ b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/util/DelegateEnchantProvider.java @@ -0,0 +1,18 @@ +package com.github.jikoo.planarenchanting.util; + +import com.github.jikoo.planarenchanting.util.EnchantData.Provider; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class DelegateEnchantProvider implements Provider { + + private final Provider delegate = + ServerCapabilities.DATA_COMPONENT ? new ComponentEnchantProvider() : new MetaEnchantProvider(); + + @Override + public EnchantData of(Enchantment enchantment) { + return delegate.of(enchantment); + } + +} diff --git a/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/util/ServerCapabilities.java b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/util/ServerCapabilities.java new file mode 100644 index 0000000..6f5c3e1 --- /dev/null +++ b/enchanting-bundler/src/main/java/com/github/jikoo/planarenchanting/util/ServerCapabilities.java @@ -0,0 +1,15 @@ +package com.github.jikoo.planarenchanting.util; + +/** + * A container for constants tracking server capabilities. + */ +public final class ServerCapabilities { + + /** Whether the server supports Paper's {@code DataComponent} API. */ + public static final boolean DATA_COMPONENT = ComponentCapability.get(); + + private ServerCapabilities() { + throw new IllegalStateException("Cannot instantiate static helper method container."); + } + +} diff --git a/enchanting-bundler/src/main/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider b/enchanting-bundler/src/main/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider new file mode 100644 index 0000000..7d13224 --- /dev/null +++ b/enchanting-bundler/src/main/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider @@ -0,0 +1 @@ +com.github.jikoo.planarenchanting.util.DelegateEnchantProvider diff --git a/enchanting-bundler/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilitiesTest.java b/enchanting-bundler/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilitiesTest.java new file mode 100644 index 0000000..a9c5dc3 --- /dev/null +++ b/enchanting-bundler/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilitiesTest.java @@ -0,0 +1,95 @@ +package com.github.jikoo.planarenchanting.table; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +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.mockStatic; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +@TestInstance(Lifecycle.PER_CLASS) +class EnchantabilitiesTest { + + private MockedStatic bukkit; + + @BeforeAll + void setUp() throws ClassNotFoundException { + bukkit = mockStatic(); + // Set up registries. + // Note that Registry.MATERIAL is an enum-based faux registry and cannot + // be mocked effectively without tricks not available through Mockito alone + // because it is initialized in place rather than fetched from the server. + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(ignored -> mock(Registry.class)); + // Touch Registry to initialize static fields. + Class.forName("org.bukkit.Registry"); + + // Mock values for Registry.ITEM to allow Material#isItem on constants. + doAnswer(invocation -> { + NamespacedKey key = invocation.getArgument(0); + if (key.getKey().equals("AIR")) { + return null; + } + return mock(ItemType.class); + }).when(Registry.ITEM).get(any()); + } + + @AfterAll + void tearDown() { + bukkit.close(); + } + + @ParameterizedTest + @CsvSource({ "DIAMOND_PICKAXE,true", "AIR,false" }) + void ofMaterial(Material material, boolean isEnchantable) { + assertThat( + "Enchantability matches expectation", + Enchantabilities.of(material), + is(isEnchantable ? not(nullValue()) : nullValue()) + ); + } + + @ParameterizedTest + @CsvSource({ "diamond_pickaxe,true", "air,false" }) + void ofItemType(String key, boolean isEnchantable) { + NamespacedKey namespacedKey = NamespacedKey.minecraft(key); + ItemType itemType = mock(ItemType.class); + doReturn(namespacedKey).when(itemType).getKey(); + + assertThat( + "Enchantability matches expectation", + Enchantabilities.of(itemType), + is(isEnchantable ? not(nullValue()) : nullValue()) + ); + } + + @ParameterizedTest + @CsvSource({ "DIAMOND_PICKAXE,true", "AIR,false" }) + void ofItemStack(Material material, boolean isEnchantable) { + ItemStack itemStack = mock(); + doReturn(material).when(itemStack).getType(); + + assertThat( + "Enchantability matches expectation", + Enchantabilities.of(itemStack), + is(isEnchantable ? not(nullValue()) : nullValue()) + ); + } + +} diff --git a/enchanting-bundler/src/test/java/com/github/jikoo/planarenchanting/util/ComponentCapableTest.java b/enchanting-bundler/src/test/java/com/github/jikoo/planarenchanting/util/ComponentCapableTest.java new file mode 100644 index 0000000..d8661e6 --- /dev/null +++ b/enchanting-bundler/src/test/java/com/github/jikoo/planarenchanting/util/ComponentCapableTest.java @@ -0,0 +1,67 @@ +package com.github.jikoo.planarenchanting.util; + +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.mockStatic; + +import com.github.jikoo.planarenchanting.anvil.AnvilCreator; +import com.github.jikoo.planarenchanting.table.Enchantabilities; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.bukkit.inventory.view.AnvilView; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.MockedStatic; + +@TestInstance(Lifecycle.PER_CLASS) +public class ComponentCapableTest { + + private MockedStatic componentCapabilities; + private MockedStatic bukkit; + + @BeforeAll + void setUp() { + componentCapabilities = mockStatic(); + componentCapabilities.when(ComponentCapability::get).thenReturn(true); + bukkit = mockStatic(); + // Set up registries so Enchantment can be mocked. + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(ignored -> mock(Registry.class)); + } + + @AfterAll + void tearDown() { + componentCapabilities.close(); + bukkit.close(); + } + + @Test + void enchantabilitiesDelegate() { + // Use ItemType to ensure DataComponent is checked + assertThrows(LinkageError.class, () -> Enchantabilities.of(mock(ItemType.class))); + } + + @Test + void enchantDataDelegate() { + assertThrows(LinkageError.class, () -> new DelegateEnchantProvider().of(mock()).getWeight()); + } + + @Test + void anvilDelegate() { + // Set up non-empty first item so we're sure to hit a function application. + ItemStack base = mock(); + doReturn(Material.DIRT).when(base).getType(); + doReturn(1).when(base).getAmount(); + AnvilView view = mock(); + doReturn(base).when(view).getItem(0); + assertThrows(LinkageError.class, () -> AnvilCreator.create().getResult(view)); + } + +} diff --git a/enchanting-common/build.gradle.kts b/enchanting-common/build.gradle.kts new file mode 100644 index 0000000..51d3f64 --- /dev/null +++ b/enchanting-common/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.shadow) +} + +repositories { + maven("https://hub.spigotmc.org/nexus/content/groups/public/") +} + +dependencies { + compileOnly(libs.org.spigotmc.spigot.api) + implementation(libs.com.github.jikoo.planarwrappers) + + testImplementation(libs.org.spigotmc.spigot.api) +} + +sourceSets { + main { + java.srcDirs("src/generated/java") + } +} + +tasks.shadowJar { + relocate("com.github.jikoo.planarwrappers", "com.github.jikoo.planarenchanting.lib.planarwrappers") + minimize() +} diff --git a/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableData.java b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableData.java new file mode 100644 index 0000000..010cfb4 --- /dev/null +++ b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableData.java @@ -0,0 +1,111 @@ +package com.github.jikoo.planarenchanting.anvil; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; +import org.bukkit.NamespacedKey; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Pre-baked data used for {@link RepairMaterial}. + * + *

This file was generated from Minecraft 1.21.11. Regenerate it rather than modify.

+ */ +@Generated("com.github.jikoo.planarenchanting.generator.impl.RepairMaterialsGenerator") +@NullMarked +final class BakedRepairableData { + + private BakedRepairableData() {} + + static Map<@Nullable NamespacedKey, @Nullable NamespacedKey> getTags() { + Map<@Nullable NamespacedKey, @Nullable NamespacedKey> map = new HashMap<>(); + // + map.put(NamespacedKey.fromString("minecraft:chainmail_boots"), NamespacedKey.fromString("minecraft:repairs_chain_armor")); + map.put(NamespacedKey.fromString("minecraft:chainmail_chestplate"), NamespacedKey.fromString("minecraft:repairs_chain_armor")); + map.put(NamespacedKey.fromString("minecraft:chainmail_helmet"), NamespacedKey.fromString("minecraft:repairs_chain_armor")); + map.put(NamespacedKey.fromString("minecraft:chainmail_leggings"), NamespacedKey.fromString("minecraft:repairs_chain_armor")); + map.put(NamespacedKey.fromString("minecraft:copper_axe"), NamespacedKey.fromString("minecraft:copper_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:copper_boots"), NamespacedKey.fromString("minecraft:repairs_copper_armor")); + map.put(NamespacedKey.fromString("minecraft:copper_chestplate"), NamespacedKey.fromString("minecraft:repairs_copper_armor")); + map.put(NamespacedKey.fromString("minecraft:copper_helmet"), NamespacedKey.fromString("minecraft:repairs_copper_armor")); + map.put(NamespacedKey.fromString("minecraft:copper_hoe"), NamespacedKey.fromString("minecraft:copper_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:copper_leggings"), NamespacedKey.fromString("minecraft:repairs_copper_armor")); + map.put(NamespacedKey.fromString("minecraft:copper_pickaxe"), NamespacedKey.fromString("minecraft:copper_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:copper_shovel"), NamespacedKey.fromString("minecraft:copper_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:copper_spear"), NamespacedKey.fromString("minecraft:copper_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:copper_sword"), NamespacedKey.fromString("minecraft:copper_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:diamond_axe"), NamespacedKey.fromString("minecraft:diamond_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:diamond_boots"), NamespacedKey.fromString("minecraft:repairs_diamond_armor")); + map.put(NamespacedKey.fromString("minecraft:diamond_chestplate"), NamespacedKey.fromString("minecraft:repairs_diamond_armor")); + map.put(NamespacedKey.fromString("minecraft:diamond_helmet"), NamespacedKey.fromString("minecraft:repairs_diamond_armor")); + map.put(NamespacedKey.fromString("minecraft:diamond_hoe"), NamespacedKey.fromString("minecraft:diamond_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:diamond_leggings"), NamespacedKey.fromString("minecraft:repairs_diamond_armor")); + map.put(NamespacedKey.fromString("minecraft:diamond_pickaxe"), NamespacedKey.fromString("minecraft:diamond_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:diamond_shovel"), NamespacedKey.fromString("minecraft:diamond_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:diamond_spear"), NamespacedKey.fromString("minecraft:diamond_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:diamond_sword"), NamespacedKey.fromString("minecraft:diamond_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:golden_axe"), NamespacedKey.fromString("minecraft:gold_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:golden_boots"), NamespacedKey.fromString("minecraft:repairs_gold_armor")); + map.put(NamespacedKey.fromString("minecraft:golden_chestplate"), NamespacedKey.fromString("minecraft:repairs_gold_armor")); + map.put(NamespacedKey.fromString("minecraft:golden_helmet"), NamespacedKey.fromString("minecraft:repairs_gold_armor")); + map.put(NamespacedKey.fromString("minecraft:golden_hoe"), NamespacedKey.fromString("minecraft:gold_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:golden_leggings"), NamespacedKey.fromString("minecraft:repairs_gold_armor")); + map.put(NamespacedKey.fromString("minecraft:golden_pickaxe"), NamespacedKey.fromString("minecraft:gold_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:golden_shovel"), NamespacedKey.fromString("minecraft:gold_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:golden_spear"), NamespacedKey.fromString("minecraft:gold_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:golden_sword"), NamespacedKey.fromString("minecraft:gold_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:iron_axe"), NamespacedKey.fromString("minecraft:iron_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:iron_boots"), NamespacedKey.fromString("minecraft:repairs_iron_armor")); + map.put(NamespacedKey.fromString("minecraft:iron_chestplate"), NamespacedKey.fromString("minecraft:repairs_iron_armor")); + map.put(NamespacedKey.fromString("minecraft:iron_helmet"), NamespacedKey.fromString("minecraft:repairs_iron_armor")); + map.put(NamespacedKey.fromString("minecraft:iron_hoe"), NamespacedKey.fromString("minecraft:iron_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:iron_leggings"), NamespacedKey.fromString("minecraft:repairs_iron_armor")); + map.put(NamespacedKey.fromString("minecraft:iron_pickaxe"), NamespacedKey.fromString("minecraft:iron_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:iron_shovel"), NamespacedKey.fromString("minecraft:iron_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:iron_spear"), NamespacedKey.fromString("minecraft:iron_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:iron_sword"), NamespacedKey.fromString("minecraft:iron_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:leather_boots"), NamespacedKey.fromString("minecraft:repairs_leather_armor")); + map.put(NamespacedKey.fromString("minecraft:leather_chestplate"), NamespacedKey.fromString("minecraft:repairs_leather_armor")); + map.put(NamespacedKey.fromString("minecraft:leather_helmet"), NamespacedKey.fromString("minecraft:repairs_leather_armor")); + map.put(NamespacedKey.fromString("minecraft:leather_leggings"), NamespacedKey.fromString("minecraft:repairs_leather_armor")); + map.put(NamespacedKey.fromString("minecraft:netherite_axe"), NamespacedKey.fromString("minecraft:netherite_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:netherite_boots"), NamespacedKey.fromString("minecraft:repairs_netherite_armor")); + map.put(NamespacedKey.fromString("minecraft:netherite_chestplate"), NamespacedKey.fromString("minecraft:repairs_netherite_armor")); + map.put(NamespacedKey.fromString("minecraft:netherite_helmet"), NamespacedKey.fromString("minecraft:repairs_netherite_armor")); + map.put(NamespacedKey.fromString("minecraft:netherite_hoe"), NamespacedKey.fromString("minecraft:netherite_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:netherite_leggings"), NamespacedKey.fromString("minecraft:repairs_netherite_armor")); + map.put(NamespacedKey.fromString("minecraft:netherite_pickaxe"), NamespacedKey.fromString("minecraft:netherite_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:netherite_shovel"), NamespacedKey.fromString("minecraft:netherite_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:netherite_spear"), NamespacedKey.fromString("minecraft:netherite_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:netherite_sword"), NamespacedKey.fromString("minecraft:netherite_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:shield"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:stone_axe"), NamespacedKey.fromString("minecraft:stone_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:stone_hoe"), NamespacedKey.fromString("minecraft:stone_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:stone_pickaxe"), NamespacedKey.fromString("minecraft:stone_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:stone_shovel"), NamespacedKey.fromString("minecraft:stone_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:stone_spear"), NamespacedKey.fromString("minecraft:stone_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:stone_sword"), NamespacedKey.fromString("minecraft:stone_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:turtle_helmet"), NamespacedKey.fromString("minecraft:repairs_turtle_helmet")); + map.put(NamespacedKey.fromString("minecraft:wolf_armor"), NamespacedKey.fromString("minecraft:repairs_wolf_armor")); + map.put(NamespacedKey.fromString("minecraft:wooden_axe"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:wooden_hoe"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:wooden_pickaxe"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:wooden_shovel"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:wooden_spear"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + map.put(NamespacedKey.fromString("minecraft:wooden_sword"), NamespacedKey.fromString("minecraft:wooden_tool_materials")); + // + return map; + } + + static Map<@Nullable NamespacedKey, List<@Nullable NamespacedKey>> getLists() { + Map<@Nullable NamespacedKey, List<@Nullable NamespacedKey>> map = new HashMap<>(); + // + map.put(NamespacedKey.fromString("minecraft:elytra"), List.of(NamespacedKey.fromString("minecraft:phantom_membrane"))); + map.put(NamespacedKey.fromString("minecraft:mace"), List.of(NamespacedKey.fromString("minecraft:breeze_rod"))); + // + return map; + } + +} diff --git a/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/table/BakedEnchantableData.java b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/table/BakedEnchantableData.java new file mode 100644 index 0000000..231c735 --- /dev/null +++ b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/table/BakedEnchantableData.java @@ -0,0 +1,109 @@ +package com.github.jikoo.planarenchanting.table; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import javax.annotation.processing.Generated; +import org.bukkit.NamespacedKey; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Pre-baked data used as a fallthrough for {@link Enchantable}. + * + *

This file was generated from Minecraft 1.21.11. Regenerate it rather than modify.

+ */ +@Generated("com.github.jikoo.planarenchanting.generator.impl.EnchantableGenerator") +@NullMarked +final class BakedEnchantableData { + + private BakedEnchantableData() {} + + static Map> get() { + Map> map = new HashMap<>(); + Function> create = ignored -> new HashSet<>(); + // + map.computeIfAbsent(1, create).add(NamespacedKey.fromString("minecraft:book")); + map.computeIfAbsent(1, create).add(NamespacedKey.fromString("minecraft:bow")); + map.computeIfAbsent(12, create).add(NamespacedKey.fromString("minecraft:chainmail_boots")); + map.computeIfAbsent(12, create).add(NamespacedKey.fromString("minecraft:chainmail_chestplate")); + map.computeIfAbsent(12, create).add(NamespacedKey.fromString("minecraft:chainmail_helmet")); + map.computeIfAbsent(12, create).add(NamespacedKey.fromString("minecraft:chainmail_leggings")); + map.computeIfAbsent(13, create).add(NamespacedKey.fromString("minecraft:copper_axe")); + map.computeIfAbsent(8, create).add(NamespacedKey.fromString("minecraft:copper_boots")); + map.computeIfAbsent(8, create).add(NamespacedKey.fromString("minecraft:copper_chestplate")); + map.computeIfAbsent(8, create).add(NamespacedKey.fromString("minecraft:copper_helmet")); + map.computeIfAbsent(13, create).add(NamespacedKey.fromString("minecraft:copper_hoe")); + map.computeIfAbsent(8, create).add(NamespacedKey.fromString("minecraft:copper_leggings")); + map.computeIfAbsent(13, create).add(NamespacedKey.fromString("minecraft:copper_pickaxe")); + map.computeIfAbsent(13, create).add(NamespacedKey.fromString("minecraft:copper_shovel")); + map.computeIfAbsent(13, create).add(NamespacedKey.fromString("minecraft:copper_spear")); + map.computeIfAbsent(13, create).add(NamespacedKey.fromString("minecraft:copper_sword")); + map.computeIfAbsent(1, create).add(NamespacedKey.fromString("minecraft:crossbow")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_axe")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_boots")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_chestplate")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_helmet")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_hoe")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_leggings")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_pickaxe")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_shovel")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_spear")); + map.computeIfAbsent(10, create).add(NamespacedKey.fromString("minecraft:diamond_sword")); + map.computeIfAbsent(1, create).add(NamespacedKey.fromString("minecraft:fishing_rod")); + map.computeIfAbsent(22, create).add(NamespacedKey.fromString("minecraft:golden_axe")); + map.computeIfAbsent(25, create).add(NamespacedKey.fromString("minecraft:golden_boots")); + map.computeIfAbsent(25, create).add(NamespacedKey.fromString("minecraft:golden_chestplate")); + map.computeIfAbsent(25, create).add(NamespacedKey.fromString("minecraft:golden_helmet")); + map.computeIfAbsent(22, create).add(NamespacedKey.fromString("minecraft:golden_hoe")); + map.computeIfAbsent(25, create).add(NamespacedKey.fromString("minecraft:golden_leggings")); + map.computeIfAbsent(22, create).add(NamespacedKey.fromString("minecraft:golden_pickaxe")); + map.computeIfAbsent(22, create).add(NamespacedKey.fromString("minecraft:golden_shovel")); + map.computeIfAbsent(22, create).add(NamespacedKey.fromString("minecraft:golden_spear")); + map.computeIfAbsent(22, create).add(NamespacedKey.fromString("minecraft:golden_sword")); + map.computeIfAbsent(14, create).add(NamespacedKey.fromString("minecraft:iron_axe")); + map.computeIfAbsent(9, create).add(NamespacedKey.fromString("minecraft:iron_boots")); + map.computeIfAbsent(9, create).add(NamespacedKey.fromString("minecraft:iron_chestplate")); + map.computeIfAbsent(9, create).add(NamespacedKey.fromString("minecraft:iron_helmet")); + map.computeIfAbsent(14, create).add(NamespacedKey.fromString("minecraft:iron_hoe")); + map.computeIfAbsent(9, create).add(NamespacedKey.fromString("minecraft:iron_leggings")); + map.computeIfAbsent(14, create).add(NamespacedKey.fromString("minecraft:iron_pickaxe")); + map.computeIfAbsent(14, create).add(NamespacedKey.fromString("minecraft:iron_shovel")); + map.computeIfAbsent(14, create).add(NamespacedKey.fromString("minecraft:iron_spear")); + map.computeIfAbsent(14, create).add(NamespacedKey.fromString("minecraft:iron_sword")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:leather_boots")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:leather_chestplate")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:leather_helmet")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:leather_leggings")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:mace")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_axe")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_boots")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_chestplate")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_helmet")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_hoe")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_leggings")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_pickaxe")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_shovel")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_spear")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:netherite_sword")); + map.computeIfAbsent(5, create).add(NamespacedKey.fromString("minecraft:stone_axe")); + map.computeIfAbsent(5, create).add(NamespacedKey.fromString("minecraft:stone_hoe")); + map.computeIfAbsent(5, create).add(NamespacedKey.fromString("minecraft:stone_pickaxe")); + map.computeIfAbsent(5, create).add(NamespacedKey.fromString("minecraft:stone_shovel")); + map.computeIfAbsent(5, create).add(NamespacedKey.fromString("minecraft:stone_spear")); + map.computeIfAbsent(5, create).add(NamespacedKey.fromString("minecraft:stone_sword")); + map.computeIfAbsent(1, create).add(NamespacedKey.fromString("minecraft:trident")); + map.computeIfAbsent(9, create).add(NamespacedKey.fromString("minecraft:turtle_helmet")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:wooden_axe")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:wooden_hoe")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:wooden_pickaxe")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:wooden_shovel")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:wooden_spear")); + map.computeIfAbsent(15, create).add(NamespacedKey.fromString("minecraft:wooden_sword")); + // + return map; + } + +} diff --git a/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategory.java b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategory.java similarity index 100% rename from enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategory.java rename to enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategory.java diff --git a/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/util/BakedEnchantData.java b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/util/BakedEnchantData.java new file mode 100644 index 0000000..d9a41ac --- /dev/null +++ b/enchanting-common/src/generated/java/com/github/jikoo/planarenchanting/util/BakedEnchantData.java @@ -0,0 +1,105 @@ +package com.github.jikoo.planarenchanting.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.IntUnaryOperator; +import javax.annotation.processing.Generated; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Pre-baked data used for {@link MetaEnchantProvider}. + * + *

This file was generated from Minecraft 1.21.11. Regenerate it rather than modify.

+ */ +@Generated("com.github.jikoo.planarenchanting.generator.impl.EnchantDataGenerator") +@NullMarked +final class BakedEnchantData { + + static Map<@Nullable NamespacedKey, Function> get() { + Map<@Nullable NamespacedKey, Function> map = new HashMap<>(); + // + map.put(NamespacedKey.fromString("minecraft:aqua_affinity"), create(2, 4, lvl -> 1, lvl -> 41)); + map.put(NamespacedKey.fromString("minecraft:bane_of_arthropods"), create(5, 2, lvl -> 5 + 8 * (lvl - 1), lvl -> 25 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:binding_curse"), create(1, 8, lvl -> 25, lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:blast_protection"), create(2, 4, lvl -> 5 + 8 * (lvl - 1), lvl -> 13 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:breach"), create(2, 4, lvl -> 15 + 9 * (lvl - 1), lvl -> 65 + 9 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:channeling"), create(1, 8, lvl -> 25, lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:density"), create(5, 2, lvl -> 5 + 8 * (lvl - 1), lvl -> 25 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:depth_strider"), create(2, 4, lvl -> 10 + 10 * (lvl - 1), lvl -> 25 + 10 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:efficiency"), create(10, 1, lvl -> 1 + 10 * (lvl - 1), lvl -> 51 + 10 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:feather_falling"), create(5, 2, lvl -> 5 + 6 * (lvl - 1), lvl -> 11 + 6 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:fire_aspect"), create(2, 4, lvl -> 10 + 20 * (lvl - 1), lvl -> 60 + 20 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:fire_protection"), create(5, 2, lvl -> 10 + 8 * (lvl - 1), lvl -> 18 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:flame"), create(2, 4, lvl -> 20, lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:fortune"), create(2, 4, lvl -> 15 + 9 * (lvl - 1), lvl -> 65 + 9 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:frost_walker"), create(2, 4, lvl -> 10 + 10 * (lvl - 1), lvl -> 25 + 10 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:impaling"), create(2, 4, lvl -> 1 + 8 * (lvl - 1), lvl -> 21 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:infinity"), create(1, 8, lvl -> 20, lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:knockback"), create(5, 2, lvl -> 5 + 20 * (lvl - 1), lvl -> 55 + 20 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:looting"), create(2, 4, lvl -> 15 + 9 * (lvl - 1), lvl -> 65 + 9 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:loyalty"), create(5, 2, lvl -> 12 + 7 * (lvl - 1), lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:luck_of_the_sea"), create(2, 4, lvl -> 15 + 9 * (lvl - 1), lvl -> 65 + 9 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:lunge"), create(5, 2, lvl -> 5 + 8 * (lvl - 1), lvl -> 25 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:lure"), create(2, 4, lvl -> 15 + 9 * (lvl - 1), lvl -> 65 + 9 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:mending"), create(2, 4, lvl -> 25 + 25 * (lvl - 1), lvl -> 75 + 25 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:multishot"), create(2, 4, lvl -> 20, lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:piercing"), create(10, 1, lvl -> 1 + 10 * (lvl - 1), lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:power"), create(10, 1, lvl -> 1 + 10 * (lvl - 1), lvl -> 16 + 10 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:projectile_protection"), create(5, 2, lvl -> 3 + 6 * (lvl - 1), lvl -> 9 + 6 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:protection"), create(10, 1, lvl -> 1 + 11 * (lvl - 1), lvl -> 12 + 11 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:punch"), create(2, 4, lvl -> 12 + 20 * (lvl - 1), lvl -> 37 + 20 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:quick_charge"), create(5, 2, lvl -> 12 + 20 * (lvl - 1), lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:respiration"), create(2, 4, lvl -> 10 + 10 * (lvl - 1), lvl -> 40 + 10 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:riptide"), create(2, 4, lvl -> 17 + 7 * (lvl - 1), lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:sharpness"), create(10, 1, lvl -> 1 + 11 * (lvl - 1), lvl -> 21 + 11 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:silk_touch"), create(1, 8, lvl -> 15, lvl -> 65)); + map.put(NamespacedKey.fromString("minecraft:smite"), create(5, 2, lvl -> 5 + 8 * (lvl - 1), lvl -> 25 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:soul_speed"), create(1, 8, lvl -> 10 + 10 * (lvl - 1), lvl -> 25 + 10 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:sweeping_edge"), create(2, 4, lvl -> 5 + 9 * (lvl - 1), lvl -> 20 + 9 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:swift_sneak"), create(1, 8, lvl -> 25 + 25 * (lvl - 1), lvl -> 75 + 25 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:thorns"), create(1, 8, lvl -> 10 + 20 * (lvl - 1), lvl -> 60 + 20 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:unbreaking"), create(5, 2, lvl -> 5 + 8 * (lvl - 1), lvl -> 55 + 8 * (lvl - 1))); + map.put(NamespacedKey.fromString("minecraft:vanishing_curse"), create(1, 8, lvl -> 25, lvl -> 50)); + map.put(NamespacedKey.fromString("minecraft:wind_burst"), create(2, 4, lvl -> 15 + 9 * (lvl - 1), lvl -> 65 + 9 * (lvl - 1))); + // + return map; + } + + static Function create(int weight, int anvilCost, + IntUnaryOperator minModCost, IntUnaryOperator maxModCost) { + return enchant -> new EnchantData() { + @Override + public int getWeight() { + return weight; + } + + @Override + public int getAnvilCost() { + return anvilCost; + } + + @Override + public int getMinModifiedCost(int level) { + return minModCost.applyAsInt(level); + } + + @Override + public int getMaxModifiedCost(int level) { + return maxModCost.applyAsInt(level); + } + + @Override + public boolean isTridentEnchant() { + return enchant.canEnchantItem(new ItemStack(Material.TRIDENT)) + && !enchant.canEnchantItem(new ItemStack(Material.DIAMOND_SWORD)); + } + }; + } + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java new file mode 100644 index 0000000..8a09b84 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java @@ -0,0 +1,18 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.inventory.view.AnvilView; + +/** + * Defines behavior for calculating a result based on the state of an anvil. + */ +public interface Anvil { + + /** + * Produce an {@link AnvilResult} for an anvil based on its inputs. + * + * @param view the {@link AnvilView} to calculate a result for + * @return the {@code AnvilResult} produced + */ + AnvilResult getResult(AnvilView view); + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilBehavior.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilBehavior.java new file mode 100644 index 0000000..54227c1 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilBehavior.java @@ -0,0 +1,61 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +/** + * Defines areas that are most likely to be modified for convenient {@link Anvil} manipulation. + * + * @param the type of the input items + */ +@NullMarked +public interface AnvilBehavior { + + /** + * Get whether an {@link Enchantment} is applicable for an item. + * + * @param enchantment the {@code Enchantment} to check for applicability + * @param base the item that may be enchanted + * @return whether the {@code Enchantment} can be applied + */ + boolean enchantApplies(Enchantment enchantment, T base); + + /** + * Get whether two {@link Enchantment Enchantments} conflict. + * + * @return whether the {@code Enchantments} conflict + */ + default boolean enchantsConflict(Enchantment enchant1, Enchantment enchant2) { + return enchant1.conflictsWith(enchant2); + } + + /** + * Get the maximum level for an {@link Enchantment}. + * + * @return the maximum level for an {@code Enchantment} + */ + default int getEnchantMaxLevel(Enchantment enchantment) { + return enchantment.getMaxLevel(); + } + + /** + * Get whether an item should combine its {@link Enchantment Enchantments} with another item. + * + * @param base the base item + * @param addition the item added + * @return whether items should combine {@code Enchantments} + */ + boolean itemsCombineEnchants(T base, T addition); + + /** + * Get whether an item is repaired by another item. This is not the same as a repair via + * combination of like items! Like items always attempt to combine durability unless otherwise + * specified when calculating anvil result. + * + * @param repaired the item repaired + * @param repairMat the item used to repair + * @return the method determining whether an item is repaired by another item + */ + boolean itemRepairedBy(T repaired, T repairMat); + +} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java similarity index 63% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java rename to enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java index 7a2c89a..957b851 100644 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunction.java @@ -1,12 +1,15 @@ package com.github.jikoo.planarenchanting.anvil; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NullMarked; /** * An interface representing a portion of the functionality of an anvil. By using several in * conjunction, it is possible to mimic vanilla behavior very closely. + * + * @param the type of the input and output items */ -public interface AnvilFunction { +@NullMarked +public interface AnvilFunction { /** * Check if the function is capable of generating a usable result. Note that this may be a quick @@ -15,21 +18,21 @@ public interface AnvilFunction { * cause an error if its return value is respected. * * @param behavior the definition of behaviors for the anvil - * @param state the {@link AnvilState} the state of the {@code AnvilOperation} in use + * @param state the {@link ViewState} of the anvil in use + * @param result the existing result * @return whether the {@link AnvilFunction} can generate an {@link AnvilFunctionResult} */ - boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state); + boolean canApply(AnvilBehavior behavior, ViewState state, T result); /** * Get an {@link AnvilFunctionResult} used to apply the changes from the function based on the - * provided anvil operation state and settings. + * provided anvil work piece and settings. * * @param behavior the definition of behaviors for the anvil - * @param state the {@link AnvilState} the state of the anvil in use + * @param state the {@link ViewState} of the anvil in use + * @param result the existing result * @return the resulting applicable changes */ - @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state); + AnvilFunctionResult getResult(AnvilBehavior behavior, ViewState state, T result); } diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionResult.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionResult.java similarity index 62% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionResult.java rename to enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionResult.java index aadac86..1691b66 100644 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionResult.java +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionResult.java @@ -1,15 +1,20 @@ package com.github.jikoo.planarenchanting.anvil; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; /** * The result of an {@link AnvilFunction}. Used to modify operation state. */ -public interface AnvilFunctionResult { +@NullMarked +public interface AnvilFunctionResult { /** Constant representing a result that does nothing. */ - AnvilFunctionResult EMPTY = new AnvilFunctionResult() {}; + AnvilFunctionResult EMPTY = new AnvilFunctionResult<>() {}; + + @SuppressWarnings("unchecked") + static AnvilFunctionResult empty() { + return (AnvilFunctionResult) EMPTY; + } /** * Get the number of additional levels this function will cost to perform. @@ -31,10 +36,10 @@ default int getMaterialCostIncrease() { } /** - * Modify the given {@link ItemMeta} to reflect the changes applied by a function. + * Modify the given object to reflect the changes applied by a function. * - * @param itemMeta the {@code ItemMeta} to modify + * @param modified the object to modify */ - default void modifyResult(@Nullable ItemMeta itemMeta) {} + default void modifyResult(T modified) {} } diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionsProvider.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionsProvider.java new file mode 100644 index 0000000..279581f --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionsProvider.java @@ -0,0 +1,72 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.jspecify.annotations.NullMarked; + +/** + * Interface defining {@link AnvilFunction AnvilFunctions} required to mimic vanilla behavior. + * + * @param the type of the input and output items + */ +@NullMarked +public interface AnvilFunctionsProvider { + + /** + * Get an {@link AnvilFunction} applying the prior work cost to the result level cost. + * + *

Note that this does not apply the prior work cost to the result item!

+ * + * @return the result level cost prior work application function + */ + AnvilFunction addPriorWorkLevelCost(); + + /** + * Get an {@link AnvilFunction} applying a rename operation. Applies new name and adds associated + * cost. + * + * @return the name application function + */ + AnvilFunction rename(); + + /** + * Get an {@link AnvilFunction} applying the prior work cost to the result item. + * + *

Note that this does not apply the prior work cost to the result level cost!

+ * + * @return the result item prior work application function + */ + AnvilFunction setItemPriorWork(); + + /** + * Get an {@link AnvilFunction} restoring durability to the base item using the secondary item. + * Application incurs material costs. + * + * @return the function for repairing the base item + */ + AnvilFunction repairWithMaterial(); + + /** + * Get an {@link AnvilFunction} restoring durability to the base item by combining its durability + * with that of a secondary item. + * + * @return the function for repairing the base item + */ + AnvilFunction repairWithCombine(); + + /** + * Get an {@link AnvilFunction} applying enchantments from the secondary item to the base item at + * costs mimicking vanilla Java edition. + * + * @return the function for combining enchantments + */ + AnvilFunction combineEnchantsJava(); + + /** + * Get an {@link AnvilFunction} applying enchantments from the secondary item to the base item at + * costs mimicking vanilla Bedrock edition. These are generally significantly lower than those of + * Java edition. + * + * @return the function for combining enchantments + */ + AnvilFunction combineEnchantsBedrock(); + +} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilResult.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilResult.java similarity index 87% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilResult.java rename to enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilResult.java index b5811d7..e41917f 100644 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilResult.java +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilResult.java @@ -2,7 +2,7 @@ import com.github.jikoo.planarenchanting.util.ItemUtil; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; /** * A container for the result of an anvil operation. @@ -14,7 +14,7 @@ * @param levelCost the number of levels to be consumed by the operation * @param materialCost the amount of items to be consumed from the addition slot */ -public record AnvilResult(@NotNull ItemStack item, int levelCost, int materialCost) { +public record AnvilResult(@NonNull ItemStack item, int levelCost, int materialCost) { public static final AnvilResult EMPTY = new AnvilResult(ItemUtil.AIR, 0, 0); diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/CombineEnchants.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/CombineEnchants.java new file mode 100644 index 0000000..7920db2 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/CombineEnchants.java @@ -0,0 +1,181 @@ +package com.github.jikoo.planarenchanting.anvil; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import com.github.jikoo.planarenchanting.util.EnchantData; +import com.github.jikoo.planarenchanting.util.EnchantmentAccess; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +/** + * An {@link AnvilFunction} used to apply and combine enchantments. + * + * @param the type of the input items + */ +@NullMarked +public class CombineEnchants implements AnvilFunction { + + private final EnchantingPlatform platform; + private final EnchantmentAccess access; + + protected CombineEnchants(Platform platform, EnchantmentAccess access) { + this.access = access; + this.platform = platform.platform; + } + + @Override + public boolean canApply(AnvilBehavior behavior, ViewState state, T result) { + return behavior.itemsCombineEnchants(state.getBase(), state.getAddition()); + } + + @Override + public AnvilFunctionResult getResult(AnvilBehavior behavior, ViewState state, T result) { + Map baseEnchants = access.getEnchantments(state.getBase()); + Map additionEnchants = access.getEnchantments(state.getAddition()); + + if (additionEnchants.isEmpty()) { + return AnvilFunctionResult.empty(); + } + + MergeResult mergeResult = getLevelCost( + behavior, + state, + baseEnchants, + additionEnchants + ); + + int finalCost = mergeResult.levelCost < 0 ? state.getAnvilView().getMaximumRepairCost() : mergeResult.levelCost; + + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return finalCost; + } + + @Override + public void modifyResult(T t) { + access.addEnchantments(t, mergeResult.enchantments); + } + }; + + } + + /** + * Produce a {@link MergeResult} for the combination of two sets of enchantments. + * + * @param behavior the {@link AnvilBehavior} controlling enchantment application + * @param state the {@link ViewState} being operated on + * @param baseEnchants the enchantments from the base item + * @param additionEnchants the enchantments from the item being added and merged + * @return the incurred level cost and resulting enchantments + */ + protected MergeResult getLevelCost( + AnvilBehavior behavior, + ViewState state, + Map baseEnchants, + Map additionEnchants + ) { + Map newEnchants = new HashMap<>(baseEnchants); + int levelCost = 0; + T base = state.getBase(); + boolean isFromBook = access.isBook(state.getAddition()); + + for (Entry enchantEntry : additionEnchants.entrySet()) { + Enchantment newEnchantment = enchantEntry.getKey(); + if (behavior.enchantApplies(newEnchantment, base) + && baseEnchants.keySet().stream() + .noneMatch(existingEnchant -> + !Objects.equals(existingEnchant.getKey(), newEnchantment.getKey()) + && behavior.enchantsConflict(existingEnchant, newEnchantment))) { + int baseCost = platform.getAnvilCost(newEnchantment, isFromBook); + int addedLevel = enchantEntry.getValue(); + int oldLevel = baseEnchants.getOrDefault(newEnchantment, 0); + int newLevel = oldLevel == addedLevel ? addedLevel + 1 : Math.max(oldLevel, addedLevel); + newLevel = Math.min(newLevel, behavior.getEnchantMaxLevel(newEnchantment)); + newEnchants.put(newEnchantment, newLevel); + + levelCost += platform.getTotalCost(baseCost, oldLevel, newLevel); + } else { + levelCost += platform.getInapplicableCost(); + } + } + return new MergeResult(levelCost, newEnchants); + } + + protected record MergeResult(int levelCost, Map enchantments) {} + + public enum Platform { + JAVA(new Java()), + BEDROCK(new Bedrock()),; + + private final EnchantingPlatform platform; + + Platform(EnchantingPlatform platform) { + this.platform = platform; + } + } + + private sealed interface EnchantingPlatform { + int getAnvilCost(Enchantment enchantment, boolean isFromBook); + + int getTotalCost(int baseCost, int oldLevel, int newLevel); + + int getInapplicableCost(); + } + + private static final class Java implements EnchantingPlatform { + @Override + public int getAnvilCost(Enchantment enchantment, boolean isFromBook) { + int value = EnchantData.Service.PROVIDER.of(enchantment).getAnvilCost(); + return isFromBook ? Math.max(1, value / 2) : value; + } + + @Override + public int getTotalCost(int baseCost, int oldLevel, int newLevel) { + return baseCost * newLevel; + } + + @Override + public int getInapplicableCost() { + return 1; + } + } + + private static final class Bedrock implements EnchantingPlatform { + @Override + public int getAnvilCost(Enchantment enchantment, boolean isFromBook) { + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + + int cost = data.getAnvilCost(); + + if (isFromBook) { + cost /= 2; + } + + if (data.isTridentEnchant()) { + // Bedrock Edition rarity is 1 tier lower for trident enchantments. + cost /= 2; + } + + return Math.max(1, cost); + } + + @Override + public int getTotalCost(int baseCost, int oldLevel, int newLevel) { + if (oldLevel >= newLevel) { + return 0; + } + + return baseCost * (newLevel - oldLevel); + } + + @Override + public int getInapplicableCost() { + return 0; + } + + } + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/PlanarForge.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/PlanarForge.java new file mode 100644 index 0000000..b751d0c --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/PlanarForge.java @@ -0,0 +1,78 @@ +package com.github.jikoo.planarenchanting.anvil; + +import java.util.function.Function; +import org.bukkit.Material; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; + +/** + * The default {@link Anvil} implementation. Wraps a {@link WorkPiece}, {@link AnvilBehavior}, + * and {@link AnvilFunctionsProvider} to create an {@link AnvilResult} with vanilla parity. + * + * @param the type of the input and output items + */ +@NullMarked +public final class PlanarForge implements Anvil { + + private final Function> createPiece; + private final AnvilBehavior behavior; + private final AnvilFunctionsProvider functions; + + PlanarForge( + Function> createPiece, + AnvilBehavior behavior, + AnvilFunctionsProvider functions + ) { + this.createPiece = createPiece; + this.behavior = behavior; + this.functions = functions; + } + + @Override + public AnvilResult getResult(AnvilView view) { + WorkPiece piece = createPiece.apply(view); + + AnvilInventory anvil = view.getTopInventory(); + ItemStack base = anvil.getItem(0); + if (base == null || base.getType() == Material.AIR || base.getAmount() < 1) { + return AnvilResult.EMPTY; + } + + ItemStack addition = anvil.getItem(1); + if (addition == null || addition.getType() == Material.AIR || addition.getAmount() < 1) { + if (!piece.apply(behavior, functions.rename())) { + // If there isn't a rename occurring, nothing is happening. + return AnvilResult.EMPTY; + } + + // No addition means no other operations to perform. + return piece.temper(); + } + + if (base.getAmount() != 1) { + // Multi-renames are allowed, multi-modifications are not. + // Vanilla allows multi-modifications "for creative" but the way it does it is problematic. + return AnvilResult.EMPTY; + } + + piece.apply(behavior, functions.rename()); + // Apply prior work cost after rename. + // Rename also applies a prior work cost but does not increase it. + piece.apply(behavior, functions.setItemPriorWork()); + + if (!piece.apply(behavior, functions.repairWithMaterial())) { + // Only do combination repair if this is not a material repair. + piece.apply(behavior, functions.repairWithCombine()); + } + + // Differing from vanilla - since we use a custom determination for whether enchantments should + // transfer (which defaults to indirectly mimicking vanilla), enchantments may need to be + // applied from a material repair. + piece.apply(behavior, functions.combineEnchantsJava()); + + return piece.temper(); + } + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/Temperer.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/Temperer.java new file mode 100644 index 0000000..75cd843 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/Temperer.java @@ -0,0 +1,32 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +/** + * Defines behavior required to finalize a workable item into an {@link ItemStack}. + * + * @param the type of the input and output items + */ +@NullMarked +public interface Temperer { + + /** + * Check if an object has been changed while being worked in the anvil. + * + * @param base the original object + * @param addition the object being added to the base + * @param result the current result state + * @return true if the result is meaningfully different + */ + boolean hasChanged(T base, T addition, T result); + + /** + * Transform the working result into a finalized ItemStack. + * + * @param result the current result state + * @return the result as an ItemStack + */ + ItemStack temper(T result); + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/ViewState.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/ViewState.java new file mode 100644 index 0000000..5eb3800 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/ViewState.java @@ -0,0 +1,43 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; + +/** + * A wrapper for an {@link AnvilView} transforming the inputs and output into the correct + * implementation types. + * + * @param the type of the input items + */ +@NullMarked +public interface ViewState { + + /** + * Get the {@link AnvilView} the state is derived from. + * + * @return the {@code AnvilView} + */ + AnvilView getAnvilView(); + + /** + * Get the base input item. + * + * @return the base input item + */ + T getBase(); + + /** + * Get the secondary input item. + * + * @return the secondary input item + */ + T getAddition(); + + /** + * Create a result item copied from the base item. + * + * @return the result item + */ + T createResult(); + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/WorkPiece.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/WorkPiece.java new file mode 100644 index 0000000..45113af --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/anvil/WorkPiece.java @@ -0,0 +1,120 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; + +/** + * A work-in-progress anvil result. + * + * @param the type of the input and output items + */ +@NullMarked +public final class WorkPiece { + + private final ViewState state; + private final Temperer temperer; + final T result; + private int levelCost = 0; + private int materialCost = 0; + + /** + * Create a {@code WorkPiece} instance for the given operation and inventory. + * + * @param state the {@link ViewState} the state is derived from + */ + public WorkPiece(ViewState state, Temperer temperer) { + this.state = state; + this.temperer = temperer; + this.result = this.state.createResult(); + } + + /** + * Get the base input item from the {@link AnvilView}. + * + * @return the base input item + */ + public T getBase() { + return state.getBase(); + } + + /** + * Get the secondary input item from the {@link AnvilView}. + * + * @return the secondary input item + */ + public T getAddition() { + return state.getAddition(); + } + + /** + * Get the number of levels to be consumed by the operation. + * + * @return the number of levels to be consumed by the operation + */ + public int getLevelCost() { + return levelCost; + } + + /** + * Set the number of levels to be consumed by the operation. + * + * @param levelCost the number of levels to be consumed by the operation + */ + public void setLevelCost(int levelCost) { + this.levelCost = levelCost; + } + + /** + * Get the amount of items to be consumed from the addition slot. + * + * @return the amount of items to be consumed from the addition slot + */ + public int getMaterialCost() { + return materialCost; + } + + /** + * Set the amount of items to be consumed from the addition slot. + * + * @param materialCost the amount of items to be consumed from the addition slot + */ + public void setMaterialCost(int materialCost) { + this.materialCost = materialCost; + } + + /** + * Attempt to apply the given {@link AnvilFunction}, modifying the result and costs as necessary. + * Note that a function reporting itself applicable does not guarantee that the result or costs + * will actually differ. + * + * @see AnvilFunction#canApply(AnvilBehavior, ViewState, T) + * @param function the {@code AnvilFunction} to apply + * @return whether the {@link AnvilFunction} could apply + */ + public boolean apply(AnvilBehavior behavior, AnvilFunction function) { + if (!function.canApply(behavior, state, result)) { + return false; + } + + AnvilFunctionResult anvilResult = function.getResult(behavior, state, result); + + anvilResult.modifyResult(result); + setLevelCost(getLevelCost() + anvilResult.getLevelCostIncrease()); + setMaterialCost(getMaterialCost() + anvilResult.getMaterialCostIncrease()); + + return true; + } + + /** + * Finalize the piece, applying any changes required to produce an {@link AnvilResult}. + * + * @return the finalized result + */ + public AnvilResult temper() { + if (temperer.hasChanged(state.getBase(), state.getAddition(), result)) { + return new AnvilResult(temperer.temper(result), levelCost, materialCost); + } + return AnvilResult.EMPTY; + } + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/Enchantability.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/Enchantability.java new file mode 100644 index 0000000..17a50ae --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/Enchantability.java @@ -0,0 +1,17 @@ +package com.github.jikoo.planarenchanting.table; + +import org.jetbrains.annotations.Range; +import org.jspecify.annotations.NullMarked; + +/** + * A representation of how easily an item can be enchanted. + * + *

Note that setting enchantability too high may actually cause some enchantments to be available + * less frequently. For example, {@code minecraft:infinity} is available only when the effective + * enchanting level after modifiers is between two hardcoded values. Higher enchantability + * increases the calculated end number of a randomized bonus range starting at {@code 0}. The higher + * the max, the higher the average. If the total number including the bonus exceeds the maximum, you + * may see very rare enchantments generate at low levels or higher than intended enchantment rarity. + */ +@NullMarked +public record Enchantability(@Range(from = 1, to = Integer.MAX_VALUE) int value) {} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/EnchantabilityProvider.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/EnchantabilityProvider.java new file mode 100644 index 0000000..82308a9 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/EnchantabilityProvider.java @@ -0,0 +1,18 @@ +package com.github.jikoo.planarenchanting.table; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +interface EnchantabilityProvider { + + @Nullable Enchantability of(Material material); + + @Nullable Enchantability of(ItemType itemType); + + @Nullable Enchantability of(ItemStack item); + +} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/EnchantingTable.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/EnchantingTable.java similarity index 94% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/EnchantingTable.java rename to enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/EnchantingTable.java index 79ea840..1c42ce2 100644 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/EnchantingTable.java +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/EnchantingTable.java @@ -1,5 +1,6 @@ package com.github.jikoo.planarenchanting.table; +import com.github.jikoo.planarenchanting.util.EnchantData; import com.github.jikoo.planarwrappers.util.WeightedRandom; import java.util.Collection; import java.util.Collections; @@ -103,10 +104,11 @@ public void setMaxLevel(@NotNull ToIntFunction<@NotNull Enchantment> maxLevel) { Map available = new HashMap<>(); for (Enchantment enchantment : this.enchantments) { + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); // Find a level appropriate for the finalized enchanting level. for (int lvl = maxLevel.applyAsInt(enchantment); lvl >= enchantment.getStartLevel(); --lvl) { - if (enchantQuality >= enchantment.getMinModifiedCost(lvl) - && enchantQuality <= enchantment.getMaxModifiedCost(lvl)) { + if (enchantQuality >= data.getMinModifiedCost(lvl) + && enchantQuality <= data.getMaxModifiedCost(lvl)) { available.put(enchantment, lvl); break; } @@ -126,13 +128,18 @@ public void setMaxLevel(@NotNull ToIntFunction<@NotNull Enchantment> maxLevel) { private void addEnchant( @NotNull Random random, @NotNull Map selected, - @NotNull Map available) { - if (available.isEmpty()) { + @NotNull Map available + ) { + if (available.isEmpty()) { return; } // Select enchantment. - Enchantment choice = WeightedRandom.choose(random, available.keySet(), Enchantment::getWeight); + Enchantment choice = WeightedRandom.choose( + random, + available.keySet(), + enchant -> EnchantData.Service.PROVIDER.of(enchant).getWeight() + ); // Add selected enchantment and remove it from the available listings. selected.put(choice, available.remove(choice)); diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/TableEnchantListener.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/TableEnchantListener.java similarity index 97% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/TableEnchantListener.java rename to enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/TableEnchantListener.java index 2769105..64f975e 100644 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/TableEnchantListener.java +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/table/TableEnchantListener.java @@ -3,7 +3,6 @@ import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import java.util.function.IntSupplier; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -63,7 +62,7 @@ public final void onPrepareItemEnchant(@NotNull PrepareItemEnchantEvent event) { // Force button refresh. This is required for normally unenchantable items. // Waiting a tick fixes desync problems that prevent the client from enchanting // ordinarily un-enchantable objects. - Bukkit.getScheduler().runTaskLater(plugin, () -> event.getView().setOffers(event.getOffers()), 1L); + plugin.getServer().getScheduler().runTaskLater(plugin, () -> event.getView().setOffers(event.getOffers()), 1L); } @EventHandler diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/EnchantData.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/EnchantData.java new file mode 100644 index 0000000..0e86343 --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/EnchantData.java @@ -0,0 +1,76 @@ +package com.github.jikoo.planarenchanting.util; + +import java.util.ServiceLoader; +import org.bukkit.enchantments.Enchantment; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +/** + * Enchantment details that are either not available via the standard Bukkit enchantment + * API or which are fetched differently using Paper's API. + */ +@NullMarked +public interface EnchantData { + + /** + * Get the weight of the enchantment. Higher weights are more likely to be produced by an + * enchanting table. + * + * @return the enchantment weight + */ + int getWeight(); + + /** + * Get the cost modifier of applying the enchantment in an anvil. + * + * @return the anvil cost modifier + */ + int getAnvilCost(); + + /** + * Get the minimum enchanting table roll cost of an enchantment at a certain level. + * + * @param level the level of the enchantment + * @return the minimum roll required for the enchantment at the specified level + */ + int getMinModifiedCost(int level); + + /** + * Get the maximum enchanting table roll cost of an enchantment at a certain level. + * + * @param level the level of the enchantment + * @return the maximum roll required for the enchantment at the specified level + */ + int getMaxModifiedCost(int level); + + /** + * Check if an enchantment is a trident enchantment. + * + * @return true if the enchantment is a trident-exclusive enchantment + */ + boolean isTridentEnchant(); + + /** + * An enchantment data provider. + * + * @see Service#PROVIDER + */ + @NullMarked + interface Provider { + + EnchantData of(Enchantment enchantment); + + } + + @ApiStatus.NonExtendable + interface Service { + + /** + * A {@link Provider} loaded from a service. + */ + Provider PROVIDER = ServiceLoader.load(Provider.class, Provider.class.getClassLoader()) + .findFirst().orElseThrow(); + + } + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/EnchantmentAccess.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/EnchantmentAccess.java new file mode 100644 index 0000000..3d42acd --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/EnchantmentAccess.java @@ -0,0 +1,43 @@ +package com.github.jikoo.planarenchanting.util; + +import java.util.Map; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +/** + * Defines behavior for accessing enchantment details from an item implementation. + * + * @param the type of item + */ +@NullMarked +public interface EnchantmentAccess { + + /** + * Gets whether an item represents an enchanted book. + * + * @param t the item + * @return true if the item represents an enchanted book + */ + boolean isBook(T t); + + /** + * Gets the enchantments from an item. For normal items, this retrieves active enchantments. + * For enchanted books, this retrieves stored enchantments. + * + * @param t the item + * @return the enchantments on the item + */ + Map getEnchantments(T t); + + /** + * Adds enchantments to an item. For normal items, this applies active enchantments. For + * enchanted books, this applies stored enchantments. + * + *

This does not remove enchantments that are not modified!

+ * + * @param t the item + * @param enchantments the set of enchantments to add + */ + void addEnchantments(T t, Map enchantments); + +} diff --git a/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/ItemUtil.java b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/ItemUtil.java new file mode 100644 index 0000000..cbfabdf --- /dev/null +++ b/enchanting-common/src/main/java/com/github/jikoo/planarenchanting/util/ItemUtil.java @@ -0,0 +1,34 @@ +package com.github.jikoo.planarenchanting.util; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NullMarked; + +/** + * A collection of item-related utility functions. + */ +@NullMarked +public final class ItemUtil { + + /** Constant for air itemstacks. */ + public static final ItemStack AIR = new ItemStack(Material.AIR) { + /** + * Don't do this. Make a new item instead. + * + * @throws UnsupportedOperationException when invoked + * @deprecated Material for AIR constant cannot be changed. + */ + @Contract(value = "_ -> fail", pure = true) + @Deprecated(since = "added") + @Override + public void setType(Material type) { + throw new UnsupportedOperationException("Cannot modify AIR constant."); + } + }; + + private ItemUtil() { + throw new IllegalStateException("Cannot instantiate static helper container."); + } + +} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableDataTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableDataTest.java similarity index 99% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableDataTest.java rename to enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableDataTest.java index eb128a0..6cf8b2f 100644 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableDataTest.java +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableDataTest.java @@ -20,4 +20,4 @@ void getLists() { assertThat("Values must be provided", BakedRepairableData.getLists(), is(not(anEmptyMap()))); } -} \ No newline at end of file +} diff --git a/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantsTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantsTest.java new file mode 100644 index 0000000..c20cfb9 --- /dev/null +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantsTest.java @@ -0,0 +1,399 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +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.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import com.github.jikoo.planarenchanting.anvil.CombineEnchants.MergeResult; +import com.github.jikoo.planarenchanting.anvil.CombineEnchants.Platform; +import com.github.jikoo.planarenchanting.util.EnchantData; +import com.github.jikoo.planarenchanting.util.EnchantmentAccess; +import java.util.Map; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; + +@NullMarked +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CombineEnchantsTest { + + private MockedStatic bukkit; + private EnchantmentAccess access; + private AnvilBehavior behavior; + private ViewState state; + private Void resultStack; + + @BeforeAll + void setUp() { + bukkit = mockStatic(Bukkit.class); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(inv -> { + Registry registry = mock(); + doAnswer(invocation -> { + NamespacedKey key = invocation.getArgument(0); + Enchantment enchant = mock(); + doReturn(key).when(enchant).getKey(); + return enchant; + }).when(registry).getOrThrow(any()); + return registry; + }); + } + + @AfterAll + void tearDown() { + bukkit.close(); + } + + @BeforeEach + void beforeEach() { + access = mock(); + behavior = mock(); + state = mock(); + resultStack = mock(); + } + + @Test + void applyCombineTrue() { + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + doReturn(true).when(behavior).itemsCombineEnchants(any(), any()); + + assertThat("Function can apply", function.canApply(behavior, state, resultStack), is(true)); + } + + @Test + void applyCombineFalse() { + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + + assertThat("Function cannot apply", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void getLevelCostEmpty() { + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + MergeResult result = function.getLevelCost(behavior, state, Map.of(), Map.of()); + assertThat( + "Enchantment map for no enchantments is empty", + result.enchantments(), + is(anEmptyMap()) + ); + assertThat("Level cost for no enchantments is zero", result.levelCost(), is(0)); + } + + @ParameterizedTest + @CsvSource({"JAVA,1", "BEDROCK,0"}) + void getLevelCostInapplicableBehavior(Platform platform, int cost) { + Map added = Map.of(mock(), 0); + + CombineEnchants function = new CombineEnchants<>(platform, access); + MergeResult result = function.getLevelCost(behavior, state, Map.of(), added); + + assertThat( + "Enchantment map for an inapplicable enchant is empty", + result.enchantments(), + is(anEmptyMap()) + ); + assertThat( + "Level cost for an inapplicable enchant is expected value", + result.levelCost(), + is(cost) + ); + } + + @ParameterizedTest + @CsvSource({"JAVA,1", "BEDROCK,0"}) + void getLevelCostInapplicableConflict(Platform platform, int cost) { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(true).when(behavior).enchantsConflict(any(), any()); + + Enchantment enchantment = mock(); + doReturn(NamespacedKey.minecraft("a")).when(enchantment).getKey(); + Map base = Map.of(enchantment, 0); + enchantment = mock(); + doReturn(NamespacedKey.minecraft("b")).when(enchantment).getKey(); + Map added = Map.of(enchantment, 0); + + CombineEnchants function = new CombineEnchants<>(platform, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + assertThat( + "Enchantment map for an inapplicable enchant is unchanged", + result.enchantments(), + is(base) + ); + assertThat( + "Level cost for an inapplicable enchant is expected value", + result.levelCost(), + is(cost) + ); + } + + @Test + void getLevelCostNonconflictingAdded() { + doReturn(true).when(behavior).enchantApplies(any(), any()); + + Enchantment baseEnchant = mock(); + doReturn(NamespacedKey.minecraft("a")).when(baseEnchant).getKey(); + Map base = Map.of(baseEnchant, 0); + Enchantment addedEnchant = mock(); + doReturn(NamespacedKey.minecraft("b")).when(addedEnchant).getKey(); + Map added = Map.of(addedEnchant, 0); + + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + assertThat( + "Nonconflicting enchantment is added", + result.enchantments(), + is(allOf(aMapWithSize(2), hasKey(baseEnchant), hasKey(addedEnchant))) + ); + } + + @ParameterizedTest + @CsvSource({"JAVA,10,true", "JAVA,10,false", "BEDROCK,5,true", "BEDROCK,5,false"}) + void getLevelCostMerge(Platform platform, int cost, boolean conflict) { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(2).when(behavior).getEnchantMaxLevel(any()); + doReturn(conflict).when(behavior).enchantsConflict(any(), any()); + + Enchantment enchantment = mock(); + doReturn(NamespacedKey.minecraft("a")).when(enchantment).getKey(); + + Map base = Map.of(enchantment, 1); + Map added = Map.of(enchantment, 1); + + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + doReturn(5).when(data).getAnvilCost(); + + CombineEnchants function = new CombineEnchants<>(platform, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + assertThat( + "Enchantment entries were merged", + result.enchantments(), + is(allOf(not(base), aMapWithSize(base.size()), hasEntry(enchantment, 2))) + ); + assertThat( + "Level cost for enchantment merge is expected value", + result.levelCost(), + is(cost) + ); + } + + @ParameterizedTest + @CsvSource({"JAVA,2,1,2", "JAVA,1,2,2", "BEDROCK,2,1,0", "BEDROCK,1,2,1"}) + void getLevelCostUsesHigher( + Platform platform, + int baseLevel, + int addedLevel, + int levelsCharged + ) { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(2).when(behavior).getEnchantMaxLevel(any()); + + Enchantment enchantment = mock(); + doReturn(NamespacedKey.minecraft("a")).when(enchantment).getKey(); + + Map base = Map.of(enchantment, baseLevel); + Map added = Map.of(enchantment, addedLevel); + + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + int anvilCost = 5; + doReturn(anvilCost).when(data).getAnvilCost(); + + CombineEnchants function = new CombineEnchants<>(platform, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + int max = Math.max(baseLevel, addedLevel); + assertThat( + "Enchantment entries were merged", + result.enchantments(), + is(both(aMapWithSize(base.size())).and(hasEntry(enchantment, max))) + ); + assertThat( + "Level cost for enchantment merge is expected value", + result.levelCost(), + is(levelsCharged * anvilCost) + ); + } + + @ParameterizedTest + @CsvSource({"1,3,2", "2,2,1"}) + void getLevelCostCapsToMax(int baseLevel, int addedLevel, int maxLevel) { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(maxLevel).when(behavior).getEnchantMaxLevel(any()); + + Enchantment enchantment = mock(); + doReturn(NamespacedKey.minecraft("a")).when(enchantment).getKey(); + + Map base = Map.of(enchantment, baseLevel); + Map added = Map.of(enchantment, addedLevel); + + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + int anvilCost = 5; + doReturn(anvilCost).when(data).getAnvilCost(); + + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + assertThat( + "Enchantment entries were merged", + result.enchantments(), + is(both(aMapWithSize(1)).and(hasEntry(enchantment, maxLevel))) + ); + assertThat( + "Level cost for enchantment merge is expected value", + result.levelCost(), + is(maxLevel * anvilCost) + ); + } + + @ParameterizedTest + @EnumSource(Platform.class) + void getLevelCostBook(Platform platform) { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(1).when(behavior).getEnchantMaxLevel(any()); + doReturn(true).when(access).isBook(any()); + + Enchantment enchantment = mock(); + doReturn(NamespacedKey.minecraft("a")).when(enchantment).getKey(); + + Map base = Map.of(); + Map added = Map.of(enchantment, 1); + + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + int anvilCost = 5; + doReturn(anvilCost).when(data).getAnvilCost(); + + CombineEnchants function = new CombineEnchants<>(platform, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + assertThat( + "Enchantment was added", + result.enchantments(), + is(both(aMapWithSize(1)).and(hasEntry(enchantment, 1))) + ); + assertThat( + "Level cost is expected value", + result.levelCost(), + is(2) + ); + } + + @Test + void getLevelCostBedrockTrident() { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(1).when(behavior).getEnchantMaxLevel(any()); + + Enchantment enchantment = mock(); + doReturn(NamespacedKey.minecraft("a")).when(enchantment).getKey(); + + Map base = Map.of(); + Map added = Map.of(enchantment, 1); + + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + int anvilCost = 5; + doReturn(anvilCost).when(data).getAnvilCost(); + doReturn(true).when(data).isTridentEnchant(); + + CombineEnchants function = new CombineEnchants<>(Platform.BEDROCK, access); + MergeResult result = function.getLevelCost(behavior, state, base, added); + + assertThat( + "Enchantment was added", + result.enchantments(), + is(both(aMapWithSize(1)).and(hasEntry(enchantment, 1))) + ); + assertThat( + "Level cost is expected value", + result.levelCost(), + is(2) + ); + } + + @Test + void getResultNoAddedEnchants() { + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + doReturn(Map.of()).when(access).getEnchantments(any()); + + assertThat( + "No enchants added yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResult() { + doReturn(true).when(behavior).enchantApplies(any(), any()); + doReturn(1).when(behavior).getEnchantMaxLevel(any()); + + Enchantment enchantment = mock(); + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + doReturn(5).when(data).getAnvilCost(); + doReturn(Map.of()).doReturn(Map.of(enchantment, 1)).when(access).getEnchantments(any()); + + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat( + "Result is not empty", + result, + is(not(AnvilFunctionResult.empty())) + ); + assertThat("Final cost is expected", result.getLevelCostIncrease(), is(5)); + assertThat("Material cost is not specified", result.getMaterialCostIncrease(), is(0)); + + verify(access, never()).addEnchantments(any(), any()); + result.modifyResult(resultStack); + verify(access).addEnchantments(any(), any()); + } + + @Test + void getResultNegative() { + doReturn(true).when(behavior).enchantApplies(any(), any()); + + AnvilView view = mock(); + doReturn(99).when(view).getMaximumRepairCost(); + doReturn(view).when(state).getAnvilView(); + + Enchantment enchantment = mock(); + EnchantData data = EnchantData.Service.PROVIDER.of(enchantment); + doReturn(5).when(data).getAnvilCost(); + doReturn(Map.of(enchantment, -2)).when(access).getEnchantments(any()); + + CombineEnchants function = new CombineEnchants<>(Platform.JAVA, access); + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat( + "Result is not empty", + result, + is(not(AnvilFunctionResult.empty())) + ); + assertThat("Final cost is expected", result.getLevelCostIncrease(), is(99)); + } + +} diff --git a/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/PlanarForgeTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/PlanarForgeTest.java new file mode 100644 index 0000000..d15611d --- /dev/null +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/PlanarForgeTest.java @@ -0,0 +1,205 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +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.never; +import static org.mockito.Mockito.verify; + +import org.bukkit.Material; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.Mockito; + +@NullMarked +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PlanarForgeTest { + + private AnvilView view; + private AnvilInventory inventory; + private ItemStack base; + private ItemStack addition; + private AnvilFunctionsProvider functions; + private PlanarForge anvil; + private AnvilResult forgeResult; + + @BeforeEach + void beforeEach() { + view = mock(); + inventory = mock(); + doReturn(inventory).when(view).getTopInventory(); + + base = mock(); + doReturn(1).when(base).getAmount(); + doReturn(base).when(inventory).getItem(0); + addition = mock(); + doReturn(1).when(addition).getAmount(); + doReturn(addition).when(inventory).getItem(1); + + ViewState state = mock(); + + AnvilBehavior behavior = mock(); + functions = mock(Mockito.RETURNS_MOCKS); + forgeResult = mock(); + anvil = new PlanarForge<>( + view -> { + WorkPiece piece = mock(); + doReturn(forgeResult).when(piece).temper(); + doAnswer(invocation -> { + AnvilFunction function = invocation.getArgument(1); + if (function.canApply(behavior, state, mock())) { + function.getResult(behavior, state, mock()); + return true; + } + return false; + }).when(piece).apply(any(), any()); + return piece; + }, + behavior, + functions + ); + } + + @Test + void getResultNullBase() { + doReturn(null).when(inventory).getItem(0); + + assertThat( + "Result is empty for null base.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + } + + @Test + void getResultAirBase() { + doReturn(Material.AIR).when(base).getType(); + + assertThat( + "Result is empty for empty base.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + } + + @Test + void getResultEmptyBase() { + doReturn(0).when(base).getAmount(); + + assertThat( + "Result is empty for empty base.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + } + + @Test + void getResultNullAddition() { + doReturn(null).when(inventory).getItem(1); + + assertThat( + "Result is empty for null addition and no rename.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + + // No addition means only a rename. + verify(functions).rename(); + } + + @Test + void getResultAirAddition() { + doReturn(Material.AIR).when(addition).getType(); + + assertThat( + "Result is empty for empty addition and no rename.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + + verify(functions).rename(); + } + + @Test + void getResultEmptyAddition() { + doReturn(0).when(addition).getAmount(); + + assertThat( + "Result is empty for empty addition and no rename.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + + verify(functions).rename(); + } + + @Test + void getResultEmptyAdditionRename() { + doReturn(Material.AIR).when(addition).getType(); + AnvilFunction rename = mock(); + doReturn(true).when(rename).canApply(any(), any(), any()); + AnvilFunctionResult result = mock(); + doReturn(result).when(rename).getResult(any(), any(), any()); + doReturn(rename).when(functions).rename(); + + assertThat( + "Result is forged for rename with no addition", + anvil.getResult(view), + is(sameInstance(forgeResult)) + ); + + verify(functions).rename(); + verify(functions, never()).setItemPriorWork(); + } + + @Test + void getResultMultipleBase() { + doReturn(2).when(base).getAmount(); + + assertThat( + "Result is empty for multiple base with addition.", + anvil.getResult(view).item().getType() == Material.AIR, + is(true) + ); + + verify(functions, never()).rename(); + } + + @Test + void getResult() { + assertThat("Result is expected value", anvil.getResult(view), is(forgeResult)); + + verify(functions).rename(); + verify(functions).setItemPriorWork(); + verify(functions).repairWithMaterial(); + verify(functions).repairWithCombine(); + verify(functions).combineEnchantsJava(); + } + + @Test + void getResultMaterialRepair() { + AnvilFunction repairWithMaterial = mock(); + doReturn(true).when(repairWithMaterial).canApply(any(), any(), any()); + AnvilFunctionResult result = mock(); + doReturn(result).when(repairWithMaterial).getResult(any(), any(), any()); + doReturn(repairWithMaterial).when(functions).repairWithMaterial(); + + assertThat("Result is expected value", anvil.getResult(view), is(forgeResult)); + + verify(functions).rename(); + verify(functions).setItemPriorWork(); + verify(functions).repairWithMaterial(); + verify(functions, never()).repairWithCombine(); + verify(functions).combineEnchantsJava(); + } + +} diff --git a/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/WorkPieceTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/WorkPieceTest.java new file mode 100644 index 0000000..d0bcc5f --- /dev/null +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/anvil/WorkPieceTest.java @@ -0,0 +1,82 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class WorkPieceTest { + + private ViewState state; + private Temperer temperer; + + @BeforeEach + void setUp() { + state = mock(); + temperer = mock(); + } + + @Test + void applyFalse() { + AnvilFunction function = mock(); + WorkPiece piece = new WorkPiece<>(state, temperer); + + assertThat( + "AnvilFunction that cannot apply results in no changes", + piece.apply(mock(), function), + is(false) + ); + + verify(function, never()).getResult(any(), any(), any()); + } + + @Test + void applyTrue() { + AnvilFunction function = mock(); + doReturn(true).when(function).canApply(any(), any(), any()); + AnvilFunctionResult result = mock(); + doReturn(result).when(function).getResult(any(), any(), any()); + WorkPiece piece = new WorkPiece<>(state, temperer); + + assertThat( + "AnvilFunction that can apply is applied", + piece.apply(mock(), function), + is(true) + ); + + verify(function).getResult(any(), any(), any()); + verify(result).modifyResult(any()); + } + + @Test + void temperChanged() { + doReturn(true).when(temperer).hasChanged(any(), any(), any()); + WorkPiece workPiece = new WorkPiece<>(state, temperer); + + assertThat( + "Changed work piece does not result in empty result", + workPiece.temper(), + is(not(sameInstance(AnvilResult.EMPTY))) + ); + } + + @Test + void temperUnchanged() { + WorkPiece workPiece = new WorkPiece<>(state, temperer); + + assertThat( + "Unchanged work piece produces empty result", + workPiece.temper(), + is(AnvilResult.EMPTY) + ); + } + +} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/BakedEnchantableDataTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/BakedEnchantableDataTest.java similarity index 99% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/BakedEnchantableDataTest.java rename to enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/BakedEnchantableDataTest.java index c274293..46a60bf 100644 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/BakedEnchantableDataTest.java +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/BakedEnchantableDataTest.java @@ -14,4 +14,4 @@ void get() { assertThat("Values must be provided", BakedEnchantableData.get(), is(not(anEmptyMap()))); } -} \ No newline at end of file +} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategoryTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategoryTest.java similarity index 75% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategoryTest.java rename to enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategoryTest.java index 9679c61..6973ee5 100644 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategoryTest.java +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityCategoryTest.java @@ -5,8 +5,6 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class EnchantabilityCategoryTest { @@ -14,12 +12,6 @@ class EnchantabilityCategoryTest { private static final String REAL_CATEGORY = "STONE_TOOL"; private static final String NOT_A_CATEGORY = "Java Developer Eats Bugs LIVE On Stream For Your Viewing Pleasure!"; - @BeforeAll - static void setUp() { - // Enchantability checks server defaults or listings, server should be set up to be safe. - ServerMocks.mockServer(); - } - @Test void get() { assertThat( @@ -38,4 +30,4 @@ void getNotPresent() { ); } -} \ No newline at end of file +} diff --git a/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableTest.java new file mode 100644 index 0000000..d5c0c50 --- /dev/null +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableTest.java @@ -0,0 +1,254 @@ +package com.github.jikoo.planarenchanting.table; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import com.github.jikoo.planarenchanting.util.EnchantData; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Random; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +/** + * Unit tests for enchantments. + * + *

As a developer, I want to be able to generate enchantments + * because I would like to support enchanting tables. + * + *

Feature: Calculate enchantments for special items + *
Given I am a user + *
When I attempt to enchant an item + *
And the item is a special item + *
Then the item should recieve applicable enchantments + */ +@DisplayName("Feature: Calculate enchantments for enchanting tables.") +@NullMarked +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class EnchantingTableTest { + + private MockedStatic bukkit; + private final Random random = new Random(0); + private Collection toolEnchants; + + @BeforeAll + void setUp() { + bukkit = mockStatic(Bukkit.class); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(inv -> { + Registry registry = mock(); + doAnswer(invocation -> { + NamespacedKey key = invocation.getArgument(0); + Enchantment enchant = mock(); + doReturn(key).when(enchant).getKey(); + return enchant; + }).when(registry).getOrThrow(any()); + return registry; + }); + + toolEnchants = List.of( + Enchantment.EFFICIENCY, + Enchantment.UNBREAKING, + Enchantment.FORTUNE, + Enchantment.SILK_TOUCH + ); + setUpToolEnchants(); + } + + @AfterAll + void tearDown() { + bukkit.close(); + } + + @BeforeEach + void setUpEach() { + random.setSeed(0); + } + + static void setUpToolEnchants() { + EnchantData data = EnchantData.Service.PROVIDER.of(Enchantment.EFFICIENCY); + doReturn(10).when(data).getWeight(); + doAnswer(invocation -> { + int level = invocation.getArgument(0); + return 1 + level * 10; + }).when(data).getMinModifiedCost(anyInt()); + doAnswer(invocation -> { + int level = invocation.getArgument(0); + return 51 + level * 10; + }).when(data).getMaxModifiedCost(anyInt()); + doReturn(5).when(Enchantment.EFFICIENCY).getMaxLevel(); + + data = EnchantData.Service.PROVIDER.of(Enchantment.UNBREAKING); + doReturn(5).when(data).getWeight(); + doAnswer(invocation -> { + int level = invocation.getArgument(0); + return 5 + level * 8; + }).when(data).getMinModifiedCost(anyInt()); + doAnswer(invocation -> { + int level = invocation.getArgument(0); + return 55 + level * 8; + }).when(data).getMaxModifiedCost(anyInt()); + doReturn(3).when(Enchantment.UNBREAKING).getMaxLevel(); + + data = EnchantData.Service.PROVIDER.of(Enchantment.FORTUNE); + doReturn(2).when(data).getWeight(); + doAnswer(invocation -> { + int level = invocation.getArgument(0); + return 15 + level * 9; + }).when(data).getMinModifiedCost(anyInt()); + doAnswer(invocation -> { + int level = invocation.getArgument(0); + return 65 + level * 9; + }).when(data).getMaxModifiedCost(anyInt()); + doReturn(3).when(Enchantment.FORTUNE).getMaxLevel(); + + data = EnchantData.Service.PROVIDER.of(Enchantment.SILK_TOUCH); + doReturn(1).when(data).getWeight(); + doReturn(15).when(data).getMinModifiedCost(anyInt()); + doReturn(65).when(data).getMaxModifiedCost(anyInt()); + doReturn(1).when(Enchantment.SILK_TOUCH).getMaxLevel(); + } + + @DisplayName("Empty enchantment list yields empty enchantments.") + @Test + void testEmptyEnchantList() { + var operation = new EnchantingTable(List.of(), new Enchantability(10)); + + assertThat( + "Empty available enchants yields empty enchants", + operation.apply(random, 1), + is(anEmptyMap())); + } + + @DisplayName("Enchantment incompatibility can be customized.") + @Test + void testAllIncompatibleAlwaysSingle() { + var operation = new EnchantingTable(toolEnchants, new Enchantability(20)); + operation.setIncompatibility((a, b) -> true); + + assertThat( + "All enchantments incompatible with others yields single enchantment", + operation.apply(random, 1), + is(aMapWithSize(1))); + } + + @DisplayName("Enchantment level max can be modified.") + @Test + void testSetMaxLevel() { + Enchantment enchant = Enchantment.EFFICIENCY; + var operation = new EnchantingTable(List.of(enchant), new Enchantability(40)); + // Double max level for enchants that go over 1. + operation.setMaxLevel(enchant1 -> enchant1.getMaxLevel() > 1 ? enchant1.getMaxLevel() * 2 : 1); + + String assertation = "High level enchantment generates higher level enchantments"; + random.setSeed(assertation.hashCode()); + assertThat( + assertation, + operation.apply(random, 50), + both(hasEntry(is(enchant), greaterThan(enchant.getMaxLevel()))).and(aMapWithSize(1))); + } + + @DisplayName("When enchantments are selected") + @Nested + class EnchantmentAttempt { + + private Map selected; + + @BeforeEach + void beforeEach() { + var operation = new EnchantingTable(toolEnchants, new Enchantability(10)); + selected = operation.apply(random, random.nextInt(1, 31)); + } + + @DisplayName("One or more enchantments should be selected.") + @Test + void checkSize() { + var operation = new EnchantingTable(toolEnchants, new Enchantability(10)); + selected = operation.apply(random, 30); + assertThat( + "One or more enchantments must be selected", + selected, + is(aMapWithSize(greaterThan(0)))); + } + + @DisplayName("Enchantments should not conflict.") + @RepeatedTest(10) + void checkConflict() { + Enchantment[] enchantments = selected.keySet().toArray(new Enchantment[0]); + for (int i = 0; i < enchantments.length; ++i) { + for (int j = 0; j < enchantments.length; ++j) { + if (i == j) { + continue; + } + assertThat( + "Enchantments may not conflict", + conflicts(enchantments[i], enchantments[j]), + is(false)); + } + } + } + + private boolean conflicts(Enchantment enchantment1, Enchantment enchantment2) { + if (enchantment1.equals(enchantment2)) { + return true; + } + return enchantment1.conflictsWith(enchantment2) || enchantment2.conflictsWith(enchantment1); + } + + } + + @DisplayName("Enchanting table button levels should be calculated consistently.") + @ParameterizedTest + @CsvSource({"1,0", "10,0", "15,0", "1,12348", "10,98124", "15,23479"}) + void testGetButtonLevels(int shelves, int seed) { + Random random = new Random(seed); + int[] buttonLevels1 = EnchantingTable.getButtonLevels(random, shelves); + random.setSeed(seed); + int[] buttonLevels2 = EnchantingTable.getButtonLevels(random, shelves); + + assertThat("There are always three buttons", buttonLevels1.length, is(3)); + assertThat("There are always three buttons", buttonLevels2.length, is(3)); + + List buttonLevelsList1 = Arrays.stream(buttonLevels1).boxed().toList(); + + assertThat( + "Button levels should be generated consistently", + buttonLevelsList1, + contains(buttonLevels2[0], buttonLevels2[1], buttonLevels2[2])); + assertThat( + "Button levels must be positive integers that do not exceed 30", + buttonLevelsList1, + everyItem(is(both(lessThanOrEqualTo(30)).and(greaterThanOrEqualTo(0))))); + } + +} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/TableEnchantListenerTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/TableEnchantListenerTest.java similarity index 78% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/TableEnchantListenerTest.java rename to enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/TableEnchantListenerTest.java index 6cbf6c7..244a957 100644 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/TableEnchantListenerTest.java +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/table/TableEnchantListenerTest.java @@ -16,20 +16,20 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.Server; import org.bukkit.enchantments.Enchantment; import org.bukkit.enchantments.EnchantmentOffer; @@ -43,24 +43,29 @@ import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; import org.hamcrest.Matchers; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; @DisplayName("Feature: Enchant blocks in enchanting tables.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@NullMarked class TableEnchantListenerTest { - private static final Material ENCHANTABLE_MATERIAL = Material.COAL_ORE; - private static final Material UNENCHANTABLE_MATERIAL = Material.DIRT; - private static Enchantment validEnchant; - private static Collection toolEnchants; + private MockedStatic bukkit; + private final Material ENCHANTABLE_MATERIAL = Material.COAL_ORE; + private final Material UNENCHANTABLE_MATERIAL = Material.DIRT; + private Enchantment validEnchant; + private Collection toolEnchants; + private Plugin plugin; private TableEnchantListener listener; private Player player; private ItemStack itemStack; @@ -68,21 +73,17 @@ class TableEnchantListenerTest { @BeforeAll void setUpAll() { - Server server = ServerMocks.mockServer(); - - var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); - - var scheduler = mock(BukkitScheduler.class); - // Immediately run tasks when called. - when(scheduler.runTaskLater(any(Plugin.class), any(Runnable.class), anyLong())) - .thenAnswer(invocation -> { - invocation.getArgument(1, Runnable.class).run(); - return null; - }); - when(server.getScheduler()).thenReturn(scheduler); - - EnchantmentMocks.init(); + bukkit = mockStatic(Bukkit.class); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(inv -> { + Registry registry = mock(); + doAnswer(invocation -> { + NamespacedKey key = invocation.getArgument(0); + Enchantment enchant = mock(); + doReturn(key).when(enchant).getKey(); + return enchant; + }).when(registry).getOrThrow(any()); + return registry; + }); validEnchant = Enchantment.EFFICIENCY; toolEnchants = List.of( @@ -91,32 +92,39 @@ void setUpAll() { Enchantment.FORTUNE, Enchantment.SILK_TOUCH ); + EnchantingTableTest.setUpToolEnchants(); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach void setUp() { - Plugin plugin = mock(Plugin.class); + plugin = mock(Plugin.class); doReturn("SampleText").when(plugin).getName(); - doReturn("sampletext").when(plugin).namespace(); + Server server = mock(); + doReturn(server).when(plugin).getServer(); + BukkitScheduler scheduler = mock(); + doReturn(scheduler).when(server).getScheduler(); listener = new TableEnchantListener(plugin) { - private final EnchantingTable table = new EnchantingTable(toolEnchants, EnchantabilityCategory.STONE_TOOL); + private final EnchantingTable table = new EnchantingTable(toolEnchants, new Enchantability(5)); @Override - protected boolean isIneligible(@NotNull Player player, - @NotNull ItemStack enchanted) { + protected boolean isIneligible(Player player, ItemStack enchanted) { return itemStack.getType() != ENCHANTABLE_MATERIAL; } @Override - protected @NotNull EnchantingTable getTable(@NotNull Player player, - @NotNull ItemStack enchanted) { + protected EnchantingTable getTable(Player player, ItemStack enchanted) { return table; } }; var pdc = mock(PersistentDataContainer.class); - AtomicReference value = new AtomicReference<>(null); + AtomicReference<@Nullable Long> value = new AtomicReference<>(null); when(pdc.get(key, PersistentDataType.LONG)).thenAnswer(invocation -> value.get()); doAnswer(invocation -> { value.set(invocation.getArgument(2)); @@ -139,25 +147,27 @@ protected boolean isIneligible(@NotNull Player player, return null; }).when(player).setEnchantmentSeed(anyInt()); - itemStack = new ItemStack(ENCHANTABLE_MATERIAL); + itemStack = mock(); + doReturn(ENCHANTABLE_MATERIAL).when(itemStack).getType(); + doReturn(1).when(itemStack).getAmount(); key = new NamespacedKey(plugin, "enchanting_table_seed"); } @Test void testCanNotEnchantEnchanted() { - itemStack.addUnsafeEnchantment(validEnchant, 10); + doReturn(Map.of(validEnchant, 10)).when(itemStack).getEnchantments(); assertThat("Enchanted item cannot be enchanted", listener.canNotEnchant(player, itemStack)); } @Test void testCanNotEnchantStack() { - itemStack.setAmount(2); + doReturn(2).when(itemStack).getAmount(); assertThat("Stacked item cannot be enchanted", listener.canNotEnchant(player, itemStack)); } @Test void testCanNotEnchantWrongMaterial() { - itemStack = new ItemStack(UNENCHANTABLE_MATERIAL); + doReturn(UNENCHANTABLE_MATERIAL).when(itemStack).getType(); assertThat( "Material with no enchants cannot be enchanted", listener.canNotEnchant(player, itemStack)); @@ -173,7 +183,7 @@ void testCanEnchant() { @Test void testPrepareItemEnchantInvalid() { - itemStack = new ItemStack(UNENCHANTABLE_MATERIAL); + doReturn(UNENCHANTABLE_MATERIAL).when(itemStack).getType(); var event = prepareEvent(15); assertDoesNotThrow(() -> listener.onPrepareItemEnchant(event)); assertThat( @@ -210,15 +220,12 @@ void testSeedChangedPostEnchant() { @DisplayName("Button updates send to user as expected.") @Test void testSendButtonUpdates() { - var scheduler = mock(BukkitScheduler.class); + BukkitScheduler scheduler = plugin.getServer().getScheduler(); ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(Runnable.class); ArgumentCaptor delayCaptor = ArgumentCaptor.forClass(Long.class); BukkitTask bukkitTask = mock(BukkitTask.class); when(scheduler.runTaskLater(any(Plugin.class), taskCaptor.capture(), delayCaptor.capture())).thenReturn(bukkitTask); - var server = Bukkit.getServer(); - when(server.getScheduler()).thenReturn(scheduler); - var event = prepareEvent(30); var offerData = new AtomicReference(); doAnswer(invocation -> { @@ -249,8 +256,7 @@ void testSendButtonUpdates() { assertThat("Offer data is sent", offerData.get(), is(arrayContaining(offers))); } - @Contract("_ -> new") - private @NotNull PrepareItemEnchantEvent prepareEvent(int bonus) { + private PrepareItemEnchantEvent prepareEvent(int bonus) { return new PrepareItemEnchantEvent( player, mock(), @@ -260,8 +266,7 @@ void testSendButtonUpdates() { bonus); } - @Contract("_, _ -> new") - private @NotNull EnchantItemEvent enchantEvent(int level, int buttonIndex) { + private EnchantItemEvent enchantEvent(int level, int buttonIndex) { return new EnchantItemEvent( player, player.getOpenInventory(), @@ -274,4 +279,4 @@ void testSendButtonUpdates() { buttonIndex); } -} \ No newline at end of file +} diff --git a/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/util/ItemUtilTest.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/util/ItemUtilTest.java new file mode 100644 index 0000000..2ba52e0 --- /dev/null +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/util/ItemUtilTest.java @@ -0,0 +1,20 @@ +package com.github.jikoo.planarenchanting.util; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.bukkit.Material; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +@DisplayName("Item utility methods") +@TestInstance(Lifecycle.PER_CLASS) +class ItemUtilTest { + + @Test + void testAirConstantImmutable() { + assertThrows(UnsupportedOperationException.class, () -> ItemUtil.AIR.setType(Material.DIRT)); + } + +} diff --git a/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/util/MockEnchantDataProvider.java b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/util/MockEnchantDataProvider.java new file mode 100644 index 0000000..a9bbc2d --- /dev/null +++ b/enchanting-common/src/test/java/com/github/jikoo/planarenchanting/util/MockEnchantDataProvider.java @@ -0,0 +1,21 @@ +package com.github.jikoo.planarenchanting.util; + +import static org.mockito.Mockito.mock; + +import java.util.HashMap; +import java.util.Map; +import com.github.jikoo.planarenchanting.util.EnchantData.Provider; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class MockEnchantDataProvider implements Provider { + + private Map enchants = new HashMap<>(); + + @Override + public EnchantData of(Enchantment enchantment) { + return enchants.computeIfAbsent(enchantment, ench -> mock()); + } + +} diff --git a/enchanting-common/src/test/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider b/enchanting-common/src/test/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider new file mode 100644 index 0000000..f43acd1 --- /dev/null +++ b/enchanting-common/src/test/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider @@ -0,0 +1 @@ +com.github.jikoo.planarenchanting.util.MockEnchantDataProvider diff --git a/enchanting-components/build.gradle.kts b/enchanting-components/build.gradle.kts new file mode 100644 index 0000000..c12382a --- /dev/null +++ b/enchanting-components/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + compileOnly(libs.io.papermc.paper.paper.api) + implementation(project(":enchanting-common")) { + exclude(group = "com.github.jikoo", module = "planarwrappers") + exclude(group = "org.spigotmc", module = "spigot-api") + } + + testImplementation(libs.io.papermc.paper.paper.api) +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentAnvilFunctions.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentAnvilFunctions.java new file mode 100644 index 0000000..ff7ac1b --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentAnvilFunctions.java @@ -0,0 +1,281 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static io.papermc.paper.datacomponent.DataComponentTypes.CUSTOM_NAME; +import static io.papermc.paper.datacomponent.DataComponentTypes.DAMAGE; +import static io.papermc.paper.datacomponent.DataComponentTypes.MAX_DAMAGE; +import static io.papermc.paper.datacomponent.DataComponentTypes.REPAIR_COST; + +import io.papermc.paper.datacomponent.DataComponentType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +/** + * Data component-based {@link AnvilFunctionsProvider}. + */ +@NullMarked +public final class ComponentAnvilFunctions implements AnvilFunctionsProvider { + + public static final AnvilFunction PRIOR_WORK_LEVEL_COST = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + return true; + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return get(state.getBase(), REPAIR_COST) + get(state.getAddition(), REPAIR_COST); + } + }; + } + }; + public static final AnvilFunction RENAME = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + Component data = state.getBase().getData(CUSTOM_NAME); + String anvilText = state.getAnvilView().getRenameText(); + + // If the names aren't the same, the rename can be applied. + if (data == null) { + return anvilText != null; + } + + return !LegacyComponentSerializer.legacySection().serialize(data).equals(anvilText); + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return 1; + } + + @Override + public void modifyResult(ItemStack modified) { + String anvilText = state.getAnvilView().getRenameText(); + + if (anvilText == null) { + modified.resetData(CUSTOM_NAME); + } else { + modified.setData(CUSTOM_NAME, Component.text(anvilText)); + } + + int priorCost = Math.max(get(state.getBase(), REPAIR_COST), get(state.getAddition(), REPAIR_COST)); + modified.setData(REPAIR_COST, priorCost); + } + }; + } + }; + public static final AnvilFunction UPDATE_PRIOR_WORK_COST = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + return true; + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + return new AnvilFunctionResult<>() { + @Override + public void modifyResult(ItemStack modified) { + int priorCost = Math.max( + get(state.getBase(), REPAIR_COST), + get(state.getAddition(), REPAIR_COST) + ); + modified.setData(REPAIR_COST, priorCost * 2 + 1); + } + }; + } + }; + public static final AnvilFunction REPAIR_WITH_MATERIAL = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + return get(state.getBase(), DAMAGE) > 0 + && behavior.itemRepairedBy(state.getBase(), state.getAddition()); + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + int damage = get(state.getBase(), DAMAGE); + if (damage <= 0) { + return AnvilFunctionResult.empty(); + } + + int maxDamage = get(state.getBase(), MAX_DAMAGE); + if (maxDamage <= 0) { + return AnvilFunctionResult.empty(); + } + + int repairPerMaterial = maxDamage / 4; + int repairsNeeded = Math.ceilDiv(damage, repairPerMaterial); + + final int repairsAvailable = Math.min(repairsNeeded, state.getAddition().getAmount()); + final int resultDamage = Math.max(0, damage - (repairsAvailable * repairPerMaterial)); + + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return repairsAvailable; + } + + @Override + public int getMaterialCostIncrease() { + return repairsAvailable; + } + + @Override + public void modifyResult(ItemStack modified) { + modified.setData(DAMAGE, resultDamage); + } + }; + } + }; + public static final AnvilFunction REPAIR_WITH_COMBINATION = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + if (state.getBase().getType() != state.getAddition().getType()) { + return false; + } + // If either is undamaged, not eligible for repair. + if (get(state.getBase(), DAMAGE) <= 0) { + return false; + } + // If the base doesn't have a valid max damage, not eligible. + Integer baseMaxDamage = state.getBase().getData(MAX_DAMAGE); + if (baseMaxDamage == null || baseMaxDamage <= 0) { + return false; + } + // If the base and addition mismatch for some reason, they're probably secretly + // different, and we don't really want to open that can of worms. + return baseMaxDamage.equals(state.getAddition().getData(MAX_DAMAGE)); + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + ItemStack result + ) { + int damage = get(state.getBase(), DAMAGE); + + if (damage <= 0) { + return AnvilFunctionResult.empty(); + } + + int maxDamage = get(state.getBase(), MAX_DAMAGE); + + if (maxDamage <= 0) { + return AnvilFunctionResult.empty(); + } + + int restored = (int) (maxDamage - get(state.getAddition(), DAMAGE) + maxDamage * 0.12); + + final int resultDamage = Math.max(0, damage - restored); + + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return 2; + } + + @Override + public void modifyResult(ItemStack modified) { + modified.setData(DAMAGE, resultDamage); + } + }; + } + }; + public static final AnvilFunction COMBINE_ENCHANTMENTS_JAVA; + public static final AnvilFunction COMBINE_ENCHANTMENTS_BEDROCK; + + public static ComponentAnvilFunctions INSTANCE = new ComponentAnvilFunctions(); + + static { + ComponentEnchantmentAccess access = new ComponentEnchantmentAccess(); + COMBINE_ENCHANTMENTS_JAVA = new CombineEnchants<>(CombineEnchants.Platform.JAVA, access); + COMBINE_ENCHANTMENTS_BEDROCK = new CombineEnchants<>(CombineEnchants.Platform.BEDROCK, access); + } + + private ComponentAnvilFunctions() {} + + @Override + public AnvilFunction addPriorWorkLevelCost() { + return PRIOR_WORK_LEVEL_COST; + } + + @Override + public AnvilFunction rename() { + return RENAME; + } + + @Override + public AnvilFunction setItemPriorWork() { + return UPDATE_PRIOR_WORK_COST; + } + + @Override + public AnvilFunction repairWithMaterial() { + return REPAIR_WITH_MATERIAL; + } + + @Override + public AnvilFunction repairWithCombine() { + return REPAIR_WITH_COMBINATION; + } + + @Override + public AnvilFunction combineEnchantsJava() { + return COMBINE_ENCHANTMENTS_JAVA; + } + + @Override + public AnvilFunction combineEnchantsBedrock() { + return COMBINE_ENCHANTMENTS_BEDROCK; + } + + private static int get(ItemStack itemStack, DataComponentType.Valued type) { + Integer data = itemStack.getData(type); + return data != null ? data : 0; + } + +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentEnchantmentAccess.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentEnchantmentAccess.java new file mode 100644 index 0000000..557493a --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentEnchantmentAccess.java @@ -0,0 +1,41 @@ +package com.github.jikoo.planarenchanting.anvil; + +import com.github.jikoo.planarenchanting.util.EnchantmentAccess; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.ItemEnchantments; +import java.util.Map; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class ComponentEnchantmentAccess implements EnchantmentAccess { + + @Override + public boolean isBook(ItemStack itemStack) { + return itemStack.getType() == Material.ENCHANTED_BOOK; + } + + @Override + public Map getEnchantments(ItemStack itemStack) { + ItemEnchantments enchants; + if (itemStack.getType() == Material.ENCHANTED_BOOK) { + enchants = itemStack.getData(DataComponentTypes.STORED_ENCHANTMENTS); + } else { + enchants = itemStack.getData(DataComponentTypes.ENCHANTMENTS); + } + return enchants != null ? enchants.enchantments() : Map.of(); + } + + @Override + public void addEnchantments(ItemStack itemStack, Map enchantments) { + ItemEnchantments enchants = ItemEnchantments.itemEnchantments(enchantments); + if (itemStack.getType() == Material.ENCHANTED_BOOK) { + itemStack.setData(DataComponentTypes.STORED_ENCHANTMENTS, enchants); + } else { + itemStack.setData(DataComponentTypes.ENCHANTMENTS, enchants); + } + } + +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentTemperer.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentTemperer.java new file mode 100644 index 0000000..36723bd --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentTemperer.java @@ -0,0 +1,48 @@ +package com.github.jikoo.planarenchanting.anvil; + +import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentTypes; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class ComponentTemperer implements Temperer { + + static final ComponentTemperer INSTANCE = new ComponentTemperer(); + + @Override + public boolean hasChanged(ItemStack base, ItemStack addition, ItemStack result) { + if (base.isEmpty() || result.isEmpty()) { + return false; + } + + // Prepare to check if the item has changed and there is a real result. + // As we want to conditionally ignore certain changes, we need to copy the result. + ItemStack modResult = result.clone(); + // Ignore changes to repair cost - these should generally always happen. + resetData(modResult, base, DataComponentTypes.REPAIR_COST); + // Ignore custom name if the addition is not empty. + if (!addition.isEmpty()) { + resetData(modResult, base, DataComponentTypes.CUSTOM_NAME); + } + + return !base.equals(modResult); + } + + @Override + public ItemStack temper(ItemStack result) { + return result; + } + + private void resetData(ItemStack reset, ItemStack original, DataComponentType.Valued type) { + T data = original.getData(type); + if (data == null) { + reset.resetData(type); + } else { + reset.setData(type, data); + } + } + + private ComponentTemperer() {} + +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentVanillaBehavior.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentVanillaBehavior.java new file mode 100644 index 0000000..86a6084 --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentVanillaBehavior.java @@ -0,0 +1,43 @@ +package com.github.jikoo.planarenchanting.anvil; + +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.Repairable; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.TypedKey; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.NullMarked; + +/** + * Data component-based {@link AnvilBehavior}. + */ +@NullMarked +public class ComponentVanillaBehavior implements AnvilBehavior { + + @Override + public boolean enchantApplies(Enchantment enchantment, ItemStack base) { + return enchantment.canEnchantItem(base); + } + + @Override + public boolean itemsCombineEnchants(ItemStack base, ItemStack addition) { + return base.getType() == addition.getType() || addition.getType() == Material.ENCHANTED_BOOK; + } + + @Override + public boolean itemRepairedBy(ItemStack repaired, ItemStack repairMat) { + Repairable repairable = repaired.getData(DataComponentTypes.REPAIRABLE); + + if (repairable == null) { + return false; + } + + // Note: KeyImpl and NamespacedKey have the same hashCode implementation, so + // the fact that ItemTypeKeys all have a KeyImpl instead shouldn't matter. + TypedKey itemKey = TypedKey.create(RegistryKey.ITEM, repairMat.getType().getKey()); + return repairable.types().contains(itemKey); + } + +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentViewState.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentViewState.java new file mode 100644 index 0000000..e05182c --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/anvil/ComponentViewState.java @@ -0,0 +1,42 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class ComponentViewState implements ViewState { + + private final AnvilView view; + private final ItemStack base; + private final ItemStack addition; + + ComponentViewState(AnvilView view) { + this.view = view; + ItemStack stack = view.getItem(0); + this.base = stack != null ? stack : ItemStack.empty(); + stack = view.getItem(1); + this.addition = stack != null ? stack : ItemStack.empty(); + } + + @Override + public AnvilView getAnvilView() { + return view; + } + + @Override + public ItemStack getBase() { + return base; + } + + @Override + public ItemStack getAddition() { + return addition; + } + + @Override + public ItemStack createResult() { + return base.clone(); + } + +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/table/ComponentEnchantabilities.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/table/ComponentEnchantabilities.java new file mode 100644 index 0000000..24b235d --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/table/ComponentEnchantabilities.java @@ -0,0 +1,31 @@ +package com.github.jikoo.planarenchanting.table; + +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.Enchantable; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class ComponentEnchantabilities implements EnchantabilityProvider { + + @Override + public @Nullable Enchantability of(Material material) { + ItemType itemType = material.asItemType(); + return itemType != null ? of(itemType) : null; + } + + @Override + public @Nullable Enchantability of(ItemType itemType) { + Enchantable enchantable = itemType.getDefaultData(DataComponentTypes.ENCHANTABLE); + return enchantable != null ? new Enchantability(enchantable.value()) : null; + } + + @Override + public @Nullable Enchantability of(ItemStack item) { + Enchantable enchantable = item.getData(DataComponentTypes.ENCHANTABLE); + return enchantable != null ? new Enchantability(enchantable.value()) : null; + } +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/util/ComponentCapability.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/util/ComponentCapability.java new file mode 100644 index 0000000..678e956 --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/util/ComponentCapability.java @@ -0,0 +1,62 @@ +package com.github.jikoo.planarenchanting.util; + +import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.Enchantable; +import io.papermc.paper.datacomponent.item.ItemEnchantments; +import io.papermc.paper.datacomponent.item.Repairable; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.set.RegistrySet; +import java.util.Map; +import java.util.function.Supplier; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +final class ComponentCapability { + + // Providing a getter rather than a constant allows us to do a static mock. + static boolean get() { + try { + return Wrapper.get(); + } catch (LinkageError e) { + return false; + } + } + + // Inner class allows us to catch the exception in the outer class. + // The only alternatives are not using a static helper method (which looks terrible) + // or not catching the errors internally. + private static final class Wrapper { + + static boolean get() { + ItemStack itemStack = new ItemStack(Material.DIAMOND_PICKAXE); + test(itemStack, DataComponentTypes.MAX_DAMAGE, () -> 50); + test(itemStack, DataComponentTypes.DAMAGE, () -> 50); + test(itemStack, DataComponentTypes.REPAIR_COST, () -> 50); + test(itemStack, DataComponentTypes.REPAIRABLE, () -> Repairable.repairable(RegistrySet.keySet(RegistryKey.ITEM))); + test(itemStack, DataComponentTypes.CUSTOM_NAME, () -> Component.text("Sample text")); + test(itemStack, DataComponentTypes.ITEM_NAME, () -> Component.text("Sample text")); + test(itemStack, DataComponentTypes.ENCHANTMENTS, () -> ItemEnchantments.itemEnchantments(Map.of())); + test(itemStack, DataComponentTypes.STORED_ENCHANTMENTS, () -> ItemEnchantments.itemEnchantments(Map.of())); + test(itemStack, DataComponentTypes.ENCHANTABLE, () -> Enchantable.enchantable(5)); + return true; + } + + private static void test(ItemStack itemStack, DataComponentType.Valued type, Supplier value) { + T data = itemStack.getData(type); + + if (data == null) { + data = value.get(); + } + + itemStack.setData(type, data); + } + + private Wrapper() {} + + } + + private ComponentCapability() {} + +} diff --git a/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/util/ComponentEnchantProvider.java b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/util/ComponentEnchantProvider.java new file mode 100644 index 0000000..f2c41a8 --- /dev/null +++ b/enchanting-components/src/main/java/com/github/jikoo/planarenchanting/util/ComponentEnchantProvider.java @@ -0,0 +1,41 @@ +package com.github.jikoo.planarenchanting.util; + +import com.github.jikoo.planarenchanting.util.EnchantData.Provider; +import io.papermc.paper.registry.keys.ItemTypeKeys; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class ComponentEnchantProvider implements Provider { + + @Override + public EnchantData of(Enchantment enchantment) { + return new EnchantData() { + @Override + public int getWeight() { + return enchantment.getWeight(); + } + + @Override + public int getAnvilCost() { + return enchantment.getAnvilCost(); + } + + @Override + public int getMinModifiedCost(int level) { + return enchantment.getMinModifiedCost(level); + } + + @Override + public int getMaxModifiedCost(int level) { + return enchantment.getMaxModifiedCost(level); + } + + @Override + public boolean isTridentEnchant() { + return enchantment.getSupportedItems().contains(ItemTypeKeys.TRIDENT); + } + }; + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentAnvilFunctionsTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentAnvilFunctionsTest.java new file mode 100644 index 0000000..640874b --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentAnvilFunctionsTest.java @@ -0,0 +1,573 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.github.jikoo.planarenchanting.util.mock.ServerMocks; +import io.papermc.paper.datacomponent.DataComponentTypes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Stream; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.view.AnvilView; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@DisplayName("Default basic AnvilFunctions") +@TestInstance(Lifecycle.PER_CLASS) +class ComponentAnvilFunctionsTest { + + @BeforeAll + void beforeAll() { + ServerMocks.mockServer(); + // Touch DataComponentType so it is available during later mocking. + DataComponentTypes.DAMAGE.key(); + } + + @Nested + class PriorWorkLevelCost { + + private final AnvilFunction function = ComponentAnvilFunctions.PRIOR_WORK_LEVEL_COST; + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + assertThat( + "Prior work level cost always applies", + function.canApply(behavior, state, resultStack), + is(true) + ); + } + + @ParameterizedTest + @MethodSource("getPriorWork") + void getResult(int baseWork, int addedWork) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + + ItemStack stack = mock(); + doReturn(baseWork).when(stack).getData(DataComponentTypes.REPAIR_COST); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(addedWork).when(stack).getData(DataComponentTypes.REPAIR_COST); + doReturn(stack).when(state).getAddition(); + + var result = function.getResult(behavior, state, mock()); + assertThat( + "Cost must be total prior work", + result.getLevelCostIncrease(), + is(baseWork + addedWork) + ); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + ItemStack resultStack = mock(); + result.modifyResult(resultStack); + verifyNoInteractions(resultStack); + } + + private static @NotNull Collection getPriorWork() { + Collection arguments = new ArrayList<>(); + int [] values = { 0, 1, 3, 7, 15, 31 }; + + for (int base : values) { + for (int added : values) { + arguments.add(Arguments.of(base, added)); + } + } + + return arguments; + } + + } + + @Nested + class Rename { + + private final AnvilFunction function = ComponentAnvilFunctions.RENAME; + + @DisplayName("Rename requires different name") + @ParameterizedTest + @MethodSource("renameSituations") + void canApplyRequiresNameChange(String anvilName, Component baseName, boolean canApply) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + AnvilView view = mock(); + doReturn(anvilName).when(view).getRenameText(); + doReturn(view).when(state).getAnvilView(); + + ItemStack stack = mock(); + doReturn(baseName).when(stack).getData(DataComponentTypes.CUSTOM_NAME); + doReturn(stack).when(state).getBase(); + + assertThat("Rename requires different name", function.canApply(behavior, state, resultStack), is(canApply)); + } + + @Test + void getResultResetName() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + AnvilView view = mock(); + doReturn(view).when(state).getAnvilView(); + + ItemStack stack = mock(); + doReturn(stack).when(state).getBase(); + doReturn(stack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Level cost increase is 1", result.getLevelCostIncrease(), is(1)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + stack = mock(); + result.modifyResult(stack); + + verify(stack).resetData(DataComponentTypes.CUSTOM_NAME); + verify(stack).setData(eq(DataComponentTypes.REPAIR_COST), anyInt()); + } + + @Test + void getResultSetName() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + ItemStack stack = mock(); + doReturn(stack).when(state).getBase(); + doReturn(stack).when(state).getAddition(); + + AnvilView view = mock(); + doReturn("sample text").when(view).getRenameText(); + doReturn(view).when(state).getAnvilView(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Level cost increase is 1", result.getLevelCostIncrease(), is(1)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + stack = mock(); + result.modifyResult(stack); + + verify(stack).setData(eq(DataComponentTypes.CUSTOM_NAME), any(Component.class)); + verify(stack).setData(eq(DataComponentTypes.REPAIR_COST), anyInt()); + } + + private static Stream renameSituations() { + String name1 = "Sample text"; + String name2 = "Example text"; + return Stream.of( + // NON-APPLICABLE + // Both unnamed + Arguments.of(null, null, false), + // Both identically named + Arguments.of(name1, Component.text(name1), false), + + // APPLICABLE + // Only anvil named + Arguments.of(name1, null, true), + // Only item named + Arguments.of(null, Component.text(name1), true), + // Both named differently + Arguments.of(name1, Component.text(name2), true) + ); + } + + } + + @Nested + class UpdatePriorWorkCost { + + private final AnvilFunction function = ComponentAnvilFunctions.UPDATE_PRIOR_WORK_COST; + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + assertThat( + "Prior work update always applies", + function.canApply(behavior, state, resultStack), + is(true) + ); + } + + @ParameterizedTest + @MethodSource("getPriorWork") + void testPriorWorkUpdate(int baseWork, int addedWork) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + ItemStack stack = mock(); + doReturn(baseWork).when(stack).getData(DataComponentTypes.REPAIR_COST); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(addedWork).when(stack).getData(DataComponentTypes.REPAIR_COST); + doReturn(stack).when(state).getAddition(); + + var result = function.getResult(behavior, state, resultStack); + assertThat("Result is not empty", result, is(not(AnvilFunctionResult.empty()))); + assertThat("Cost must be unchanged", result.getLevelCostIncrease(), is(0)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + stack = mock(); + + result.modifyResult(stack); + verify(stack).setData(eq(DataComponentTypes.REPAIR_COST), anyInt()); + } + + private static @NotNull Collection getPriorWork() { + Collection arguments = new ArrayList<>(); + int [] values = { 0, 1, 3, 7, 15, 31 }; + + for (int base : values) { + for (int added : values) { + arguments.add(Arguments.of(base, added)); + } + } + + return arguments; + } + + } + + @Nested + class RepairWithMaterial { + + private final AnvilFunction function = ComponentAnvilFunctions.REPAIR_WITH_MATERIAL; + + @Test + void canApplyNoDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(stack).when(state).getBase(); + + assertThat("Cannot apply to undamaged item", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotRepairedBy() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(1).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getBase(); + + assertThat( + "Cannot apply with unusable addition", + function.canApply(behavior, state, resultStack), + is(false) + ); + } + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(1).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getBase(); + + assertThat( + "Must apply to damaged and repairable item", + function.canApply(behavior, state, resultStack), + is(true) + ); + } + + @Test + void getResultNoDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(stack).when(state).getBase(); + + assertThat( + "Item without damage yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResultNoMaxDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(1).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getBase(); + + assertThat( + "Item without max damage yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @ParameterizedTest + @CsvSource({"1,1,3", "64,4,0"}) + void getResult(int additionAmount, int expectedRepairs, int expectedDamage) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + + ItemStack stack = mock(); + doReturn(4).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(4).when(stack).getData(DataComponentTypes.MAX_DAMAGE); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(additionAmount).when(stack).getAmount(); + doReturn(stack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + assertThat("Cost is repair count", result.getLevelCostIncrease(), is(expectedRepairs)); + assertThat("Cost is repair count", result.getMaterialCostIncrease(), is(expectedRepairs)); + + stack = mock(); + result.modifyResult(stack); + verify(stack).setData(DataComponentTypes.DAMAGE, expectedDamage); + } + + } + + @Nested + class RepairWithCombination { + + private final AnvilFunction function = ComponentAnvilFunctions.REPAIR_WITH_COMBINATION; + + @Test + void canApplyNotSameType() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(Material.DIAMOND_AXE).when(stack).getType(); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(stack).when(state).getAddition(); + + assertThat("Items must be same type", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNoBaseDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(stack).when(state).getAddition(); + + assertThat("Base must be damaged", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNoAdditionDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(stack).when(state).getAddition(); + + assertThat("Addition must be damaged", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNoMaxDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getAddition(); + + assertThat("Base must have max damage", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyMaxDamageMismatch() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(16).when(stack).getData(DataComponentTypes.MAX_DAMAGE); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(1).when(stack).getData(DataComponentTypes.MAX_DAMAGE); + doReturn(stack).when(state).getAddition(); + + assertThat( + "Base and addition max damage must match", + function.canApply(behavior, state, resultStack), + is(false) + ); + } + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(16).when(stack).getData(DataComponentTypes.MAX_DAMAGE); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(stack).getType(); + doReturn(16).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(16).when(stack).getData(DataComponentTypes.MAX_DAMAGE); + doReturn(stack).when(state).getAddition(); + + assertThat("Function can apply", function.canApply(behavior, state, resultStack), is(true)); + } + + @Test + void getResultNoDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(stack).when(state).getBase(); + + assertThat( + "Item without damage yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResultNoMaxDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(1).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getBase(); + + assertThat( + "Item without max damage yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResult() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + ItemStack resultStack = mock(); + ItemStack stack = mock(); + doReturn(100).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(100).when(stack).getData(DataComponentTypes.MAX_DAMAGE); + doReturn(stack).when(state).getBase(); + stack = mock(); + doReturn(100).when(stack).getData(DataComponentTypes.DAMAGE); + doReturn(stack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + assertThat("Combine repair costs 2", result.getLevelCostIncrease(), is(2)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + stack = mock(); + result.modifyResult(stack); + verify(stack).setData(DataComponentTypes.DAMAGE, 88); + } + + } + + @Test + void gettersFetchConstants() { + // A bit of a silly test, but might prevent accidents. + ComponentAnvilFunctions functions = ComponentAnvilFunctions.INSTANCE; + assertThat( + "Provided function is constant", + functions.addPriorWorkLevelCost(), + is(sameInstance(ComponentAnvilFunctions.PRIOR_WORK_LEVEL_COST)) + ); + assertThat( + "Provided function is constant", + functions.rename(), + is(sameInstance(ComponentAnvilFunctions.RENAME)) + ); + assertThat( + "Provided function is constant", + functions.setItemPriorWork(), + is(sameInstance(ComponentAnvilFunctions.UPDATE_PRIOR_WORK_COST)) + ); + assertThat( + "Provided function is constant", + functions.repairWithMaterial(), + is(sameInstance(ComponentAnvilFunctions.REPAIR_WITH_MATERIAL)) + ); + assertThat( + "Provided function is constant", + functions.repairWithCombine(), + is(sameInstance(ComponentAnvilFunctions.REPAIR_WITH_COMBINATION)) + ); + assertThat( + "Provided function is constant", + functions.combineEnchantsJava(), + is(sameInstance(ComponentAnvilFunctions.COMBINE_ENCHANTMENTS_JAVA)) + ); + assertThat( + "Provided function is constant", + functions.combineEnchantsBedrock(), + is(sameInstance(ComponentAnvilFunctions.COMBINE_ENCHANTMENTS_BEDROCK)) + ); + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentEnchantmentAccessTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentEnchantmentAccessTest.java new file mode 100644 index 0000000..4327bcd --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentEnchantmentAccessTest.java @@ -0,0 +1,91 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +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.verify; + +import com.github.jikoo.planarenchanting.util.mock.ServerMocks; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.ItemEnchantments; +import java.util.Map; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +@TestInstance(Lifecycle.PER_CLASS) +class ComponentEnchantmentAccessTest { + + private ComponentEnchantmentAccess access; + + @BeforeAll + void setUp() { + // DataComponentTypes are fetched from the server registry. + ServerMocks.mockServer(); + // Touch to initialize. + DataComponentTypes.ENCHANTMENTS.key(); + access = new ComponentEnchantmentAccess(); + } + + @Test + void isBook() { + ItemStack stack = mock(); + doReturn(Material.ENCHANTED_BOOK).when(stack).getType(); + + assertThat("Item is book", access.isBook(stack)); + } + + @Test + void isNotBook() { + ItemStack stack = mock(); + doReturn(Material.DIRT).when(stack).getType(); + + assertThat("Item is not a book", access.isBook(stack), is(false)); + } + + @Test + void getEnchantments() { + ItemStack stack = mock(); + ItemEnchantments enchantments = mock(ItemEnchantments.class); + doReturn(enchantments).when(stack).getData(DataComponentTypes.ENCHANTMENTS); + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + doReturn(enchants).when(enchantments).enchantments(); + + assertThat("Item enchantments are fetched", access.getEnchantments(stack), is(enchants)); + } + + @Test + void getEnchantmentsStored() { + ItemStack stack = mock(); + doReturn(Material.ENCHANTED_BOOK).when(stack).getType(); + ItemEnchantments enchantments = mock(ItemEnchantments.class); + doReturn(enchantments).when(stack).getData(DataComponentTypes.STORED_ENCHANTMENTS); + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + doReturn(enchants).when(enchantments).enchantments(); + + assertThat("Stored enchantments are fetched", access.getEnchantments(stack), is(enchants)); + } + + @Test + void addEnchantments() { + ItemStack stack = mock(); + access.addEnchantments(stack, Map.of(Enchantment.EFFICIENCY, 5)); + verify(stack).setData(eq(DataComponentTypes.ENCHANTMENTS), any(ItemEnchantments.class)); + } + + @Test + void addEnchantmentsStored() { + ItemStack stack = mock(); + doReturn(Material.ENCHANTED_BOOK).when(stack).getType(); + access.addEnchantments(stack, Map.of(Enchantment.EFFICIENCY, 5)); + verify(stack).setData(eq(DataComponentTypes.STORED_ENCHANTMENTS), any(ItemEnchantments.class)); + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentTempererTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentTempererTest.java new file mode 100644 index 0000000..a245d3b --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentTempererTest.java @@ -0,0 +1,131 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.github.jikoo.planarenchanting.util.mock.ServerMocks; +import io.papermc.paper.datacomponent.DataComponentTypes; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ComponentTempererTest { + + @BeforeAll + void setUp() { + // DataComponentTypes are fetched from the server registry. + ServerMocks.mockServer(); + // Touch to initialize. + DataComponentTypes.REPAIR_COST.key(); + } + + @Test + void hasChangedEmpty() { + ItemStack base = mock(); + doReturn(true).when(base).isEmpty(); + ItemStack addition = mock(); + ItemStack result = mock(); + doReturn(true).when(result).isEmpty(); + + assertThat( + "Empty base and result are not changed", + ComponentTemperer.INSTANCE.hasChanged(base, addition, result), + is(false) + ); + } + + @Test + void hasChangedEmptyBase() { + ItemStack base = mock(); + doReturn(true).when(base).isEmpty(); + ItemStack addition = mock(); + ItemStack result = mock(); + + assertThat( + "Empty base is not changed", + ComponentTemperer.INSTANCE.hasChanged(base, addition, result), + is(false) + ); + } + + @Test + void hasChangedEmptyResult() { + ItemStack base = mock(); + ItemStack addition = mock(); + ItemStack result = mock(); + doReturn(true).when(result).isEmpty(); + + assertThat( + "Empty result is not changed", + ComponentTemperer.INSTANCE.hasChanged(base, addition, result), + is(false) + ); + } + + @Test + void hasChanged() { + ItemStack base = mock(); + ItemStack addition = mock(); + ItemStack result = mock(); + doReturn(result).when(result).clone(); + + assertThat( + "Different result is changed", + ComponentTemperer.INSTANCE.hasChanged(base, addition, result), + is(true) + ); + } + + @Test + void hasChangedUnchanged() { + ItemStack base = mock(); + ItemStack addition = mock(); + doReturn(true).when(addition).isEmpty(); + ItemStack result = mock(); + // Since we can't reimplement .equals for the return value, + // we can just make the "copy" be the original. + doReturn(base).when(result).clone(); + + assertThat( + "Unchanged result is not changed", + ComponentTemperer.INSTANCE.hasChanged(base, addition, result), + is(false) + ); + } + + @Test + void hasChangedResetAndSet() { + ItemStack base = mock(); + doReturn(5).when(base).getData(DataComponentTypes.REPAIR_COST); + ItemStack addition = mock(); + ItemStack result = mock(); + doReturn(result).when(result).clone(); + + assertThat( + "Different result is changed", + ComponentTemperer.INSTANCE.hasChanged(base, addition, result), + is(true) + ); + + verify(result).setData(DataComponentTypes.REPAIR_COST, 5); + verify(result).resetData(DataComponentTypes.CUSTOM_NAME); + } + + @Test + void temper() { + ItemStack result = mock(); + assertThat( + "Temper is a no-op", + ComponentTemperer.INSTANCE.temper(result), + is(result) + ); + verifyNoInteractions(result); + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentVanillaBehaviorTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentVanillaBehaviorTest.java new file mode 100644 index 0000000..4960a77 --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentVanillaBehaviorTest.java @@ -0,0 +1,110 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.github.jikoo.planarenchanting.util.mock.ServerMocks; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.Repairable; +import io.papermc.paper.registry.set.RegistryKeySet; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ComponentVanillaBehaviorTest { + + private ComponentVanillaBehavior behavior; + + @BeforeAll + void setUp() { + ServerMocks.mockServer(); + // Touch to initialize and ensure mocking is complete. + DataComponentTypes.REPAIR_COST.key(); + } + + @BeforeEach + void setUpEach() { + behavior = new ComponentVanillaBehavior(); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void enchantApplies(boolean applies) { + Enchantment enchantment = mock(); + doReturn(applies).when(enchantment).canEnchantItem(any()); + assertThat( + "Behavior applies if enchant applies", + behavior.enchantApplies(enchantment, mock()), + is(applies) + ); + verify(enchantment).canEnchantItem(any()); + verifyNoMoreInteractions(enchantment); + } + + @ParameterizedTest + @CsvSource({ + "DIAMOND,DIAMOND,true", + "DIAMOND_PICKAXE,DIAMOND,false", + "DIAMOND,ENCHANTED_BOOK,true", + "ENCHANTED_BOOK,ENCHANTED_BOOK,true", + "ENCHANTED_BOOK,DIAMOND,false" + }) + void itemsCombineEnchants(Material baseMat, Material additionMat, boolean result) { + ItemStack base = mock(); + doReturn(baseMat).when(base).getType(); + ItemStack addition = mock(); + doReturn(additionMat).when(addition).getType(); + + assertThat( + "Like items and enchanted book additions combine enchantments", + behavior.itemsCombineEnchants(base, addition), + is(result) + ); + } + + @Test + void itemRepairedByNotRepairable() { + ItemStack base = mock(); + ItemStack addition = mock(); + + assertThat( + "Item without Repairable is not repairable", + behavior.itemRepairedBy(base, addition), + is(false) + ); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void itemRepairedBy(boolean repairedBy) { + RegistryKeySet types = mock(); + doReturn(repairedBy).when(types).contains(any()); + Repairable repairable = mock(); + doReturn(types).when(repairable).types(); + ItemStack base = mock(); + doReturn(repairable).when(base).getData(DataComponentTypes.REPAIRABLE); + ItemStack addition = mock(); + doReturn(Material.DIRT).when(addition).getType(); + + assertThat( + "Item is repairable as expected", + behavior.itemRepairedBy(base, addition), + is(repairedBy) + ); + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentViewStateTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentViewStateTest.java new file mode 100644 index 0000000..ef57b67 --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/anvil/ComponentViewStateTest.java @@ -0,0 +1,105 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.view.AnvilView; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ComponentViewStateTest { + + private MockedStatic itemStack; + + @BeforeAll + void setUp() { + itemStack = mockStatic(ItemStack.class); + itemStack.when(ItemStack::empty).thenAnswer(invocation -> { + ItemStack stack = mock(); + doReturn(true).when(stack).isEmpty(); + return stack; + }); + } + + @AfterAll + void tearDown() { + itemStack.close(); + } + + @Test + void getOriginalView() { + AnvilView view = mock(); + ComponentViewState state = new ComponentViewState(view); + assertThat("Original view is available", state.getAnvilView(), is(view)); + } + + @Test + void getBase() { + AnvilView view = mock(); + ItemStack stack = mock(); + doReturn(stack).when(view).getItem(0); + + ComponentViewState state = new ComponentViewState(view); + + assertThat("Base is expected value", state.getBase(), is(stack)); + } + + @Test + void getBaseNull() { + AnvilView view = mock(); + + ComponentViewState state = new ComponentViewState(view); + + ItemStack result = state.getBase(); + assertThat("Base is not null", result, is(notNullValue())); + assertThat("Base is empty", result.isEmpty(), is(true)); + } + + @Test + void getAddition() { + AnvilView view = mock(); + ItemStack stack = mock(); + doReturn(stack).when(view).getItem(1); + + ComponentViewState state = new ComponentViewState(view); + + assertThat("Addition is expected value", state.getAddition(), is(stack)); + } + + @Test + void getAdditionNull() { + AnvilView view = mock(); + + ComponentViewState state = new ComponentViewState(view); + + ItemStack result = state.getAddition(); + assertThat("Addition is not null", result, is(notNullValue())); + assertThat("Addition is empty", result.isEmpty(), is(true)); + } + + @Test + void createResult() { + ItemStack stack = mock(); + doAnswer(invocation -> mock(ItemStack.class)).when(stack).clone(); + AnvilView view = mock(); + doReturn(stack).when(view).getItem(0); + + ComponentViewState state = new ComponentViewState(view); + + assertThat("Base is expected value", state.getBase(), is(stack)); + assertThat("Result is not base", state.createResult(), is(not(sameInstance(stack)))); + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/table/ComponentEnchantabilitiesTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/table/ComponentEnchantabilitiesTest.java new file mode 100644 index 0000000..6145fe5 --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/table/ComponentEnchantabilitiesTest.java @@ -0,0 +1,151 @@ +package com.github.jikoo.planarenchanting.table; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +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.mockStatic; + +import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentType.NonValued; +import io.papermc.paper.datacomponent.DataComponentType.Valued; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.Enchantable; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import net.kyori.adventure.key.Key; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; + +@TestInstance(Lifecycle.PER_CLASS) +class ComponentEnchantabilitiesTest { + + private MockedStatic registryAccess; + private EnchantabilityProvider provider; + + @SuppressWarnings({ "removal", "unchecked" }) + @BeforeAll + void setUp() { + Answer get = invocation -> switch (invocation.getArgument(0, Key.class).value()) { + // Bit of a kludge, but there aren't many non-valued types. + // Only alternative would be to reflectively examine fields here. + case "unbreakable", "intangible_projectile", "glider" -> mock(NonValued.class); + default -> mock(Valued.class); + }; + + registryAccess = mockStatic(); + registryAccess.when(RegistryAccess::registryAccess).thenAnswer(invRegAcc -> { + RegistryAccess access = mock(); + // Deprecated method is required for legacy registry types like ART, + // which will produce a NPE if not present when initializing Registry. + doAnswer(invocation -> mock(Registry.class)) + .when(access).getRegistry(any(Class.class)); + doAnswer(invocation -> mock(Registry.class)) + .when(access).getRegistry(any(RegistryKey.class)); + doAnswer(invocation -> { + Registry registry = mock(); + doAnswer(get).when(registry).getOrThrow(any(Key.class)); + return registry; + }).when(access).getRegistry(RegistryKey.DATA_COMPONENT_TYPE); + + return access; + }); + // touch types + DataComponentTypes.REPAIR_COST.key(); + + provider = new ComponentEnchantabilities(); + } + + @AfterAll + void tearDown() { + registryAccess.close(); + } + + @Test + void ofMaterialNullItem() { + Material material = mock(); + + assertThat( + "Non-item material does not have enchantability", + provider.of(material), + is(nullValue()) + ); + } + + @Test + void ofMaterial() { + Enchantable enchantable = mock(); + ItemType itemType = mock(); + doReturn(enchantable).when(itemType).getDefaultData(DataComponentTypes.ENCHANTABLE); + Material material = mock(); + doReturn(itemType).when(material).asItemType(); + + assertThat( + "Material has enchantability", + provider.of(material), + is(notNullValue()) + ); + } + + @Test + void ofItemTypeNotEnchantable() { + ItemType itemType = mock(); + + assertThat( + "ItemType does not have enchantability", + provider.of(itemType), + is(nullValue()) + ); + } + + @Test + void ofItemType() { + Enchantable enchantable = mock(); + ItemType itemType = mock(); + doReturn(enchantable).when(itemType).getDefaultData(DataComponentTypes.ENCHANTABLE); + + assertThat( + "ItemType has enchantability", + provider.of(itemType), + is(notNullValue()) + ); + } + + @Test + void ofItemStackNotEnchantable() { + ItemStack itemStack = mock(); + + assertThat( + "ItemStack does not have enchantability", + provider.of(itemStack), + is(nullValue()) + ); + } + + @Test + void ofItemStack() { + Enchantable enchantable = mock(); + ItemStack itemStack = mock(); + doReturn(enchantable).when(itemStack).getData(DataComponentTypes.ENCHANTABLE); + + assertThat( + "ItemStack has enchantability", + provider.of(itemStack), + is(notNullValue()) + ); + } + +} diff --git a/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/ComponentEnchantProviderTest.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/ComponentEnchantProviderTest.java new file mode 100644 index 0000000..fed611c --- /dev/null +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/ComponentEnchantProviderTest.java @@ -0,0 +1,110 @@ +package com.github.jikoo.planarenchanting.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.github.jikoo.planarenchanting.util.mock.ServerMocks; +import io.papermc.paper.registry.keys.ItemTypeKeys; +import io.papermc.paper.registry.set.RegistryKeySet; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ComponentEnchantProviderTest { + + private ComponentEnchantProvider provider; + private Enchantment enchant; + + @BeforeAll + void setUpAll() { + ServerMocks.mockServer(); + provider = new ComponentEnchantProvider(); + } + + @BeforeEach + void setUpEach() { + enchant = mock(); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 2, 5, 10 }) + void getWeight(int weight) { + doReturn(weight).when(enchant).getWeight(); + + EnchantData data = provider.of(enchant); + + assertThat("Weight is fetched from enchantment", data.getWeight(), is(weight)); + verify(enchant).getWeight(); + verifyNoMoreInteractions(enchant); + } + + @Test + void getAnvilCost() { + int cost = 5; + doReturn(cost).when(enchant).getAnvilCost(); + + EnchantData data = provider.of(enchant); + + assertThat("Anvil cost is fetched from enchantment", data.getAnvilCost(), is(cost)); + verify(enchant).getAnvilCost(); + verifyNoMoreInteractions(enchant); + } + + @Test + void getMinModifiedCost() { + int cost = 5; + doReturn(cost).when(enchant).getMinModifiedCost(anyInt()); + + EnchantData data = provider.of(enchant); + + int level = 1; + assertThat("Min cost is fetched from enchantment", data.getMinModifiedCost(level), is(cost)); + verify(enchant).getMinModifiedCost(level); + verifyNoMoreInteractions(enchant); + } + + @Test + void getMaxModifiedCost() { + int cost = 5; + doReturn(cost).when(enchant).getMaxModifiedCost(anyInt()); + + EnchantData data = provider.of(enchant); + + int level = 1; + assertThat("Max cost is fetched from enchantment", data.getMaxModifiedCost(level), is(cost)); + verify(enchant).getMaxModifiedCost(level); + verifyNoMoreInteractions(enchant); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void isTridentEnchant(boolean isTrident) { + RegistryKeySet supported = mock(); + doReturn(isTrident).when(supported).contains(ItemTypeKeys.TRIDENT); + doReturn(supported).when(enchant).getSupportedItems(); + + EnchantData data = provider.of(enchant); + + assertThat( + "Trident enchant status matches expected", + data.isTridentEnchant(), + is(isTrident) + ); + verify(supported).contains(ItemTypeKeys.TRIDENT); + verifyNoMoreInteractions(supported); + verify(enchant).getSupportedItems(); + verifyNoMoreInteractions(enchant); + } + +} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/ServerMocks.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/ServerMocks.java similarity index 100% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/ServerMocks.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/ServerMocks.java diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemFactoryMocks.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemFactoryMocks.java similarity index 100% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemFactoryMocks.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemFactoryMocks.java diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemMetaBase.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemMetaBase.java similarity index 86% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemMetaBase.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemMetaBase.java index 817734d..51e9b1b 100644 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemMetaBase.java +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemMetaBase.java @@ -3,7 +3,6 @@ import java.util.Map; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.Repairable; import org.jetbrains.annotations.NotNull; @@ -13,7 +12,7 @@ * implementation exists that does not implement Damageable, Repairable, and BlockDataMeta. * See CraftMetaItem. */ -public interface ItemMetaBase extends ItemMeta, Repairable, Damageable { +public interface ItemMetaBase extends Repairable, Damageable { void setEnchants(Map enchantments); diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemStackMocks.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemStackMocks.java similarity index 100% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemStackMocks.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/ItemStackMocks.java diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/RegistryHelper.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/RegistryHelper.java similarity index 100% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/RegistryHelper.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/RegistryHelper.java diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestRegistryAccess.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestRegistryAccess.java similarity index 100% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestRegistryAccess.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestRegistryAccess.java diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestServerBuildInfo.java b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestServerBuildInfo.java similarity index 99% rename from enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestServerBuildInfo.java rename to enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestServerBuildInfo.java index 09e0a96..d462d00 100644 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestServerBuildInfo.java +++ b/enchanting-components/src/test/java/com/github/jikoo/planarenchanting/util/mock/server/TestServerBuildInfo.java @@ -1,12 +1,11 @@ package com.github.jikoo.planarenchanting.util.mock.server; import io.papermc.paper.ServerBuildInfo; -import net.kyori.adventure.key.Key; -import org.jetbrains.annotations.NotNull; - import java.time.Instant; import java.util.Optional; import java.util.OptionalInt; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; public class TestServerBuildInfo implements ServerBuildInfo { diff --git a/enchanting-components/src/test/java/io/papermc/paper/datacomponent/item/PaperPleaseStopMakingItHarderToWriteTestsThankYou.java b/enchanting-components/src/test/java/io/papermc/paper/datacomponent/item/PaperPleaseStopMakingItHarderToWriteTestsThankYou.java new file mode 100644 index 0000000..87836b1 --- /dev/null +++ b/enchanting-components/src/test/java/io/papermc/paper/datacomponent/item/PaperPleaseStopMakingItHarderToWriteTestsThankYou.java @@ -0,0 +1,311 @@ +package io.papermc.paper.datacomponent.item; + +import com.destroystokyo.paper.profile.PlayerProfile; +import io.papermc.paper.datacomponent.item.ChargedProjectiles.Builder; +import io.papermc.paper.datacomponent.item.KineticWeapon.Condition; +import io.papermc.paper.datacomponent.item.MapDecorations.DecorationEntry; +import io.papermc.paper.datacomponent.item.ResolvableProfile.SkinPatch; +import io.papermc.paper.datacomponent.item.ResolvableProfile.SkinPatchBuilder; +import io.papermc.paper.datacomponent.item.Tool.Rule; +import io.papermc.paper.registry.set.RegistryKeySet; +import io.papermc.paper.registry.tag.TagKey; +import io.papermc.paper.text.Filtered; +import java.util.HashMap; +import java.util.Map; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.util.TriState; +import org.bukkit.JukeboxSong; +import org.bukkit.block.BlockType; +import org.bukkit.damage.DamageType; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.map.MapCursor.Type; +import org.checkerframework.common.value.qual.IntRange; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@SuppressWarnings("NonExtendableApiUsage") +@NullMarked +public class PaperPleaseStopMakingItHarderToWriteTestsThankYou implements ItemComponentTypesBridge { + + @Override + public Builder chargedProjectiles() { + throw new UnsupportedOperationException(); + } + + @Override + public PotDecorations.Builder potDecorations() { + throw new UnsupportedOperationException(); + } + + @Override + public ItemLore.Builder lore() { + throw new UnsupportedOperationException(); + } + + @Override + public ItemEnchantments.Builder enchantments() { + return new ItemEnchantments.Builder() { + private final Map enchantments = new HashMap<>(); + @Override + public ItemEnchantments.Builder add(Enchantment enchantment, @IntRange(from = 1L, to = 255L) int level) { + this.enchantments.put(enchantment, level); + return this; + } + + @Override + public ItemEnchantments.Builder addAll( + Map enchantments) { + this.enchantments.putAll(enchantments); + return this; + } + + @Override + public ItemEnchantments build() { + return new ItemEnchantments() { + private final Map enchants = Map.copyOf(enchantments); + @Override + public @Unmodifiable Map enchantments() { + return enchants; + } + }; + } + }; + } + + @Override + public ItemAttributeModifiers.Builder modifiers() { + throw new UnsupportedOperationException(); + } + + @Override + public FoodProperties.Builder food() { + throw new UnsupportedOperationException(); + } + + @Override + public DyedItemColor.Builder dyedItemColor() { + throw new UnsupportedOperationException(); + } + + @Override + public PotionContents.Builder potionContents() { + throw new UnsupportedOperationException(); + } + + @Override + public BundleContents.Builder bundleContents() { + throw new UnsupportedOperationException(); + } + + @Override + public SuspiciousStewEffects.Builder suspiciousStewEffects() { + throw new UnsupportedOperationException(); + } + + @Override + public MapItemColor.Builder mapItemColor() { + throw new UnsupportedOperationException(); + } + + @Override + public MapDecorations.Builder mapDecorations() { + throw new UnsupportedOperationException(); + } + + @Override + public DecorationEntry decorationEntry(Type type, double x, double z, float rotation) { + throw new UnsupportedOperationException(); + } + + @Override + public SeededContainerLoot.Builder seededContainerLoot(Key lootTableKey) { + throw new UnsupportedOperationException(); + } + + @Override + public WrittenBookContent.Builder writtenBookContent(Filtered title, String author) { + throw new UnsupportedOperationException(); + } + + @Override + public WritableBookContent.Builder writeableBookContent() { + throw new UnsupportedOperationException(); + } + + @Override + public ItemArmorTrim.Builder itemArmorTrim(ArmorTrim armorTrim) { + throw new UnsupportedOperationException(); + } + + @Override + public LodestoneTracker.Builder lodestoneTracker() { + throw new UnsupportedOperationException(); + } + + @Override + public Fireworks.Builder fireworks() { + throw new UnsupportedOperationException(); + } + + @Override + public ResolvableProfile.Builder resolvableProfile() { + throw new UnsupportedOperationException(); + } + + @Override + public SkinPatchBuilder skinPatch() { + throw new UnsupportedOperationException(); + } + + @Override + public SkinPatch emptySkinPatch() { + throw new UnsupportedOperationException(); + } + + @Override + public ResolvableProfile resolvableProfile(PlayerProfile profile) { + throw new UnsupportedOperationException(); + } + + @Override + public BannerPatternLayers.Builder bannerPatternLayers() { + throw new UnsupportedOperationException(); + } + + @Override + public BlockItemDataProperties.Builder blockItemStateProperties() { + throw new UnsupportedOperationException(); + } + + @Override + public ItemContainerContents.Builder itemContainerContents() { + throw new UnsupportedOperationException(); + } + + @Override + public JukeboxPlayable.Builder jukeboxPlayable(JukeboxSong song) { + throw new UnsupportedOperationException(); + } + + @Override + public Tool.Builder tool() { + throw new UnsupportedOperationException(); + } + + @Override + public Rule rule(RegistryKeySet blocks, @Nullable Float speed, + TriState correctForDrops) { + throw new UnsupportedOperationException(); + } + + @Override + public ItemAdventurePredicate.Builder itemAdventurePredicate() { + throw new UnsupportedOperationException(); + } + + @Override + public CustomModelData.Builder customModelData() { + throw new UnsupportedOperationException(); + } + + @Override + public MapId mapId(int id) { + throw new UnsupportedOperationException(); + } + + @Override + public UseRemainder useRemainder(ItemStack stack) { + throw new UnsupportedOperationException(); + } + + @Override + public Consumable.Builder consumable() { + throw new UnsupportedOperationException(); + } + + @Override + public UseCooldown.Builder useCooldown(float seconds) { + throw new UnsupportedOperationException(); + } + + @Override + public DamageResistant damageResistant(TagKey types) { + throw new UnsupportedOperationException(); + } + + @Override + public Enchantable enchantable(int level) { + throw new UnsupportedOperationException(); + } + + @Override + public Repairable repairable(RegistryKeySet types) { + throw new UnsupportedOperationException(); + } + + @Override + public Equippable.Builder equippable(EquipmentSlot slot) { + throw new UnsupportedOperationException(); + } + + @Override + public DeathProtection.Builder deathProtection() { + throw new UnsupportedOperationException(); + } + + @Override + public OminousBottleAmplifier ominousBottleAmplifier(int amplifier) { + throw new UnsupportedOperationException(); + } + + @Override + public BlocksAttacks.Builder blocksAttacks() { + throw new UnsupportedOperationException(); + } + + @Override + public TooltipDisplay.Builder tooltipDisplay() { + throw new UnsupportedOperationException(); + } + + @Override + public Weapon.Builder weapon() { + throw new UnsupportedOperationException(); + } + + @Override + public KineticWeapon.Builder kineticWeapon() { + throw new UnsupportedOperationException(); + } + + @Override + public UseEffects.Builder useEffects() { + throw new UnsupportedOperationException(); + } + + @Override + public PiercingWeapon.Builder piercingWeapon() { + throw new UnsupportedOperationException(); + } + + @Override + public AttackRange.Builder attackRange() { + throw new UnsupportedOperationException(); + } + + @Override + public SwingAnimation.Builder swingAnimation() { + throw new UnsupportedOperationException(); + } + + @Override + public Condition kineticWeaponCondition(int maxDurationTicks, float minSpeed, + float minRelativeSpeed) { + throw new UnsupportedOperationException(); + } +} diff --git a/enchanting-core/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo similarity index 100% rename from enchanting-core/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo rename to enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo diff --git a/enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge b/enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge new file mode 100644 index 0000000..9aec418 --- /dev/null +++ b/enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge @@ -0,0 +1 @@ +io.papermc.paper.datacomponent.item.PaperPleaseStopMakingItHarderToWriteTestsThankYou diff --git a/enchanting-core/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess b/enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess similarity index 100% rename from enchanting-core/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess rename to enchanting-components/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess diff --git a/enchanting-core/build.gradle.kts b/enchanting-core/build.gradle.kts deleted file mode 100644 index 462390c..0000000 --- a/enchanting-core/build.gradle.kts +++ /dev/null @@ -1,63 +0,0 @@ -plugins { - jacoco - alias(libs.plugins.org.sonarqube) -} - -repositories { - maven("https://repo.papermc.io/repository/maven-public/") - maven("https://jitpack.io/") -} - -val mockitoAgent: Configuration = configurations.create("mockitoAgent") - -dependencies { - implementation(libs.org.jetbrains.annotations) - implementation(libs.io.papermc.paper.paper.api) - implementation(libs.com.github.jikoo.planarwrappers) - - testImplementation(libs.org.hamcrest.hamcrest) - testImplementation(libs.org.mockito.mockito.core) - mockitoAgent(libs.org.mockito.mockito.core) { isTransitive = false } - testImplementation(libs.com.jparams.to.string.verifier) -} - -sourceSets { - main { - java.srcDirs("src/generated/java") - } -} - -testing { - suites { - named("test") { - useJUnitJupiter("6.0.2") - } - } -} - -tasks.test { - useJUnitPlatform() - // As Bukkit is very heavily statically initialized, don't reuse forks. - forkEvery = 1 - jvmArgs("-javaagent:${mockitoAgent.asPath}") - // Generate coverage reports - finalizedBy(tasks.jacocoTestReport) -} - -tasks.jacocoTestReport { - reports { - // Produce XML report for Sonar - xml.required = true - } -} - -sonar { - properties { - property("sonar.projectKey", "Jikoo_PlanarEnchanting") - property("sonar.organization", "jikoo") - property("sonar.host.url", "https://sonarcloud.io") - property("sonar.language", "java") - property("sonar.java.coveragePlugin", "jacoco") - property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml") - } -} diff --git a/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableData.java b/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableData.java deleted file mode 100644 index b288117..0000000 --- a/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/anvil/BakedRepairableData.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.processing.Generated; -import net.kyori.adventure.key.Key; -import org.jspecify.annotations.NullMarked; - -/** - * Pre-baked data used as a fallthrough for RepairMaterial. - * - *

This file was generated from Minecraft 1.21.11. Regenerate it rather than modify.

- */ -@Generated("com.github.jikoo.planarenchanting.generator.impl.RepairMaterialsGenerator") -@NullMarked -final class BakedRepairableData { - - private BakedRepairableData() {} - - static Map getTags() { - Map map = new HashMap<>(); - // - map.put(Key.key("minecraft", "chainmail_boots"), Key.key("minecraft", "repairs_chain_armor")); - map.put(Key.key("minecraft", "chainmail_chestplate"), Key.key("minecraft", "repairs_chain_armor")); - map.put(Key.key("minecraft", "chainmail_helmet"), Key.key("minecraft", "repairs_chain_armor")); - map.put(Key.key("minecraft", "chainmail_leggings"), Key.key("minecraft", "repairs_chain_armor")); - map.put(Key.key("minecraft", "copper_axe"), Key.key("minecraft", "copper_tool_materials")); - map.put(Key.key("minecraft", "copper_boots"), Key.key("minecraft", "repairs_copper_armor")); - map.put(Key.key("minecraft", "copper_chestplate"), Key.key("minecraft", "repairs_copper_armor")); - map.put(Key.key("minecraft", "copper_helmet"), Key.key("minecraft", "repairs_copper_armor")); - map.put(Key.key("minecraft", "copper_hoe"), Key.key("minecraft", "copper_tool_materials")); - map.put(Key.key("minecraft", "copper_leggings"), Key.key("minecraft", "repairs_copper_armor")); - map.put(Key.key("minecraft", "copper_pickaxe"), Key.key("minecraft", "copper_tool_materials")); - map.put(Key.key("minecraft", "copper_shovel"), Key.key("minecraft", "copper_tool_materials")); - map.put(Key.key("minecraft", "copper_spear"), Key.key("minecraft", "copper_tool_materials")); - map.put(Key.key("minecraft", "copper_sword"), Key.key("minecraft", "copper_tool_materials")); - map.put(Key.key("minecraft", "diamond_axe"), Key.key("minecraft", "diamond_tool_materials")); - map.put(Key.key("minecraft", "diamond_boots"), Key.key("minecraft", "repairs_diamond_armor")); - map.put(Key.key("minecraft", "diamond_chestplate"), Key.key("minecraft", "repairs_diamond_armor")); - map.put(Key.key("minecraft", "diamond_helmet"), Key.key("minecraft", "repairs_diamond_armor")); - map.put(Key.key("minecraft", "diamond_hoe"), Key.key("minecraft", "diamond_tool_materials")); - map.put(Key.key("minecraft", "diamond_leggings"), Key.key("minecraft", "repairs_diamond_armor")); - map.put(Key.key("minecraft", "diamond_pickaxe"), Key.key("minecraft", "diamond_tool_materials")); - map.put(Key.key("minecraft", "diamond_shovel"), Key.key("minecraft", "diamond_tool_materials")); - map.put(Key.key("minecraft", "diamond_spear"), Key.key("minecraft", "diamond_tool_materials")); - map.put(Key.key("minecraft", "diamond_sword"), Key.key("minecraft", "diamond_tool_materials")); - map.put(Key.key("minecraft", "golden_axe"), Key.key("minecraft", "gold_tool_materials")); - map.put(Key.key("minecraft", "golden_boots"), Key.key("minecraft", "repairs_gold_armor")); - map.put(Key.key("minecraft", "golden_chestplate"), Key.key("minecraft", "repairs_gold_armor")); - map.put(Key.key("minecraft", "golden_helmet"), Key.key("minecraft", "repairs_gold_armor")); - map.put(Key.key("minecraft", "golden_hoe"), Key.key("minecraft", "gold_tool_materials")); - map.put(Key.key("minecraft", "golden_leggings"), Key.key("minecraft", "repairs_gold_armor")); - map.put(Key.key("minecraft", "golden_pickaxe"), Key.key("minecraft", "gold_tool_materials")); - map.put(Key.key("minecraft", "golden_shovel"), Key.key("minecraft", "gold_tool_materials")); - map.put(Key.key("minecraft", "golden_spear"), Key.key("minecraft", "gold_tool_materials")); - map.put(Key.key("minecraft", "golden_sword"), Key.key("minecraft", "gold_tool_materials")); - map.put(Key.key("minecraft", "iron_axe"), Key.key("minecraft", "iron_tool_materials")); - map.put(Key.key("minecraft", "iron_boots"), Key.key("minecraft", "repairs_iron_armor")); - map.put(Key.key("minecraft", "iron_chestplate"), Key.key("minecraft", "repairs_iron_armor")); - map.put(Key.key("minecraft", "iron_helmet"), Key.key("minecraft", "repairs_iron_armor")); - map.put(Key.key("minecraft", "iron_hoe"), Key.key("minecraft", "iron_tool_materials")); - map.put(Key.key("minecraft", "iron_leggings"), Key.key("minecraft", "repairs_iron_armor")); - map.put(Key.key("minecraft", "iron_pickaxe"), Key.key("minecraft", "iron_tool_materials")); - map.put(Key.key("minecraft", "iron_shovel"), Key.key("minecraft", "iron_tool_materials")); - map.put(Key.key("minecraft", "iron_spear"), Key.key("minecraft", "iron_tool_materials")); - map.put(Key.key("minecraft", "iron_sword"), Key.key("minecraft", "iron_tool_materials")); - map.put(Key.key("minecraft", "leather_boots"), Key.key("minecraft", "repairs_leather_armor")); - map.put(Key.key("minecraft", "leather_chestplate"), Key.key("minecraft", "repairs_leather_armor")); - map.put(Key.key("minecraft", "leather_helmet"), Key.key("minecraft", "repairs_leather_armor")); - map.put(Key.key("minecraft", "leather_leggings"), Key.key("minecraft", "repairs_leather_armor")); - map.put(Key.key("minecraft", "netherite_axe"), Key.key("minecraft", "netherite_tool_materials")); - map.put(Key.key("minecraft", "netherite_boots"), Key.key("minecraft", "repairs_netherite_armor")); - map.put(Key.key("minecraft", "netherite_chestplate"), Key.key("minecraft", "repairs_netherite_armor")); - map.put(Key.key("minecraft", "netherite_helmet"), Key.key("minecraft", "repairs_netherite_armor")); - map.put(Key.key("minecraft", "netherite_hoe"), Key.key("minecraft", "netherite_tool_materials")); - map.put(Key.key("minecraft", "netherite_leggings"), Key.key("minecraft", "repairs_netherite_armor")); - map.put(Key.key("minecraft", "netherite_pickaxe"), Key.key("minecraft", "netherite_tool_materials")); - map.put(Key.key("minecraft", "netherite_shovel"), Key.key("minecraft", "netherite_tool_materials")); - map.put(Key.key("minecraft", "netherite_spear"), Key.key("minecraft", "netherite_tool_materials")); - map.put(Key.key("minecraft", "netherite_sword"), Key.key("minecraft", "netherite_tool_materials")); - map.put(Key.key("minecraft", "shield"), Key.key("minecraft", "wooden_tool_materials")); - map.put(Key.key("minecraft", "stone_axe"), Key.key("minecraft", "stone_tool_materials")); - map.put(Key.key("minecraft", "stone_hoe"), Key.key("minecraft", "stone_tool_materials")); - map.put(Key.key("minecraft", "stone_pickaxe"), Key.key("minecraft", "stone_tool_materials")); - map.put(Key.key("minecraft", "stone_shovel"), Key.key("minecraft", "stone_tool_materials")); - map.put(Key.key("minecraft", "stone_spear"), Key.key("minecraft", "stone_tool_materials")); - map.put(Key.key("minecraft", "stone_sword"), Key.key("minecraft", "stone_tool_materials")); - map.put(Key.key("minecraft", "turtle_helmet"), Key.key("minecraft", "repairs_turtle_helmet")); - map.put(Key.key("minecraft", "wolf_armor"), Key.key("minecraft", "repairs_wolf_armor")); - map.put(Key.key("minecraft", "wooden_axe"), Key.key("minecraft", "wooden_tool_materials")); - map.put(Key.key("minecraft", "wooden_hoe"), Key.key("minecraft", "wooden_tool_materials")); - map.put(Key.key("minecraft", "wooden_pickaxe"), Key.key("minecraft", "wooden_tool_materials")); - map.put(Key.key("minecraft", "wooden_shovel"), Key.key("minecraft", "wooden_tool_materials")); - map.put(Key.key("minecraft", "wooden_spear"), Key.key("minecraft", "wooden_tool_materials")); - map.put(Key.key("minecraft", "wooden_sword"), Key.key("minecraft", "wooden_tool_materials")); - // - return map; - } - - static Map> getLists() { - Map> map = new HashMap<>(); - // - map.put(Key.key("minecraft", "elytra"), List.of(Key.key("minecraft", "phantom_membrane"))); - map.put(Key.key("minecraft", "mace"), List.of(Key.key("minecraft", "breeze_rod"))); - // - return map; - } - -} diff --git a/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/table/BakedEnchantableData.java b/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/table/BakedEnchantableData.java deleted file mode 100644 index 0d359c5..0000000 --- a/enchanting-core/src/generated/java/com/github/jikoo/planarenchanting/table/BakedEnchantableData.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.github.jikoo.planarenchanting.table; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import javax.annotation.processing.Generated; -import net.kyori.adventure.key.Key; -import org.jspecify.annotations.NullMarked; - -/** - * Pre-baked enchantment data used as a fallthrough for Enchantability. - * - *

This file was generated from Minecraft 1.21.11. Regenerate it rather than modify.

- */ -@Generated("com.github.jikoo.planarenchanting.generator.impl.EnchDataGenerator") -@NullMarked -final class BakedEnchantableData { - - private BakedEnchantableData() {} - - static Map> get() { - Map> map = new HashMap<>(); - Function> create = ignored -> new HashSet<>(); - // - map.computeIfAbsent(1, create).add(Key.key("minecraft", "book")); - map.computeIfAbsent(1, create).add(Key.key("minecraft", "bow")); - map.computeIfAbsent(12, create).add(Key.key("minecraft", "chainmail_boots")); - map.computeIfAbsent(12, create).add(Key.key("minecraft", "chainmail_chestplate")); - map.computeIfAbsent(12, create).add(Key.key("minecraft", "chainmail_helmet")); - map.computeIfAbsent(12, create).add(Key.key("minecraft", "chainmail_leggings")); - map.computeIfAbsent(13, create).add(Key.key("minecraft", "copper_axe")); - map.computeIfAbsent(8, create).add(Key.key("minecraft", "copper_boots")); - map.computeIfAbsent(8, create).add(Key.key("minecraft", "copper_chestplate")); - map.computeIfAbsent(8, create).add(Key.key("minecraft", "copper_helmet")); - map.computeIfAbsent(13, create).add(Key.key("minecraft", "copper_hoe")); - map.computeIfAbsent(8, create).add(Key.key("minecraft", "copper_leggings")); - map.computeIfAbsent(13, create).add(Key.key("minecraft", "copper_pickaxe")); - map.computeIfAbsent(13, create).add(Key.key("minecraft", "copper_shovel")); - map.computeIfAbsent(13, create).add(Key.key("minecraft", "copper_spear")); - map.computeIfAbsent(13, create).add(Key.key("minecraft", "copper_sword")); - map.computeIfAbsent(1, create).add(Key.key("minecraft", "crossbow")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_axe")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_boots")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_chestplate")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_helmet")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_hoe")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_leggings")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_pickaxe")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_shovel")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_spear")); - map.computeIfAbsent(10, create).add(Key.key("minecraft", "diamond_sword")); - map.computeIfAbsent(1, create).add(Key.key("minecraft", "fishing_rod")); - map.computeIfAbsent(22, create).add(Key.key("minecraft", "golden_axe")); - map.computeIfAbsent(25, create).add(Key.key("minecraft", "golden_boots")); - map.computeIfAbsent(25, create).add(Key.key("minecraft", "golden_chestplate")); - map.computeIfAbsent(25, create).add(Key.key("minecraft", "golden_helmet")); - map.computeIfAbsent(22, create).add(Key.key("minecraft", "golden_hoe")); - map.computeIfAbsent(25, create).add(Key.key("minecraft", "golden_leggings")); - map.computeIfAbsent(22, create).add(Key.key("minecraft", "golden_pickaxe")); - map.computeIfAbsent(22, create).add(Key.key("minecraft", "golden_shovel")); - map.computeIfAbsent(22, create).add(Key.key("minecraft", "golden_spear")); - map.computeIfAbsent(22, create).add(Key.key("minecraft", "golden_sword")); - map.computeIfAbsent(14, create).add(Key.key("minecraft", "iron_axe")); - map.computeIfAbsent(9, create).add(Key.key("minecraft", "iron_boots")); - map.computeIfAbsent(9, create).add(Key.key("minecraft", "iron_chestplate")); - map.computeIfAbsent(9, create).add(Key.key("minecraft", "iron_helmet")); - map.computeIfAbsent(14, create).add(Key.key("minecraft", "iron_hoe")); - map.computeIfAbsent(9, create).add(Key.key("minecraft", "iron_leggings")); - map.computeIfAbsent(14, create).add(Key.key("minecraft", "iron_pickaxe")); - map.computeIfAbsent(14, create).add(Key.key("minecraft", "iron_shovel")); - map.computeIfAbsent(14, create).add(Key.key("minecraft", "iron_spear")); - map.computeIfAbsent(14, create).add(Key.key("minecraft", "iron_sword")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "leather_boots")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "leather_chestplate")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "leather_helmet")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "leather_leggings")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "mace")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_axe")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_boots")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_chestplate")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_helmet")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_hoe")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_leggings")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_pickaxe")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_shovel")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_spear")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "netherite_sword")); - map.computeIfAbsent(5, create).add(Key.key("minecraft", "stone_axe")); - map.computeIfAbsent(5, create).add(Key.key("minecraft", "stone_hoe")); - map.computeIfAbsent(5, create).add(Key.key("minecraft", "stone_pickaxe")); - map.computeIfAbsent(5, create).add(Key.key("minecraft", "stone_shovel")); - map.computeIfAbsent(5, create).add(Key.key("minecraft", "stone_spear")); - map.computeIfAbsent(5, create).add(Key.key("minecraft", "stone_sword")); - map.computeIfAbsent(1, create).add(Key.key("minecraft", "trident")); - map.computeIfAbsent(9, create).add(Key.key("minecraft", "turtle_helmet")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "wooden_axe")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "wooden_hoe")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "wooden_pickaxe")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "wooden_shovel")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "wooden_spear")); - map.computeIfAbsent(15, create).add(Key.key("minecraft", "wooden_sword")); - // - return map; - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java deleted file mode 100644 index 64c0152..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/Anvil.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import com.github.jikoo.planarenchanting.util.ItemUtil; -import org.bukkit.Bukkit; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.Repairable; -import org.bukkit.inventory.view.AnvilView; -import org.jetbrains.annotations.NotNull; - -public class Anvil { - - private final @NotNull AnvilBehavior behavior; - - public Anvil() { - this(AnvilBehavior.VANILLA); - } - - public Anvil(@NotNull AnvilBehavior behavior) { - this.behavior = behavior; - } - - /** - * Get an {@link AnvilResult} for this anvil operation. - * - * @return the {@code AnvilResult} - */ - public @NotNull AnvilResult getResult(@NotNull AnvilView view) { - AnvilState state = new AnvilState(view); - - if (ItemUtil.isEmpty(state.getBase().getItem())) { - return AnvilResult.EMPTY; - } - - // Apply base cost. - apply(state, AnvilFunctions.PRIOR_WORK_LEVEL_COST); - - if (ItemUtil.isEmpty(state.getAddition().getItem())) { - if (apply(state, AnvilFunctions.RENAME)) { - // If the only thing occurring is a renaming operation, it is always allowed. - state.setLevelCost(Math.min(state.getLevelCost(), state.getAnvil().getMaximumRepairCost() - 1)); - } - - // No addition means no additional operations to perform. - return forge(state); - } - - - if (state.getBase().getItem().getAmount() != 1) { - // Multi-renames are allowed, multi-modifications are not. - // Vanilla allows multi-modifications "for creative" but the way it does it is problematic. - return AnvilResult.EMPTY; - } - - apply(state, AnvilFunctions.RENAME); - // Apply prior work cost after rename. - // Rename also applies a prior work cost but does not increase it. - apply(state, AnvilFunctions.UPDATE_PRIOR_WORK_COST); - - if (!apply(state, AnvilFunctions.REPAIR_WITH_MATERIAL)) { - // Only do combination repair if this is not a material repair. - apply(state, AnvilFunctions.REPAIR_WITH_COMBINATION); - } - - // Differing from vanilla - since we use a custom determination for whether enchantments should - // transfer (which defaults to indirectly mimicking vanilla), enchantments may need to be - // applied from a material repair. - apply(state, AnvilFunctions.COMBINE_ENCHANTMENTS_JAVA_EDITION); - - return forge(state); - } - - /** - * Attempt to apply the given {@link AnvilFunction}, modifying the result and costs as necessary. - * Note that a function reporting itself applicable does not guarantee that the result or costs - * will actually differ. - * - * @see AnvilFunction#canApply(AnvilBehavior, AnvilState) - * @param function the {@code AnvilFunction} to apply - * @return whether the {@link AnvilFunction} could apply - */ - protected final boolean apply(@NotNull AnvilState state, @NotNull AnvilFunction function) { - if (!function.canApply(this.behavior, state)) { - return false; - } - - AnvilFunctionResult anvilResult = function.getResult(this.behavior, state); - - anvilResult.modifyResult(state.result.getMeta()); - state.setLevelCost(state.getLevelCost() + anvilResult.getLevelCostIncrease()); - state.setMaterialCost(state.getMaterialCost() + anvilResult.getMaterialCostIncrease()); - - return true; - } - - /** - * Finalize the {@code AnvilOperationState} into an {@link AnvilResult}. - * - * @return the finalized result - */ - protected final AnvilResult forge(@NotNull AnvilState state) { - - ItemMeta baseMeta = state.getBase().getMeta(); - ItemMeta resultMeta = state.result.getMeta(); - - // If base meta is null, it is empty. No input = no output. - if (baseMeta == null) { - return AnvilResult.EMPTY; - } - - // If result meta is null, result is empty. - if (resultMeta == null) { - return AnvilResult.EMPTY; - } - - // Update result meta. - state.result.getItem().setItemMeta(resultMeta); - - // Reset result meta to base meta to ignore certain characteristics when verifying that - // changes have actually been performed. - resultMeta = resultMeta.clone(); - - // Ignore repair cost changes. - if (baseMeta instanceof Repairable baseRepairable - && resultMeta instanceof Repairable resultRepairable) { - resultRepairable.setRepairCost(baseRepairable.getRepairCost()); - } - // Ignore name changes if addition is not empty. - if (!ItemUtil.isEmpty(state.getAddition().getItem())) { - resultMeta.customName(baseMeta.customName()); - } - - // If reset meta is identical then no operation is occurring. - // Note that meta must be compared via ItemFactory#equals(ItemMeta, ItemMeta) for test purposes. - if (Bukkit.getItemFactory().equals(baseMeta, resultMeta)) { - return AnvilResult.EMPTY; - } - - return new AnvilResult(state.result.getItem(), state.getLevelCost(), state.getMaterialCost()); - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilBehavior.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilBehavior.java deleted file mode 100644 index 7294b55..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilBehavior.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import com.github.jikoo.planarenchanting.util.MetaCachedStack; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.jetbrains.annotations.NotNull; - -public interface AnvilBehavior { - - AnvilBehavior VANILLA = new AnvilBehavior() {}; - - /** - * Get whether an {@link Enchantment} is applicable for a wrapped {@link ItemStack}. - * - * @param enchantment the {@code Enchantment} to check for applicability - * @param base the item that may be enchanted - * @return whether the {@code Enchantment} can be applied - */ - default boolean enchantApplies(@NotNull Enchantment enchantment, @NotNull MetaCachedStack base) { - return enchantment.canEnchantItem(base.getItem()); - } - - /** - * Get whether two {@link Enchantment Enchantments} conflict. - * - * @return whether the {@code Enchantments} conflict - */ - default boolean enchantsConflict(@NotNull Enchantment enchant1, @NotNull Enchantment enchant2) { - return enchant1.conflictsWith(enchant2); - } - - /** - * Get the maximum level for an {@link Enchantment}. - * - * @return the maximum level for an {@code Enchantment} - */ - default int getEnchantMaxLevel(@NotNull Enchantment enchantment) { - return enchantment.getMaxLevel(); - } - - /** - * Get whether an item should combine its {@link Enchantment Enchantments} with another item. - * - * @param base the base item - * @param addition the item added - * @return whether items should combine {@code Enchantments} - */ - default boolean itemsCombineEnchants(@NotNull MetaCachedStack base, @NotNull MetaCachedStack addition) { - ItemType additionType = addition.getItem().getType().asItemType(); - return base.getItem().getType().asItemType() == additionType || additionType == ItemType.ENCHANTED_BOOK; - } - - /** - * Get whether an item is repaired by another item. This is not the same as a repair via - * combination of like items! Like items always attempt to combine durability. If you require - * different behavior, override {@link Anvil#getResult(org.bukkit.inventory.view.AnvilView)} - * and do not call {@link AnvilFunctions#REPAIR_WITH_COMBINATION}. - * - * @param repaired the item repaired - * @param repairMat the item used to repair - * @return the method determining whether an item is repaired by another item - */ - default boolean itemRepairedBy(@NotNull MetaCachedStack repaired, @NotNull MetaCachedStack repairMat) { - return RepairMaterial.repairs(repaired.getItem(), repairMat.getItem()); - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctions.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctions.java deleted file mode 100644 index ee1bb9d..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctions.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import com.github.jikoo.planarenchanting.util.ItemUtil; -import com.github.jikoo.planarenchanting.util.MetaCachedStack; -import io.papermc.paper.registry.keys.ItemTypeKeys; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.Repairable; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public enum AnvilFunctions { - ; // Empty enum to hold constants. - - /** Constant for adding the level cost from prior work. */ - public static final AnvilFunction PRIOR_WORK_LEVEL_COST = new AnvilFunction() { - @Override - public boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state) { - return true; - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - return ItemUtil.getRepairCost(state.getBase().getMeta()) - + ItemUtil.getRepairCost(state.getAddition().getMeta()); - } - }; - } - }; - - /** Constant for adding the level cost for a renaming operation. */ - public static final AnvilFunction RENAME = new AnvilFunction() { - @Override - public boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state) { - var itemMeta = state.getBase().getMeta(); - - if (itemMeta == null) { - return false; - } - - // If names are not the same, can be applied. - Component customName = itemMeta.customName(); - String anvilText = state.getAnvil().getRenameText(); - if (customName == null) { - return anvilText != null; - } - return !LegacyComponentSerializer.legacySection().serialize(customName).equals(anvilText); - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - // Renames always apply a level cost of 1. - return 1; - } - - @Override - public void modifyResult(@Nullable ItemMeta itemMeta) { - if (itemMeta == null) { - return; - } - - String anvilText = state.getAnvil().getRenameText(); - itemMeta.customName(anvilText == null ? null : Component.text(anvilText)); - if (itemMeta instanceof Repairable repairable) { - int repairCost = Math.max( - ItemUtil.getRepairCost(state.getBase().getMeta()), - ItemUtil.getRepairCost(state.getAddition().getMeta())); - repairable.setRepairCost(repairCost); - } - } - }; - } - }; - - /** Constant for updating prior work to new value. */ - public static final AnvilFunction UPDATE_PRIOR_WORK_COST = new AnvilFunction() { - @Override - public boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state) { - return state.getBase().getMeta() instanceof Repairable; - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - - return new AnvilFunctionResult() { - @Override - public void modifyResult(@Nullable ItemMeta itemMeta) { - if (itemMeta instanceof Repairable repairable) { - int priorRepairCost = Math.max( - ItemUtil.getRepairCost(state.getBase().getMeta()), - ItemUtil.getRepairCost(state.getAddition().getMeta())); - repairable.setRepairCost(priorRepairCost * 2 + 1); - } - } - }; - } - }; - - /** Constant for using materials to restore durability to the base item. */ - public static final AnvilFunction REPAIR_WITH_MATERIAL = new AnvilFunction() { - @Override - public boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state) { - MetaCachedStack base = state.getBase(); - return behavior.itemRepairedBy(base, state.getAddition()) - && base.getItem().getType().getMaxDurability() > 0 - && base.getMeta() instanceof Damageable damageable - && damageable.getDamage() > 0; - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - if (!(state.getBase().getMeta() instanceof Damageable damageable)) { - // If result is not damageable, it cannot be repaired. - return AnvilFunctionResult.EMPTY; - } - - int missingDurability = damageable.getDamage(); - - if (missingDurability < 1) { - // If result is not damaged, no repair. - return AnvilFunctionResult.EMPTY; - } - - int repairs = 0; - // Each repair removes up to 1/4 max durability in damage. - int damageRepairedPerMaterial = state.getBase().getItem().getType().getMaxDurability() / 4; - - while (missingDurability > 0 && repairs < state.getAddition().getItem().getAmount()) { - missingDurability -= damageRepairedPerMaterial; - ++repairs; - } - - // Finalize for later use. - int totalRepairs = repairs; - int resultDamage = Math.max(0, missingDurability); - - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - return totalRepairs; - } - - @Override - public int getMaterialCostIncrease() { - return totalRepairs; - } - - @Override - public void modifyResult(@Nullable ItemMeta itemMeta) { - if (itemMeta instanceof Damageable damageable) { - damageable.setDamage(resultDamage); - } - } - }; - } - }; - - /** Constant for using identical materials to restore durability. */ - public static final AnvilFunction REPAIR_WITH_COMBINATION = new AnvilFunction() { - @Override - public boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state) { - ItemType baseType = state.getBase().getItem().getType().asItemType(); - return baseType != null - && baseType == state.getAddition().getItem().getType().asItemType() - && baseType.getMaxDurability() > 0 - && state.getBase().getMeta() instanceof Damageable damageable - && damageable.getDamage() > 0; - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - if (!(state.getBase().getMeta() instanceof Damageable baseDamageable - && state.getAddition().getMeta() instanceof Damageable additionDamageable)) { - return AnvilFunctionResult.EMPTY; - } - - int missingDurability = baseDamageable.getDamage(); - - if (missingDurability < 1) { - return AnvilFunctionResult.EMPTY; - } - - int maxDurability = state.getBase().getItem().getType().getMaxDurability(); - // Restore durability remaining in added item. - int restoredDurability = maxDurability - additionDamageable.getDamage(); - // Add a bonus 12% total tool durability to the repair. - restoredDurability += (int) (maxDurability * 0.12); - - // Finalize for later use. - int resultDamage = Math.max(0, missingDurability - restoredDurability); - - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - return 2; - } - - @Override - public void modifyResult(@Nullable ItemMeta itemMeta) { - if (itemMeta instanceof Damageable damageable) { - damageable.setDamage(resultDamage); - } - } - }; - } - }; - - /** Constant for combining enchantments from source and target like Java Edition. */ - public static final AnvilFunction COMBINE_ENCHANTMENTS_JAVA_EDITION = new CombineEnchantments() { - @Override - protected int getTotalCost(int baseCost, int oldLevel, int newLevel) { - return baseCost * newLevel; - } - - @Override - protected int getNonApplicableCost() { - return 1; - } - }; - - /** Constant for combining enchantments from source and target like Bedrock Edition. */ - public static final AnvilFunction COMBINE_ENCHANTMENTS_BEDROCK_EDITION = new CombineEnchantments() { - @Override - protected int getAnvilCost(Enchantment enchantment, boolean isFromBook) { - if (!enchantment.getSupportedItems().contains(ItemTypeKeys.TRIDENT)) { - return super.getAnvilCost(enchantment, isFromBook); - } - - int base = enchantment.getAnvilCost(); - - // Bedrock Edition rarity is 1 tier lower for trident enchantments. - return Math.max(1, isFromBook ? base / 4 : base / 2); - } - - @Override - protected int getTotalCost(int baseCost, int oldLevel, int newLevel) { - if (oldLevel >= newLevel) { - return 0; - } - - return baseCost * (newLevel - oldLevel); - } - - @Override - protected int getNonApplicableCost() { - return 0; - } - }; - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilState.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilState.java deleted file mode 100644 index 6d94856..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/AnvilState.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import com.github.jikoo.planarenchanting.util.MetaCachedStack; -import org.bukkit.inventory.view.AnvilView; -import org.jetbrains.annotations.NotNull; - -/** - * A mutable data holder similar to an {@link AnvilResult}. - */ -public class AnvilState { - - private final @NotNull AnvilView view; - private final @NotNull MetaCachedStack base; - private final @NotNull MetaCachedStack addition; - final @NotNull MetaCachedStack result; - private int levelCost = 0; - private int materialCost = 0; - - /** - * Create an {@code AnvilOperationState} instance for the given operation and inventory. - * - * @param view the {@link AnvilView} the state is derived from - */ - public AnvilState(@NotNull AnvilView view) { - this.view = view; - this.base = new MetaCachedStack(this.view.getItem(0)); - this.addition = new MetaCachedStack(this.view.getItem(1)); - this.result = new MetaCachedStack(this.base.getItem().clone()); - } - - /** - * Get the {@link AnvilView} the state is derived from. - * - * @return the {@code AnvilView} - */ - public AnvilView getAnvil() { - return this.view; - } - - /** - * Get the base input item from the {@link AnvilView}. - * - * @return the base input item - */ - public @NotNull MetaCachedStack getBase() { - return this.base; - } - - /** - * Get the secondary input item from the {@link AnvilView}. - * - * @return the secondary input item - */ - public @NotNull MetaCachedStack getAddition() { - return this.addition; - } - - /** - * Get the number of levels to be consumed by the operation. - * - * @return the number of levels to be consumed by the operation - */ - public int getLevelCost() { - return this.levelCost; - } - - /** - * Set the number of levels to be consumed by the operation. - * - * @param levelCost the number of levels to be consumed by the operation - */ - public void setLevelCost(int levelCost) { - this.levelCost = levelCost; - } - - /** - * Get the amount of items to be consumed from the addition slot. - * - * @return the amount of items to be consumed from the addition slot - */ - public int getMaterialCost() { - return this.materialCost; - } - - /** - * Set the amount of items to be consumed from the addition slot. - * - * @param materialCost the amount of items to be consumed from the addition slot - */ - public void setMaterialCost(int materialCost) { - this.materialCost = materialCost; - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantments.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantments.java deleted file mode 100644 index 6216dd1..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantments.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import com.github.jikoo.planarenchanting.enchant.EnchantmentUtil; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -abstract class CombineEnchantments implements AnvilFunction { - - @Override - public boolean canApply(@NotNull AnvilBehavior behavior, @NotNull AnvilState state) { - return behavior.itemsCombineEnchants(state.getBase(), state.getAddition()); - } - - @Override - public final @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - Map baseEnchants = EnchantmentUtil.getEnchants( - state.getBase().getMeta()); - Map additionEnchants = EnchantmentUtil.getEnchants( - state.getAddition().getMeta()); - - if (additionEnchants.isEmpty()) { - return AnvilFunctionResult.EMPTY; - } - - Map newEnchants = new HashMap<>(baseEnchants); - boolean isFromBook = state.getAddition().getItem().getType().asItemType() == ItemType.ENCHANTED_BOOK; - - int levelCost = 0; - for (Entry enchantEntry : additionEnchants.entrySet()) { - Enchantment newEnchantment = enchantEntry.getKey(); - int oldLevel = baseEnchants.getOrDefault(newEnchantment, 0); - int baseCost = getAnvilCost(newEnchantment, isFromBook); - if (behavior.enchantApplies(newEnchantment, state.getBase()) - && baseEnchants.keySet().stream() - .noneMatch(existingEnchant -> - !existingEnchant.getKey().equals(newEnchantment.getKey()) - && behavior.enchantsConflict(existingEnchant, newEnchantment))) { - - int addedLevel = enchantEntry.getValue(); - int newLevel = oldLevel == addedLevel ? addedLevel + 1 : Math.max(oldLevel, addedLevel); - newLevel = Math.min(newLevel, behavior.getEnchantMaxLevel(newEnchantment)); - newEnchants.put(newEnchantment, newLevel); - - levelCost += getTotalCost(baseCost, oldLevel, newLevel); - } else { - levelCost += getNonApplicableCost(); - } - } - - if (levelCost < 0) { - levelCost = state.getAnvil().getMaximumRepairCost(); - } - - int totalLevelCost = levelCost; - - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - return totalLevelCost; - } - - @Override - public void modifyResult(@Nullable ItemMeta itemMeta) { - EnchantmentUtil.addEnchants(itemMeta, newEnchants); - } - }; - - } - - protected int getAnvilCost(Enchantment enchantment, boolean isFromBook) { - int value = enchantment.getAnvilCost(); - return isFromBook ? Math.max(1, value / 2) : value; - } - - protected abstract int getTotalCost(int baseCost, int oldLevel, int newLevel); - - protected abstract int getNonApplicableCost(); - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/RepairMaterial.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/RepairMaterial.java deleted file mode 100644 index c09eba8..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/anvil/RepairMaterial.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import io.papermc.paper.datacomponent.DataComponentTypes; -import io.papermc.paper.datacomponent.item.Repairable; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.TypedKey; -import io.papermc.paper.registry.tag.Tag; -import io.papermc.paper.registry.tag.TagKey; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Predicate; -import net.kyori.adventure.key.Key; -import org.bukkit.Registry; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.jetbrains.annotations.NotNull; - -/** - * Definitions of materials used in anvil repair operations. - */ -public final class RepairMaterial { - - /** - * Get whether an item is repairable by another item. - * - *

N.B. This is for pure material-based repair operations, not for combination operations. - * - * @param base the item to be repaired - * @param addition the item used to repair - * @return whether the addition material can repair the base material - */ - public static boolean repairs(@NotNull ItemStack base, @NotNull ItemStack addition) { - if (MATERIALS_TO_REPAIRABLE != null) { - Predicate predicate = MATERIALS_TO_REPAIRABLE.get(base.getType().getKey()); - return predicate != null && predicate.test(addition.getType().getKey()); - } - - Repairable repairable = base.getData(DataComponentTypes.REPAIRABLE); - - if (repairable == null) { - return false; - } - - // Note: KeyImpl and NamespacedKey have the same hashCode implementation, so - // the fact that ItemTypeKeys all have a KeyImpl instead shouldn't matter. - TypedKey itemKey = TypedKey.create(RegistryKey.ITEM, addition.getType().getKey()); - return repairable.types().contains(itemKey); - } - - private static final Map> MATERIALS_TO_REPAIRABLE; - - static { - // Defend against possible refactors of Repairable component. - boolean repairableAvailable; - try { - ItemType.DIAMOND_PICKAXE.getDefaultData(DataComponentTypes.REPAIRABLE); - repairableAvailable = true; - } catch (LinkageError ignored) { - repairableAvailable = false; - } - - if (repairableAvailable) { - MATERIALS_TO_REPAIRABLE = null; - } else { - MATERIALS_TO_REPAIRABLE = new HashMap<>(); - loadTags(); - loadLists(); - } - } - - private static void loadTags() { - Map, Predicate> tags = new HashMap<>(); - for (Entry entry : BakedRepairableData.getTags().entrySet()) { - ItemType type = Registry.ITEM.get(entry.getKey()); - if (type == null) { - continue; - } - - TagKey tagKey = TagKey.create(RegistryKey.ITEM, entry.getValue()); - if (!Registry.ITEM.hasTag(tagKey)) { - continue; - } - - RepairMaterial.MATERIALS_TO_REPAIRABLE.put( - type.getKey(), - tags.computeIfAbsent(tagKey, innerTagKey -> { - Tag tag = Registry.ITEM.getTag(innerTagKey); - return key -> tag.contains(TypedKey.create(RegistryKey.ITEM, key)); - }) - ); - } - } - - private static void loadLists() { - for (Entry> entry : BakedRepairableData.getLists().entrySet()) { - ItemType type = Registry.ITEM.get(entry.getKey()); - if (type == null) { - continue; - } - - // Note: A Key may be either a KeyImpl or NamespacedKey instance. - // This is a Set rather than a List to make use the fact that they have identical (but not - // shared, oh boy fragility!) hashCode implementations to check containment without iterating - // over every element and manually checking namespace and key. - Set values = new HashSet<>(); - for (Key key : entry.getValue()) { - ItemType value = Registry.ITEM.get(key); - if (value != null) { - values.add(value.getKey()); - } - } - - if (!values.isEmpty()) { - MATERIALS_TO_REPAIRABLE.put(type.getKey(), values::contains); - } - } - } - - private RepairMaterial() { - throw new IllegalStateException("Cannot instantiate static helper method container."); - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/enchant/EnchantmentUtil.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/enchant/EnchantmentUtil.java deleted file mode 100644 index 61a86b0..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/enchant/EnchantmentUtil.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.jikoo.planarenchanting.enchant; - -import java.util.Map; -import java.util.function.BiConsumer; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Common enchantment-related functions. - */ -public final class EnchantmentUtil { - - /** - * Get {@link Enchantment Enchantments} from an {@link ItemMeta}. - * - * @param meta the {@code ItemMeta} - * @return the stored enchantments - */ - public static @NotNull Map getEnchants(@Nullable ItemMeta meta) { - if (meta == null) { - return Map.of(); - } - - if (meta instanceof EnchantmentStorageMeta enchantmentStorageMeta) { - return enchantmentStorageMeta.getStoredEnchants(); - } - - return meta.getEnchants(); - } - - /** - * Add {@link Enchantment Enchantments} to an {@link ItemMeta}. - * - *

N.B. This is an add operation, not set! Existing enchantments not specified are not removed. - * - * @param meta the {@code ItemMeta} - * @param enchants the enchantments to add - */ - public static void addEnchants( - @Nullable ItemMeta meta, - @NotNull Map enchants) { - if (meta == null) { - return; - } - - BiConsumer addEnchant; - if (meta instanceof EnchantmentStorageMeta storageMeta) { - addEnchant = (enchant, level) -> storageMeta.addStoredEnchant(enchant, level, true); - } else { - addEnchant = (enchant, level) -> meta.addEnchant(enchant, level, true); - } - enchants.forEach(addEnchant); - } - - private EnchantmentUtil() { - throw new IllegalStateException("Cannot instantiate static helper method container."); - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/Enchantability.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/Enchantability.java deleted file mode 100644 index bdec42b..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/table/Enchantability.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.github.jikoo.planarenchanting.table; - -import io.papermc.paper.datacomponent.DataComponentTypes; -import io.papermc.paper.datacomponent.item.Enchantable; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import net.kyori.adventure.key.Key; -import org.bukkit.Material; -import org.bukkit.Registry; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.jetbrains.annotations.Range; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -/** - * A representation of how easily an item can be enchanted. - * - *

Note that setting enchantability too high may actually cause some enchantments to be available - * less frequently. For example, {@code minecraft:infinity} is available only when the effective - * enchanting level after modifiers is between two hardcoded values. Higher enchantability - * increases the calculated end number of a randomized bonus range starting at {@code 0}. The higher - * the max, the higher the average. If the total number including the bonus exceeds the maximum, you - * may see very rare enchantments generate at low levels or higher than intended enchantment rarity. - */ -@NullMarked -public record Enchantability(@Range(from = 1, to = Integer.MAX_VALUE) int value) { - - /** - * Get the {@code Enchantability} of a {@link Material}. Will return {@code null} if not - * enchantable in an enchanting table. - * - * @param material the {@code Material} - * @return the {@code Enchantability} if enchantable - */ - public static @Nullable Enchantability forMaterial(Material material) { - if (!material.isItem()) { - return null; - } - if (BY_KEY != null) { - return BY_KEY.get(material.getKey()); - } - ItemType type = material.asItemType(); - return type == null ? null : forType(type); - } - - /** - * Get the {@code Enchantability} of an {@link ItemType}. Will return {@code null} if not - * enchantable in an enchanting table. - * - * @param type the {@code ItemType} - * @return the {@code Enchantability} if enchantable - */ - public static @Nullable Enchantability forType(ItemType type) { - if (BY_KEY != null) { - return BY_KEY.get(type.getKey()); - } - - Enchantable enchantable = type.getDefaultData(DataComponentTypes.ENCHANTABLE); - return enchantable != null ? new Enchantability(enchantable.value()) : null; - } - - /** - * Get the {@code Enchantability} of an {@link ItemStack}. Will return {@code null} if not - * enchantable in an enchanting table. - * - * @param item the {@code ItemStack} - * @return the {@code Enchantability} if enchantable - */ - public static @Nullable Enchantability forItem(ItemStack item) { - if (BY_KEY != null) { - return BY_KEY.get(item.getType().getKey()); - } - - Enchantable enchantable = item.getData(DataComponentTypes.ENCHANTABLE); - return enchantable != null ? new Enchantability(enchantable.value()) : null; - } - - private static final @Nullable Map BY_KEY; - - static { - // Defend against potential refactors of Enchantable component. - boolean enchantableAvailable; - try { - ItemType.DIAMOND_PICKAXE.getDefaultData(DataComponentTypes.ENCHANTABLE); - enchantableAvailable = true; - } catch (LinkageError ignored) { - // Surprise, still compatible! - enchantableAvailable = false; - } - - if (enchantableAvailable) { - // If enchants are available, set fallthrough to null. - // This will serve as a flag in the forType implementation. - BY_KEY = null; - } else { - // Otherwise, use values from last generated version. - BY_KEY = new HashMap<>(); - for (Entry> entry : BakedEnchantableData.get().entrySet()) { - Enchantability enchantability = new Enchantability(entry.getKey()); - for (Key key : entry.getValue()) { - ItemType type = Registry.ITEM.get(key); - if (type != null) { - BY_KEY.put(type.getKey(), enchantability); - } - } - } - } - } - -} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/ItemUtil.java b/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/ItemUtil.java deleted file mode 100644 index 6e288bb..0000000 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/ItemUtil.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.jikoo.planarenchanting.util; - -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.Repairable; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * A collection of item-related utility functions. - */ -public final class ItemUtil { - - /** Constant for air itemstacks. */ - public static final ItemStack AIR = new ItemStack(Material.AIR) { - /** - * Don't do this. Make a new item instead. - * - * @throws UnsupportedOperationException when invoked - * @deprecated Material for AIR constant cannot be changed. - */ - @Contract(value = "_ -> fail", pure = true) - @Deprecated(since = "added") - @Override - public void setType(@NotNull Material type) { - throw new UnsupportedOperationException("Cannot modify AIR constant."); - } - }; - - /** - * Check if an {@link ItemStack} is empty. - * - * @param itemStack the item - * @return whether the item is empty - */ - @Contract("null -> true") - public static boolean isEmpty(@Nullable ItemStack itemStack) { - if (itemStack == null || itemStack.getAmount() < 1 || !itemStack.getType().isItem()) { - return true; - } - ItemType type = itemStack.getType().asItemType(); - return type == null || ItemType.AIR.equals(type); - } - - /** - * Get the repair cost for a {@link CachedValue} of an {@link ItemMeta}, falling through to 0. - * - * @param meta the cached value - * @return the repair cost - */ - public static int getRepairCost(@Nullable ItemMeta meta) { - if (meta instanceof Repairable repairable) { - return repairable.getRepairCost(); - } - - return 0; - } - - private ItemUtil() { - throw new IllegalStateException("Cannot instantiate static helper method container."); - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionTest.java deleted file mode 100644 index 203e7a1..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilFunctionTest.java +++ /dev/null @@ -1,609 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import static com.github.jikoo.planarenchanting.util.matcher.ItemMatcher.isMetaEqual; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.hamcrest.Matchers.not; -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.anyInt; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.InventoryMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.stream.Stream; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.bukkit.Material; -import org.bukkit.Server; -import org.bukkit.Tag; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.Repairable; -import org.bukkit.inventory.view.AnvilView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - -@DisplayName("Default basic AnvilFunctions") -@TestInstance(Lifecycle.PER_CLASS) -class AnvilFunctionTest { - - private static ItemType baseMat; - private static int maxDamage; - private static ItemType repairMat; - - @BeforeAll - void beforeAll() { - Server server = ServerMocks.mockServer(); - - ItemFactory factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); - - baseMat = ItemType.DIAMOND_SHOVEL; - doReturn((short) 1561).when(baseMat).getMaxDurability(); - maxDamage = baseMat.getMaxDurability() - 1; - repairMat = ItemType.DIAMOND; - - // RepairMaterial requires these tags to be set up to test. - Tag tag = Tag.ITEMS_STONE_TOOL_MATERIALS; - doReturn(Set.of(Material.STONE, Material.ANDESITE, Material.GRANITE, Material.DIORITE)) - .when(tag).getValues(); - tag = Tag.PLANKS; - doReturn(Set.of(Material.ACACIA_PLANKS, Material.BIRCH_PLANKS, Material.OAK_PLANKS)) //etc. non-exhaustive list - .when(tag).getValues(); - - EnchantmentMocks.init(); - } - - @Nested - class PriorWorkLevelCost { - - private final AnvilFunction function = AnvilFunctions.PRIOR_WORK_LEVEL_COST; - - @Test - void testPriorWorkLevelCostApplies() { - var anvil = getMockView(baseMat.createItemStack(), baseMat.createItemStack()); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(anvil); - - assertThat("Prior work level cost always applies", function.canApply(behavior, state)); - } - - @ParameterizedTest - @MethodSource("com.github.jikoo.planarenchanting.anvil.AnvilFunctionTest#getPriorWork") - void testPriorWorkLevelCostValues(int baseWork, int addedWork) { - var baseItem = baseMat.createItemStack(); - var anvil = getMockView( - prepareItem(baseItem, 0, baseWork), - prepareItem(baseMat.createItemStack(), 0, addedWork)); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(anvil); - - assertThat("Prior work level cost always applies", function.canApply(behavior, state)); - - var result = function.getResult(behavior, state); - assertThat( - "Cost must be total prior work", - result.getLevelCostIncrease(), - is(baseWork + addedWork)); - - result.modifyResult(state.result.getMeta()); - assertThat( - "Meta must not be changed", - state.result.getMeta(), - isMetaEqual(Objects.requireNonNull(baseItem.getItemMeta()))); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - } - - } - - private static @NotNull Collection getPriorWork() { - Collection arguments = new ArrayList<>(); - int [] values = { 0, 1, 3, 7, 15, 31 }; - - for (int base : values) { - for (int added : values) { - arguments.add(Arguments.of(base, added)); - } - } - - return arguments; - } - - @Nested - @TestInstance(Lifecycle.PER_CLASS) // Should be inherited but isn't for whatever reason - class Rename { - - private final AnvilFunction function = AnvilFunctions.RENAME; - - @DisplayName("Rename requires ItemMeta") - @Test - void testRenameRequiresMeta() { - var base = getNullMetaItem(); - var inventory = getMockView(base, null); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat("Base meta is null", state.getBase().getMeta(), is(nullValue())); - assertThat("Rename requires meta", function.canApply(behavior, state), is(false)); - } - - @DisplayName("Rename requires different name") - @ParameterizedTest - @MethodSource("renameSituations") - void testRenameRequiresDifferentName( - BiConsumer setup, - boolean canApply) { - var base = baseMat.createItemStack(); - var inventory = getMockView(base, null); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat("Base meta is not null", state.getBase().getMeta(), is(notNullValue())); - - setup.accept(state.getBase().getMeta(), inventory); - - assertThat("Rename requires different name", function.canApply(behavior, state), is(canApply)); - } - - @DisplayName("Rename applies name and cost") - @ParameterizedTest - @MethodSource("renameSuccessSituations") - void testRenameApplication(BiConsumer setup) { - var base = baseMat.createItemStack(); - var baseMeta = base.getItemMeta(); - assertThat("Base meta is not null", baseMeta, is(notNullValue())); - var inventory = getMockView(base, null); - setup.accept(baseMeta, inventory); - base.setItemMeta(baseMeta); - inventory.setItem(0, base); - - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Level cost increase is 1", result.getLevelCostIncrease(), is(1)); - - result.modifyResult(state.result.getMeta()); - assertThat( - "Meta must be changed", - state.result.getMeta(), - not(isMetaEqual(base.getItemMeta()))); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - } - - private Stream renameSuccessSituations() { - return renameSituations().filter(arguments -> (boolean) arguments.get()[1]); - } - - private Stream renameSituations() { - Component customName = Component.text("Sample text"); - String customNameText = LegacyComponentSerializer.legacySection().serialize(customName); - return Stream.of( - // NON-APPLICABLE - // Both unnamed - Arguments.of((BiConsumer) (meta, anvil) -> { - meta.customName(null); - when(anvil.getRenameText()).thenReturn(null); - }, false), - // Both identically named - Arguments.of((BiConsumer) (meta, anvil) -> { - meta.customName(customName); - when(anvil.getRenameText()).thenReturn(customNameText); - }, false), - - // APPLICABLE - // Only anvil named - Arguments.of((BiConsumer) (meta, anvil) -> { - meta.customName(null); - when(anvil.getRenameText()).thenReturn(customNameText); - }, true), - // Only item named - Arguments.of((BiConsumer) (meta, anvil) -> { - meta.customName(customName); - when(anvil.getRenameText()).thenReturn(null); - }, true), - // Both named differently - Arguments.of((BiConsumer) (meta, anvil) -> { - meta.customName(customName); - when(anvil.getRenameText()).thenReturn(customNameText + " different text"); - }, true) - ); - } - - } - - @Nested - class UpdatePriorWorkCost { - - private final AnvilFunction function = AnvilFunctions.UPDATE_PRIOR_WORK_COST; - - @Test - void testBaseNotRepairable() { - var base = getNullMetaItem(); - var inventory = getMockView(base, null); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Base must be repairable", - function.canApply(behavior, state), - is(false)); - } - - @Test - void testBaseRepairable() { - var base = baseMat.createItemStack(); - var addition = getNullMetaItem(); - var inventory = getMockView(base, addition); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Repairable base is acceptable", - function.canApply(behavior, state), - is(true)); - } - - @ParameterizedTest - @MethodSource("com.github.jikoo.planarenchanting.anvil.AnvilFunctionTest#getPriorWork") - void testPriorWorkUpdate(int baseWork, int addedWork) { - var baseItem = baseMat.createItemStack(); - var anvil = getMockView( - prepareItem(baseItem, 0, baseWork), - prepareItem(baseMat.createItemStack(), 0, addedWork)); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(anvil); - - assertThat("Prior work level cost always applies", function.canApply(behavior, state)); - - var result = function.getResult(behavior, state); - assertThat("Cost must be unchanged", result.getLevelCostIncrease(), is(0)); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - - ItemMeta resultMeta = state.result.getMeta(); - result.modifyResult(resultMeta); - int resultPriorWork = requireRepairable(resultMeta).getRepairCost(); - - int expectedPriorWork = 1 + (Math.max(baseWork, addedWork) * 2); - assertThat("Prior work must be expected value", resultPriorWork, is(expectedPriorWork)); - - assertThat( - "Repair cost must be changed", - resultPriorWork, - is(greaterThan(requireRepairable(baseItem.getItemMeta()).getRepairCost()))); - assertThat( - "Meta must be changed", - state.result.getMeta(), - not(isMetaEqual(Objects.requireNonNull(baseItem.getItemMeta())))); - } - - @Test - void testMetaNotRepairable() { - var base = getNullMetaItem(); - var inventory = getMockView(base, null); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - var result = function.getResult(behavior, state); - assertDoesNotThrow(() -> result.modifyResult(base.getItemMeta())); - } - - } - - @Nested - class RepairWithMaterial { - - private final AnvilFunction function = AnvilFunctions.REPAIR_WITH_MATERIAL; - - @Test - void testCanApplyNotRepairedBy() { - var inventory = getMockView(null, null); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(false).when(behavior).itemRepairedBy(notNull(), notNull()); - var state = new AnvilState(inventory); - - assertThat( - "Must be repairable by item", - function.canApply(behavior, state), - is(false)); - } - - @Test - void testCanApplyNotDurable() { - var inventory = getMockView(null, null); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemRepairedBy(notNull(), notNull()); - var state = new AnvilState(inventory); - - assertThat( - "Must have durability", - function.canApply(behavior, state), - is(false)); - } - - @Test - void testCanApplyNotDamageable() { - var base = getNullMetaItem(baseMat); - var inventory = getMockView(base, null); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemRepairedBy(notNull(), notNull()); - var state = new AnvilState(inventory); - - assertThat( - "Must have Damageable meta", - function.canApply(behavior, state), - is(false)); - assertThat( - "Non-Damageable must return empty result", - function.getResult(behavior, state), - is(AnvilFunctionResult.EMPTY)); - } - - @Test - void testCanApplyNotDamaged() { - var inventory = getMockView(baseMat.createItemStack(), null); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemRepairedBy(notNull(), notNull()); - var state = new AnvilState(inventory); - - assertThat( - "Must be damaged", - function.canApply(behavior, state), - is(false)); - assertThat( - "Undamaged must return empty result", - function.getResult(behavior, state), - is(AnvilFunctionResult.EMPTY)); - } - - @ParameterizedTest - @ValueSource(ints = { 1, 64 }) - void testRepair(int repairMats) { - var baseItem = getMaxDamageItem(); - var inventory = getMockView(baseItem, repairMat.createItemStack(repairMats)); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemRepairedBy(notNull(), notNull()); - var state = new AnvilState(inventory); - - assertThat( - "Must be applicable", - function.canApply(behavior, state), - is(true)); - - AnvilFunctionResult result = function.getResult(behavior, state); - - result.modifyResult(state.result.getMeta()); - - int damage = requireDamageable(state.result.getMeta()).getDamage(); - - assertThat( - "Material cost must be specified", - result.getMaterialCostIncrease(), - greaterThan(0)); - assertThat("Item must be repaired", - damage, - is(Math.max( - 0, - maxDamage - result.getMaterialCostIncrease() * (baseMat.getMaxDurability() / 4)))); - assertThat( - "Number of items to consume should not exceed number of available items", - result.getMaterialCostIncrease(), - lessThanOrEqualTo(repairMats)); - assertThat( - "Level cost is material cost", - result.getLevelCostIncrease(), - is(result.getMaterialCostIncrease())); - assertThat( - "Meta must be changed", - state.result.getMeta(), - not(isMetaEqual(Objects.requireNonNull(baseItem.getItemMeta())))); - - assertDoesNotThrow(() -> result.modifyResult(null)); - } - - } - - @Nested - class RepairWithCombination { - - private final AnvilFunction function = AnvilFunctions.REPAIR_WITH_COMBINATION; - - @Test - void testCanApplyNotRepairedBy() { - var inventory = getMockView(baseMat.createItemStack(), repairMat.createItemStack()); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Must be same item", - function.canApply(behavior, state), - is(false)); - } - - @Test - void testCanApplyNotDurable() { - var inventory = getMockView(null, null); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Must have durability", - function.canApply(behavior, state), - is(false)); - } - - @Test - void testCanApplyNotDamageable() { - var nullMetaItem = getNullMetaItem(baseMat); - ItemStack normalItem = baseMat.createItemStack(); - var inventory = getMockView(nullMetaItem, normalItem); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Must have Damageable meta", - function.canApply(behavior, state), - is(false)); - assertThat( - "Non-Damageable base must return empty result", - function.getResult(behavior, state), - is(AnvilFunctionResult.EMPTY)); - inventory = getMockView(normalItem, nullMetaItem); - state = new AnvilState(inventory); - assertThat( - "Non-Damageable addition must return empty result", - function.getResult(behavior, state), - is(AnvilFunctionResult.EMPTY)); - } - - @Test - void testCanApplyNotDamaged() { - var inventory = getMockView(baseMat.createItemStack(), baseMat.createItemStack()); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Must be damaged", - function.canApply(behavior, state), - is(false)); - assertThat( - "Undamaged must return empty result", - function.getResult(behavior, state), - is(AnvilFunctionResult.EMPTY)); - } - - @Test - void testRepair() { - var baseItem = getMaxDamageItem(); - var inventory = getMockView(baseItem, baseItem.clone()); - var behavior = AnvilBehavior.VANILLA; - var state = new AnvilState(inventory); - - assertThat( - "Must be applicable", - function.canApply(behavior, state), - is(true)); - - AnvilFunctionResult result = function.getResult(behavior, state); - - result.modifyResult(state.result.getMeta()); - - assertThat("Level cost is 2", result.getLevelCostIncrease(), is(2)); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - assertThat( - "Meta must be changed", - state.result.getMeta(), - not(isMetaEqual(Objects.requireNonNull(baseItem.getItemMeta())))); - - int maxDurability = state.result.getItem().getType().getMaxDurability(); - int remainingDurability = maxDurability - requireDamageable(baseItem.getItemMeta()).getDamage(); - int bonusDurability = maxDurability * 12 / 100; - int expectedDurability = 2 * remainingDurability + bonusDurability; - int expectedDamage = maxDurability - expectedDurability; - final int damage = requireDamageable(state.result.getMeta()).getDamage(); - - assertThat( - "Items' durability should be added with a bonus of 12% of max durability", - damage, - is(expectedDamage)); - assertDoesNotThrow(() -> result.modifyResult(null)); - } - - } - - private static @NotNull AnvilView getMockView( - @Nullable ItemStack base, - @Nullable ItemStack addition) { - var anvil = InventoryMocks.newAnvilMock(); - anvil.setItem(0, base); - anvil.setItem(1, addition); - - var view = mock(AnvilView.class); - doAnswer(params -> anvil.getItem(params.getArgument(0))) - .when(view).getItem(anyInt()); - doAnswer(params -> { - anvil.setItem(params.getArgument(0), params.getArgument(1)); - return null; - }).when(view).setItem(anyInt(), any()); - - return view; - } - - private static ItemStack getNullMetaItem() { - return getNullMetaItem(ItemType.AIR); - } - - private static ItemStack getNullMetaItem(ItemType type) { - ItemStack itemStack = type.createItemStack(); - doReturn(null).when(itemStack).getItemMeta(); - return itemStack; - } - - private static ItemStack getMaxDamageItem() { - return prepareItem(baseMat.createItemStack(), maxDamage, 0); - } - - private static ItemStack prepareItem(ItemStack itemStack, int damage, int repairCost) { - ItemMeta itemMeta = itemStack.getItemMeta(); - - if (damage != 0) { - Damageable damageable = requireDamageable(itemMeta); - damageable.setDamage(damage); - } - - if (repairCost != 0) { - Repairable repairable = requireRepairable(itemMeta); - repairable.setRepairCost(repairCost); - } - - itemStack.setItemMeta(itemMeta); - - return itemStack; - } - - private static Damageable requireDamageable(@Nullable ItemMeta itemMeta) { - assertThat("Meta may not be null", itemMeta, notNullValue()); - assertThat("Item must be damageable", itemMeta, instanceOf(Damageable.class)); - - return (Damageable) itemMeta; - } - - private static Repairable requireRepairable(@Nullable ItemMeta itemMeta) { - assertThat("Meta may not be null", itemMeta, notNullValue()); - assertThat("Item must be repairable", itemMeta, instanceOf(Repairable.class)); - - return (Repairable) itemMeta; - } - -} \ No newline at end of file diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilStateTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilStateTest.java deleted file mode 100644 index 75b15d3..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilStateTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import static com.github.jikoo.planarenchanting.util.matcher.ItemMatcher.isItem; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.InventoryMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; -import java.util.Set; -import org.bukkit.Material; -import org.bukkit.Server; -import org.bukkit.Tag; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.view.AnvilView; -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; - -@DisplayName("Verify AnvilOperationState functionality") -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class AnvilStateTest { - - private AnvilView view; - - @BeforeAll - void beforeAll() { - Server server = ServerMocks.mockServer(); - - ItemFactory factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); - - // RepairMaterial requires these tags to be set up. - Tag tag = Tag.ITEMS_STONE_TOOL_MATERIALS; - doReturn(Set.of(Material.STONE, Material.ANDESITE, Material.GRANITE, Material.DIORITE)) - .when(tag).getValues(); - tag = Tag.PLANKS; - doReturn(Set.of(Material.ACACIA_PLANKS, Material.BIRCH_PLANKS, Material.OAK_PLANKS)) //etc. non-exhaustive list - .when(tag).getValues(); - - EnchantmentMocks.init(); - } - - @BeforeEach - void beforeEach() { - var anvil = InventoryMocks.newAnvilMock(); - view = mock(AnvilView.class); - doAnswer(params -> anvil.getItem(params.getArgument(0))) - .when(view).getItem(anyInt()); - doAnswer(params -> { - anvil.setItem(params.getArgument(0), params.getArgument(1)); - return null; - }).when(view).setItem(anyInt(), any()); - } - - @Test - void testGetAnvil() { - var state = new AnvilState(view); - assertThat("Anvil must be provided instance", state.getAnvil(), is(view)); - } - - @Test - void testBase() { - var base = new ItemStack(Material.DIRT); - view.setItem(0, base); - var state = new AnvilState(view); - assertThat("Base item must match", state.getBase().getItem(), isItem(base)); - } - - @Test - void testAddition() { - var addition = new ItemStack(Material.DIRT); - view.setItem(1, addition); - var state = new AnvilState(view); - assertThat("Addition item must match", state.getAddition().getItem(), isItem(addition)); - } - - @Test - void testGetSetLevelCost() { - var state = new AnvilState(view); - assertThat("Level cost starts at 0", state.getLevelCost(), is(0)); - int value = 10; - state.setLevelCost(value); - assertThat("Level cost is set", state.getLevelCost(), is(value)); - } - - @Test - void testGetSetMaterialCost() { - var state = new AnvilState(view); - assertThat("Material cost starts at 0", state.getMaterialCost(), is(0)); - int value = 10; - state.setMaterialCost(value); - assertThat("Material cost is set", state.getMaterialCost(), is(value)); - } - -} \ No newline at end of file diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilTest.java deleted file mode 100644 index ec75b4d..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/AnvilTest.java +++ /dev/null @@ -1,445 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThan; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.github.jikoo.planarenchanting.util.MetaCachedStack; -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.InventoryMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; -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.ItemTypeKeys; -import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys; -import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; -import io.papermc.paper.registry.tag.Tag; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Stream; -import net.kyori.adventure.text.Component; -import org.bukkit.Material; -import org.bukkit.Registry; -import org.bukkit.Server; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.Repairable; -import org.bukkit.inventory.view.AnvilView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -/* - * Note: These tests are only supposed to cover the functionality of the AnvilOperation class. - * Specific operations are not verified, that is handled in more specific and thorough tests. - */ -@DisplayName("Verify Anvil application") -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class AnvilTest { - - private static ItemType tool; - private static ItemType toolRepair; - public static ItemType book; - private static ItemType incompatible; - public static Enchantment toolEnchantment; - - @BeforeAll - void beforeAll() throws ClassNotFoundException { - Server server = ServerMocks.mockServer(); - - ItemFactory factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); - - EnchantmentMocks.init(); - - tool = ItemType.DIAMOND_PICKAXE; - - // Force RepairMaterial to fall through to pre-baked defaults. - doThrow(IncompatibleClassChangeError.class).when(tool).getDefaultData(any()); - Class.forName("com.github.jikoo.planarenchanting.anvil.RepairMaterial"); - - doReturn((short) 1561).when(tool).getMaxDurability(); - toolRepair = ItemType.DIAMOND; - book = ItemType.ENCHANTED_BOOK; - incompatible = ItemType.STONE; - - // Set up tag for repair. - Registry itemTypeRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM); - Tag tag = itemTypeRegistry.getTag(ItemTypeTagKeys.DIAMOND_TOOL_MATERIALS); - doReturn(Set.of(ItemTypeKeys.DIAMOND)).when(tag).values(); - - // Set up tool enchantment. - toolEnchantment = Enchantment.EFFICIENCY; - tag = itemTypeRegistry.getTag(ItemTypeTagKeys.ENCHANTABLE_MINING); - doReturn(Set.of(TypedKey.create(RegistryKey.ITEM, tool.getKey()))).when(tag).values(); - - // Set up enchantment exclusivity for silk touch and fortune. - Tag toolExclusive = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).getTag( - EnchantmentTagKeys.EXCLUSIVE_SET_MINING); - doReturn(Set.of(EnchantmentKeys.SILK_TOUCH, EnchantmentKeys.FORTUNE)) - .when(toolExclusive).values(); - } - - @Test - void testApplyNonApplicable() { - var function = new AnvilFunction() { - @Override - public boolean canApply( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - return false; - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - return 10; - } - - @Override - public int getMaterialCostIncrease() { - return 10; - } - }; - } - }; - - var anvil = new Anvil(); - var state = new AnvilState(getMockView(null, null)); - assertThat( - "Non-applicable function does not apply", - anvil.apply(state, function), - is(false)); - assertThat("Level cost is unchanged", state.getLevelCost(), is(0)); - assertThat("Material cost is unchanged", state.getMaterialCost(), is(0)); - } - - @Test - void testApply() { - final int value = 10; - var function = new AnvilFunction() { - @Override - public boolean canApply( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - return true; - } - - @Override - public @NotNull AnvilFunctionResult getResult( - @NotNull AnvilBehavior behavior, - @NotNull AnvilState state) { - return new AnvilFunctionResult() { - @Override - public int getLevelCostIncrease() { - return value; - } - - @Override - public int getMaterialCostIncrease() { - return value; - } - }; - } - }; - - var anvil = new Anvil(); - var state = new AnvilState(getMockView(null, null)); - - assertThat("Applicable function applies", anvil.apply(state, function)); - assertThat("Level cost is added", state.getLevelCost(), is(value)); - assertThat("Material cost is added", state.getMaterialCost(), is(value)); - - anvil.apply(state, function); - assertThat("Level cost is added again", state.getLevelCost(), is(value * 2)); - assertThat("Material cost is added again", state.getMaterialCost(), is(value * 2)); - } - - @Test - void testForgeNullBaseMeta() { - var anvil = new Anvil(); - var state = new AnvilState( - getMockView(new ItemStack(Material.AIR) { - @Override - public @Nullable ItemMeta getItemMeta() { - return null; - } - }, null)); - - assertThat("AnvilResult must be empty constant", anvil.forge(state), is(AnvilResult.EMPTY)); - } - - @Test - void testForgeNullResultMeta() { - var state = new AnvilState( - getMockView(new ItemStack(Material.AIR) { - @Override - public @Nullable ItemMeta getItemMeta() { - return null; - } - - @Override - public @NotNull ItemStack clone() { - // Silence compiler warning. - super.clone(); - // Return same instance when cloning - this makes the result have a null meta. - return this; - } - }, null)) { - private final MetaCachedStack fakeBase = new MetaCachedStack(new ItemStack(Material.DIRT)); - @Override - public @NotNull MetaCachedStack getBase() { - return this.fakeBase; - } - }; - var anvil = new Anvil(); - - assertThat("AnvilResult must be empty constant", anvil.forge(state), is(AnvilResult.EMPTY)); - } - - @Test - void testForgeIgnoreRepairCost() { - var state = new AnvilState(getMockView(new ItemStack(Material.DIRT), null)); - var meta = state.result.getMeta(); - - assertThat("Meta must not be null", meta, notNullValue()); - assertThat("Meta must be Repairable", meta, instanceOf(Repairable.class)); - - ((Repairable) meta).setRepairCost(100); - - var anvil = new Anvil(); - assertThat("AnvilResult must be empty constant", anvil.forge(state), is(AnvilResult.EMPTY)); - } - - @Test - void testForgeIgnoreDisplayNameWithAddition() { - var state = new AnvilState( - getMockView(new ItemStack(Material.DIRT), new ItemStack(Material.DIRT))); - var meta = state.result.getMeta(); - - assertThat("Meta must not be null", meta, notNullValue()); - - meta.customName(Component.text("Cool beans")); - - var anvil = new Anvil(); - assertThat("AnvilResult must be empty constant", anvil.forge(state), is(AnvilResult.EMPTY)); - } - - @Test - void testForge() { - var state = new AnvilState(getMockView(new ItemStack(Material.DIRT), null)); - var meta = state.result.getMeta(); - - assertThat("Meta must not be null", meta, notNullValue()); - - meta.customName(Component.text("Cool beans")); - - var anvil = new Anvil(); - assertThat( - "AnvilResult must not be empty constant", - anvil.forge(state), - is(not(AnvilResult.EMPTY))); - } - - @Test - void testEnchantmentTarget() { - var item = new MetaCachedStack(tool.createItemStack()); - assertThat( - "Enchantment applies to tools", - AnvilBehavior.VANILLA.enchantApplies(toolEnchantment, item)); - item = new MetaCachedStack(incompatible.createItemStack()); - assertThat( - "Enchantment does not apply to non-tools", - AnvilBehavior.VANILLA.enchantApplies(toolEnchantment, item), - is(false)); - } - - @Test - void testEnchantmentConflict() { - assertThat( - "Vanilla enchantments conflict", - AnvilBehavior.VANILLA.enchantsConflict(Enchantment.SILK_TOUCH, Enchantment.FORTUNE)); - assertThat( - "Nonconflicting enchantments do not conflict", - AnvilBehavior.VANILLA.enchantsConflict(Enchantment.EFFICIENCY, Enchantment.FORTUNE), - is(false)); - } - - @ParameterizedTest - @MethodSource("getEnchantments") - void testEnchantmentMaxLevel(Enchantment enchantment) { - assertThat( - "Enchantment max level must be vanilla", - AnvilBehavior.VANILLA.getEnchantMaxLevel(enchantment), - is(enchantment.getMaxLevel())); - } - - private static @NotNull Stream getEnchantments() { - return RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).stream(); - } - - @Test - void testSameMaterialEnchantCombination() { - var base = new MetaCachedStack(tool.createItemStack()); - var addition = new MetaCachedStack(tool.createItemStack()); - assertThat( - "Same type combine enchantments", - AnvilBehavior.VANILLA.itemsCombineEnchants(base, addition)); - } - - @Test - void testEnchantedBookEnchantCombination() { - var base = new MetaCachedStack(tool.createItemStack()); - var addition = new MetaCachedStack(book.createItemStack()); - assertThat( - "Enchanted books combine enchantments", - AnvilBehavior.VANILLA.itemsCombineEnchants(base, addition)); - } - - @Test - void testDifferentMaterialEnchantCombination() { - var base = new MetaCachedStack(tool.createItemStack()); - var addition = new MetaCachedStack(incompatible.createItemStack()); - assertThat( - "Incompatible materials do not combine enchantments", - AnvilBehavior.VANILLA.itemsCombineEnchants(base, addition), - is(false)); - } - - @Test - void testEmptyBaseIsEmpty() { - var anvil = getMockView(null, null); - var result = new Anvil().getResult(anvil); - assertThat("Result must be empty", result, is(AnvilResult.EMPTY)); - } - - @Test - void testEmptyAdditionIsEmpty() { - var anvil = getMockView(tool.createItemStack(), null); - var result = new Anvil().getResult(anvil); - assertThat("Result must be empty", result, is(AnvilResult.EMPTY)); - } - - @Test - void testEmptyAdditionRenameNotEmpty() { - var anvil = getMockView(tool.createItemStack(), null); - when(anvil.getRenameText()).thenReturn("Sample Text"); - var result = new Anvil().getResult(anvil); - assertThat("Result must not be empty", result, not(AnvilResult.EMPTY)); - } - - @Test - void testMultipleBaseIsEmpty() { - var base = tool.createItemStack(); - base.setAmount(2); - var addition = tool.createItemStack(); - var anvil = getMockView(base, addition); - var result = new Anvil().getResult(anvil); - assertThat("Result must be empty", result, is(AnvilResult.EMPTY)); - } - - @Test - void testRepairWithMaterial() { - var base = tool.createItemStack(); - var itemMeta = base.getItemMeta(); - - assertThat("ItemMeta must be Damageable", itemMeta, instanceOf(Damageable.class)); - assertThat("Material must have durability", (int) tool.getMaxDurability(), greaterThan(0)); - - Damageable damageable = (Damageable) itemMeta; - Objects.requireNonNull(damageable).setDamage(tool.getMaxDurability() - 1); - base.setItemMeta(damageable); - - var addition = toolRepair.createItemStack(); - - assertThat("Base must be repairable by addition", AnvilBehavior.VANILLA.itemRepairedBy(new MetaCachedStack(base), new MetaCachedStack(addition))); - - var anvil = getMockView(base, addition); - var result = new Anvil().getResult(anvil); - - assertThat("Result must not be empty", result, is(not(AnvilResult.EMPTY))); - - ItemStack resultItem = result.item(); - assertThat("Result must be of original type", resultItem.getType(), is(base.getType())); - ItemMeta resultMeta = resultItem.getItemMeta(); - assertThat("Result must be Damageable", resultMeta, instanceOf(Damageable.class)); - assertThat( - "Result must be repaired", - ((Damageable) Objects.requireNonNull(resultMeta)).getDamage(), - lessThan(damageable.getDamage())); - } - - @Test - void testRepairWithCombination() { - var base = tool.createItemStack(); - var itemMeta = base.getItemMeta(); - - assertThat("ItemMeta must be Damageable", itemMeta, instanceOf(Damageable.class)); - assertThat("Material must have durability", (int) tool.getMaxDurability(), greaterThan(0)); - - Damageable damageable = (Damageable) itemMeta; - Objects.requireNonNull(damageable).setDamage(tool.getMaxDurability() - 1); - base.setItemMeta(damageable); - - var addition = base.clone(); - var anvil = getMockView(base, addition); - var result = new Anvil().getResult(anvil); - - assertThat("Result must not be empty", result, is(not(AnvilResult.EMPTY))); - - ItemStack resultItem = result.item(); - assertThat("Result must be of original type", resultItem.getType(), is(base.getType())); - ItemMeta resultMeta = resultItem.getItemMeta(); - assertThat("Result must be Damageable", resultMeta, instanceOf(Damageable.class)); - assertThat( - "Result must be repaired", - ((Damageable) Objects.requireNonNull(resultMeta)).getDamage(), - lessThan(damageable.getDamage())); - } - - private static @NotNull AnvilView getMockView( - @Nullable ItemStack base, - @Nullable ItemStack addition) { - var anvil = InventoryMocks.newAnvilMock(); - anvil.setItem(0, base); - anvil.setItem(1, addition); - - var view = mock(AnvilView.class); - doAnswer(params -> anvil.getItem(params.getArgument(0))) - .when(view).getItem(anyInt()); - doAnswer(params -> { - anvil.setItem(params.getArgument(0), params.getArgument(1)); - return null; - }).when(view).setItem(anyInt(), any()); - - return view; - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantmentsTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantmentsTest.java deleted file mode 100644 index 1cf279a..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/CombineEnchantmentsTest.java +++ /dev/null @@ -1,495 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.both; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.everyItem; -import static org.hamcrest.Matchers.in; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -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.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.when; - -import com.github.jikoo.planarenchanting.enchant.EnchantmentUtil; -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.InventoryMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.keys.ItemTypeKeys; -import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import io.papermc.paper.registry.tag.Tag; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Server; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.view.AnvilView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; - -@DisplayName("Default enchanting AnvilFunctions") -@TestInstance(Lifecycle.PER_CLASS) -public class CombineEnchantmentsTest { - - private static final Material BASE_MAT = Material.DIAMOND_SHOVEL; - private static Enchantment basicEnchant; - private static Enchantment rareEnchant; - private static Enchantment commonTridentEnchant; - private static Enchantment tridentEnchant; - - @BeforeAll - void beforeAll() { - Server server = ServerMocks.mockServer(); - - ItemFactory factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); - - EnchantmentMocks.init(); - - basicEnchant = Enchantment.EFFICIENCY; - rareEnchant = Enchantment.FORTUNE; - tridentEnchant = Enchantment.RIPTIDE; - - // Set up a fake common-rarity trident enchantment. - // This is necessary because no actual trident enchantments are currently common, so the - // defensive code will not be hit in testing normally. - Enchantment commonTrident = mock(); - NamespacedKey key = NamespacedKey.minecraft("common_trident"); - doReturn(key).when(commonTrident).getKey(); - doReturn(key).when(commonTrident).key(); - doReturn(10).when(commonTrident).getWeight(); - doReturn(1).when(commonTrident).getAnvilCost(); - doAnswer( - invocation -> RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM) - .getTag(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) - ).when(commonTrident).getSupportedItems(); - EnchantmentMocks.putEnchant(commonTrident); - - Tag trident = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(ItemTypeTagKeys.ENCHANTABLE_TRIDENT); - doReturn(Set.of(ItemTypeKeys.TRIDENT)).when(trident).values(); - - commonTridentEnchant = commonTrident; - } - - abstract static class CombineEnchantsTest { - - protected final AnvilFunction function; - - protected CombineEnchantsTest(AnvilFunction function) { - this.function = function; - } - - @Test - void testAppliesIfNotCombine() { - var anvil = getMockView(new ItemStack(BASE_MAT), new ItemStack(BASE_MAT)); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(false).when(behavior).itemsCombineEnchants(notNull(), notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must not apply", function.canApply(behavior, state), is(false)); - } - - @Test - void testAppliesIfCombine() { - var anvil = getMockView(new ItemStack(BASE_MAT), new ItemStack(BASE_MAT)); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - } - - @Test - void testNoEnchantsAdded() { - var anvil = getMockView(new ItemStack(BASE_MAT), new ItemStack(BASE_MAT)); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - var state = new AnvilState(anvil); - - assertThat( - "No enchants added yields empty result", - function.getResult(behavior, state), - is(AnvilFunctionResult.EMPTY)); - } - - @Test - void testBasicAdd() { - Map enchantments = new HashMap<>(); - enchantments.put(Enchantment.EFFICIENCY, 1); - ItemStack addition = new ItemStack(BASE_MAT); - applyEnchantments(addition, enchantments); - - var anvil = getMockView(new ItemStack(BASE_MAT), addition); - var behavior = spy(AnvilBehavior.VANILLA); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - assertThat("Enchantments must be added to result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - - assertDoesNotThrow(() -> result.modifyResult(null)); - } - - @Test - void testBookHalfPrice() { - ItemStack base = new ItemStack(BASE_MAT); - Map enchantments = new HashMap<>(); - enchantments.put(rareEnchant, 1); - ItemStack addition = new ItemStack(BASE_MAT); - applyEnchantments(addition, enchantments); - ItemStack bookAddition = new ItemStack(Material.ENCHANTED_BOOK); - applyEnchantments(bookAddition, enchantments); - - var anvil = getMockView(base, addition); - var bookAnvil = getMockView(base, bookAddition); - - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - - var state = new AnvilState(anvil); - var bookState = new AnvilState(bookAnvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - assertThat("Book combination must apply", function.canApply(behavior, bookState)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - AnvilFunctionResult bookResult = function.getResult(behavior, bookState); - assertThat("Book material cost is unchanged", bookResult.getMaterialCostIncrease(), is(0)); - bookResult.modifyResult(bookState.result.getMeta()); - - assertThat("Enchantments must be added to result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat("Enchantments must be added to book result", - EnchantmentUtil.getEnchants(bookState.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - - assertThat( - "Cost of book application is half regular cost", - bookResult.getLevelCostIncrease(), - is(result.getLevelCostIncrease() / 2)); - } - - @Test - void testCombineAdd() { - Map enchantments = new HashMap<>(); - enchantments.put(basicEnchant, 1); - ItemStack base = new ItemStack(BASE_MAT); - applyEnchantments(base, enchantments); - ItemStack addition = new ItemStack(BASE_MAT); - applyEnchantments(addition, enchantments); - - var anvil = getMockView(base, addition); - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - enchantments.put(basicEnchant, 2); - - assertThat("Enchantments must be merged in result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat( - "Cost must be expected value", - result.getLevelCostIncrease(), - is(getCombineAddCost())); - } - - abstract int getCombineAddCost(); - - @Test - void testMergeInvalid() { - Map enchantments = new HashMap<>(); - enchantments.put(basicEnchant, 1); - ItemStack base = new ItemStack(BASE_MAT); - applyEnchantments(base, enchantments); - ItemStack addition = new ItemStack(BASE_MAT); - Map invalidEnchants = new HashMap<>(); - invalidEnchants.put(rareEnchant, 1); - applyEnchantments(addition, invalidEnchants); - - var anvil = getMockView(base, addition); - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - doReturn(true).when(behavior).enchantsConflict(notNull(), notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - assertThat("Enchantments must not be merged in result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat( - "Cost must be expected value", - result.getLevelCostIncrease(), - is(getMergeInvalidCost())); - } - - abstract int getMergeInvalidCost(); - - @Test - void testCommonTridentEnchant() { - Map enchantments = new HashMap<>(); - enchantments.put(commonTridentEnchant, 1); - ItemStack addition = ItemType.TRIDENT.createItemStack(); - applyEnchantments(addition, enchantments); - ItemStack base = ItemType.TRIDENT.createItemStack(); - - var anvil = getMockView(base, addition); - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - assertThat("Enchantments must be the same in result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat("Cost must be expected value", result.getLevelCostIncrease(), is(1)); - } - - @Test - void testTridentEnchant() { - Map enchantments = new HashMap<>(); - enchantments.put(tridentEnchant, 1); - ItemStack addition = ItemType.TRIDENT.createItemStack(); - applyEnchantments(addition, enchantments); - ItemStack base = ItemType.TRIDENT.createItemStack(); - - var anvil = getMockView(base, addition); - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - assertThat("Enchantments must be the same in result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat( - "Cost must be expected value", - result.getLevelCostIncrease(), - is(getTridentEnchantCost())); - } - - abstract int getTridentEnchantCost(); - - } - - @Nested - class CombineEnchantsJavaEdition extends CombineEnchantsTest { - - protected CombineEnchantsJavaEdition() { - super(AnvilFunctions.COMBINE_ENCHANTMENTS_JAVA_EDITION); - } - - @Override - int getCombineAddCost() { - return 2; - } - - @Override - int getMergeInvalidCost() { - return 1; - } - - @Override - int getTridentEnchantCost() { - return 4; - } - - @Test - void testNegativeCost() { - Map enchantments = new HashMap<>(); - enchantments.put(basicEnchant, -2); - ItemStack base = new ItemStack(BASE_MAT); - applyEnchantments(base, enchantments); - enchantments.put(basicEnchant, -1); - ItemStack addition = new ItemStack(BASE_MAT); - applyEnchantments(addition, enchantments); - - var anvil = getMockView(base, addition); - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - assertThat("Enchantments must be added to result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat( - "Cost must be maximum repair cost", - result.getLevelCostIncrease(), - is(anvil.getMaximumRepairCost())); - } - - } - - @Nested - class CombineEnchantsBedrockEdition extends CombineEnchantsTest { - - protected CombineEnchantsBedrockEdition() { - super(AnvilFunctions.COMBINE_ENCHANTMENTS_BEDROCK_EDITION); - } - - @Override - int getCombineAddCost() { - return 1; - } - - @Override - int getMergeInvalidCost() { - return 0; - } - - @Override - int getTridentEnchantCost() { - return 2; - } - - @Test - void testOldLevelHigher() { - Map enchantments = new HashMap<>(); - enchantments.put(basicEnchant, 1); - ItemStack addition = new ItemStack(BASE_MAT); - applyEnchantments(addition, enchantments); - enchantments.put(basicEnchant, 2); - ItemStack base = new ItemStack(BASE_MAT); - applyEnchantments(base, enchantments); - - var anvil = getMockView(base, addition); - var behavior = mock(AnvilBehavior.class); - doReturn(true).when(behavior).itemsCombineEnchants(notNull(), notNull()); - doReturn(true).when(behavior).enchantApplies(notNull(), notNull()); - doReturn((int) Short.MAX_VALUE).when(behavior).getEnchantMaxLevel(notNull()); - var state = new AnvilState(anvil); - - assertThat("Combination must apply", function.canApply(behavior, state)); - - AnvilFunctionResult result = function.getResult(behavior, state); - assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); - result.modifyResult(state.result.getMeta()); - - assertThat("Enchantments must be the same in result", - EnchantmentUtil.getEnchants(state.result.getMeta()).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - assertThat("Cost must be unchanged", result.getLevelCostIncrease(), is(0)); - } - - } - - private static @NotNull AnvilView getMockView( - @Nullable ItemStack base, - @Nullable ItemStack addition) { - var anvil = InventoryMocks.newAnvilMock(); - anvil.setItem(0, base); - anvil.setItem(1, addition); - - var view = mock(AnvilView.class); - doAnswer(params -> anvil.getItem(params.getArgument(0))) - .when(view).getItem(anyInt()); - doAnswer(params -> { - anvil.setItem(params.getArgument(0), params.getArgument(1)); - return null; - }).when(view).setItem(anyInt(), any()); - - return view; - } - - private static void applyEnchantments( - @NotNull ItemStack itemStack, - @NotNull Map enchantments) { - ItemMeta itemMeta = itemStack.getItemMeta(); - assertThat("Meta may not be null", itemMeta, notNullValue()); - BiConsumer metaAddEnchant = addEnchant(itemMeta); - enchantments.forEach(metaAddEnchant); - itemStack.setItemMeta(itemMeta); - } - - private static BiConsumer addEnchant(@NotNull ItemMeta meta) { - if (meta instanceof EnchantmentStorageMeta storageMeta) { - return (enchantment, level) -> storageMeta.addStoredEnchant(enchantment, level, true); - } - - return (enchantment, level) -> meta.addEnchant(enchantment, level, true); - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/RepairMaterialFallthroughTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/RepairMaterialFallthroughTest.java deleted file mode 100644 index 57da080..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/RepairMaterialFallthroughTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import io.papermc.paper.datacomponent.DataComponentTypes; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.TypedKey; -import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; -import io.papermc.paper.registry.tag.Tag; -import org.bukkit.Registry; -import org.bukkit.inventory.ItemType; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import java.util.Set; - -class RepairMaterialFallthroughTest { - - @BeforeAll - static void setUpAll() throws ClassNotFoundException { - ServerMocks.mockServer(); - - // Set up tag for fallthrough. - Tag tag = Registry.ITEM.getTag(ItemTypeTagKeys.DIAMOND_TOOL_MATERIALS); - doReturn(Set.of(TypedKey.create(RegistryKey.ITEM, ItemType.DIAMOND.getKey()))).when(tag).values(); - - // Set up error for Repairable check. - var componentType = DataComponentTypes.REPAIRABLE; - ItemType type = ItemType.DIAMOND_PICKAXE; - doThrow(IncompatibleClassChangeError.class).when(type).getDefaultData(componentType); - - // Touch RepairMaterial to initialize. - Class.forName("com.github.jikoo.planarenchanting.anvil.RepairMaterial"); - } - - @Test - void repairs() { - assertThat( - "Item is repairable", - RepairMaterial.repairs( - ItemType.DIAMOND_PICKAXE.createItemStack(), - ItemType.DIAMOND.createItemStack() - ) - ); - } - - @Test - void repairsInvalid() { - assertThat( - "Item is not repairable with wrong material", - RepairMaterial.repairs( - ItemType.DIAMOND_PICKAXE.createItemStack(), - ItemType.DIAMOND_PICKAXE.createItemStack() - ), - is(false) - ); - } - - @Test - void repairsNotRepairable() { - assertThat( - "Item is not repairable", - RepairMaterial.repairs( - ItemType.DIAMOND.createItemStack(), - ItemType.DIAMOND_PICKAXE.createItemStack() - ), - is(false) - ); - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/RepairMaterialTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/RepairMaterialTest.java deleted file mode 100644 index da988bd..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/anvil/RepairMaterialTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.github.jikoo.planarenchanting.anvil; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import io.papermc.paper.datacomponent.DataComponentTypes; -import io.papermc.paper.datacomponent.item.Repairable; -import io.papermc.paper.registry.set.RegistryKeySet; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class RepairMaterialTest { - - @BeforeAll - static void setUpAll() throws ClassNotFoundException { - ServerMocks.mockServer(); - - // Set up Repairable functionality before usage. - var componentType = DataComponentTypes.REPAIRABLE; - ItemType type = ItemType.DIAMOND_PICKAXE; - Repairable repairable = mock(); - doReturn(repairable).when(type).getDefaultData(componentType); - - // Touch RepairMaterial to initialize. - Class.forName("com.github.jikoo.planarenchanting.anvil.RepairMaterial"); - } - - @Test - void repairs() { - Repairable repairable = mock(); - RegistryKeySet keys = mock(); - doReturn(true).when(keys).contains(any()); - doReturn(keys).when(repairable).types(); - ItemStack item = ItemType.DIAMOND_PICKAXE.createItemStack(); - doReturn(repairable).when(item).getData(DataComponentTypes.REPAIRABLE); - - assertThat( - "Item is repairable", - RepairMaterial.repairs(item, ItemType.DIAMOND.createItemStack()) - ); - } - - @Test - void repairsInvalid() { - Repairable repairable = mock(); - RegistryKeySet keys = mock(); - doReturn(keys).when(repairable).types(); - ItemStack item = ItemType.DIAMOND_PICKAXE.createItemStack(); - doReturn(repairable).when(item).getData(DataComponentTypes.REPAIRABLE); - - assertThat( - "Item is not repairable", - RepairMaterial.repairs(item, ItemType.DIAMOND.createItemStack()), - is(false) - ); - } - - @Test - void repairsNotRepairable() { - assertThat( - "Item is not repairable", - RepairMaterial.repairs( - ItemType.DIAMOND_PICKAXE.createItemStack(), - ItemType.DIAMOND.createItemStack() - ), - is(false) - ); - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/enchant/EnchantmentUtilTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/enchant/EnchantmentUtilTest.java deleted file mode 100644 index ac0566e..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/enchant/EnchantmentUtilTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.github.jikoo.planarenchanting.enchant; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.both; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.everyItem; -import static org.hamcrest.Matchers.in; -import static org.hamcrest.Matchers.is; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; -import org.bukkit.inventory.meta.ItemMeta; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; - -@DisplayName("Enchantment utility methods") -@TestInstance(Lifecycle.PER_CLASS) -class EnchantmentUtilTest { - - @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); - } - - @Test - void testEnchantsEmptyIfNull() { - assertThat("Null meta is empty", EnchantmentUtil.getEnchants(null), is(anEmptyMap())); - } - - @Test - void testGetEnchantsStorageMeta() { - var meta = ItemFactoryMocks.createMeta(EnchantmentStorageMeta.class); - - assertThat("Meta is empty", EnchantmentUtil.getEnchants(meta), is(anEmptyMap())); - - Map enchantments = new HashMap<>(); - enchantments.put(Enchantment.EFFICIENCY, 10); - enchantments.put(Enchantment.LUCK_OF_THE_SEA, 5); - - for (Entry enchant : enchantments.entrySet()) { - meta.addStoredEnchant(enchant.getKey(), enchant.getValue(), true); - } - - assertThat("Enchantments must be retrieved from result", - EnchantmentUtil.getEnchants(meta).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - } - - @Test - void testSetEnchantsStorageMeta() { - var meta = ItemFactoryMocks.createMeta(EnchantmentStorageMeta.class); - - assertThat("Meta is empty", EnchantmentUtil.getEnchants(meta), is(anEmptyMap())); - - Map enchantments = new HashMap<>(); - enchantments.put(Enchantment.EFFICIENCY, 10); - enchantments.put(Enchantment.LUCK_OF_THE_SEA, 5); - - EnchantmentUtil.addEnchants(meta, enchantments); - - assertThat("Enchantments must be retrieved from result", - meta.getStoredEnchants().entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - } - - @Test - void testGetEnchants() { - var meta = ItemFactoryMocks.createMeta(ItemMeta.class); - - assertThat("Meta is empty", EnchantmentUtil.getEnchants(meta), is(anEmptyMap())); - - Map enchantments = new HashMap<>(); - enchantments.put(Enchantment.EFFICIENCY, 10); - enchantments.put(Enchantment.LUCK_OF_THE_SEA, 5); - - for (Entry enchant : enchantments.entrySet()) { - meta.addEnchant(enchant.getKey(), enchant.getValue(), true); - } - - assertThat("Enchantments must be retrieved from result", - EnchantmentUtil.getEnchants(meta).entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - } - - @Test - void testSetEnchants() { - var meta = ItemFactoryMocks.createMeta(ItemMeta.class); - - assertThat("Meta is empty", EnchantmentUtil.getEnchants(meta), is(anEmptyMap())); - - Map enchantments = new HashMap<>(); - enchantments.put(Enchantment.EFFICIENCY, 10); - enchantments.put(Enchantment.LUCK_OF_THE_SEA, 5); - - EnchantmentUtil.addEnchants(meta, enchantments); - - assertThat("Enchantments must be retrieved from result", - meta.getEnchants().entrySet(), - both(everyItem(is(in(enchantments.entrySet())))).and( - containsInAnyOrder(enchantments.entrySet().toArray()))); - } - -} \ No newline at end of file diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityFallthroughTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityFallthroughTest.java deleted file mode 100644 index 01bc6e9..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityFallthroughTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.github.jikoo.planarenchanting.table; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import org.bukkit.Material; -import org.bukkit.inventory.ItemType; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class EnchantabilityFallthroughTest { - - @BeforeAll - static void setUpAll() throws ClassNotFoundException { - ServerMocks.mockServer(); - - // Set up error for item enchantability checks. - ItemType type = ItemType.DIAMOND_PICKAXE; - doThrow(IncompatibleClassChangeError.class).when(type).getDefaultData(any()); - - // Touch Enchantability. - Class.forName("com.github.jikoo.planarenchanting.table.Enchantability"); - } - - @Test - void forMaterialEnchantable() { - assertThat( - "Diamond pickaxe is enchantable", - Enchantability.forMaterial(Material.DIAMOND_PICKAXE), - is(notNullValue()) - ); - } - - @Test - void forMaterialUnenchantable() { - assertThat( - "Diamond is not enchantable", - Enchantability.forMaterial(Material.DIAMOND), - is(nullValue()) - ); - } - - @Test - void forTypeEnchantable() { - assertThat( - "Diamond pickaxe is enchantable", - Enchantability.forType(ItemType.DIAMOND_PICKAXE), - is(notNullValue()) - ); - } - - @Test - void forTypeUnenchantable() { - assertThat( - "Diamond is not enchantable", - Enchantability.forType(ItemType.DIAMOND), - is(nullValue()) - ); - } - - @Test - void forItemEnchantable() { - var item = ItemType.DIAMOND_PICKAXE.createItemStack(); - - assertThat( - "Diamond pickaxe is enchantable", - Enchantability.forItem(item), - is(notNullValue()) - ); - } - - @Test - void forItemUnenchantable() { - var item = ItemType.DIAMOND.createItemStack(); - - assertThat( - "Diamond is not enchantable", - Enchantability.forItem(item), - is(nullValue()) - ); - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityTest.java deleted file mode 100644 index 75690d2..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantabilityTest.java +++ /dev/null @@ -1,274 +0,0 @@ -package com.github.jikoo.planarenchanting.table; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import io.papermc.paper.datacomponent.DataComponentTypes; -import io.papermc.paper.datacomponent.item.Enchantable; -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.TypedKey; -import io.papermc.paper.registry.keys.ItemTypeKeys; -import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; -import io.papermc.paper.registry.tag.Tag; -import io.papermc.paper.registry.tag.TagKey; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; -import net.kyori.adventure.key.Key; -import org.bukkit.Material; -import org.bukkit.Registry; -import org.bukkit.inventory.ItemType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class EnchantabilityTest { - - @BeforeAll - void setUpAll() throws IllegalAccessException { - ServerMocks.mockServer(); - EnchantmentMocks.init(); - setUpTags(); - - // Touch Enchantable type so we can use it as a parameter when mocking. - var componentType = DataComponentTypes.ENCHANTABLE; - - // Mock all baked data. - BakedEnchantableData.get().forEach((value, holders) -> { - Enchantable enchantable = mock(); - doReturn(value).when(enchantable).value(); - for (Key key : holders) { - ItemType itemType = Registry.ITEM.get(key); - doReturn(enchantable).when(itemType).getDefaultData(componentType); - } - }); - } - - private static void setUpTags() throws IllegalAccessException { - setValues(ItemTypeTagKeys.ENCHANTABLE_BOW, Set.of(ItemTypeKeys.BOW)); - setValues(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW, Set.of(ItemTypeKeys.CROSSBOW)); - setValues(ItemTypeTagKeys.ENCHANTABLE_MACE, Set.of(ItemTypeKeys.MACE)); - setValues(ItemTypeTagKeys.ENCHANTABLE_TRIDENT, Set.of(ItemTypeKeys.TRIDENT)); - setValues(ItemTypeTagKeys.ENCHANTABLE_FISHING, Set.of(ItemTypeKeys.FISHING_ROD)); - - Set> lunge = new HashSet<>(); - Set> sweeping = new HashSet<>(); - - Set> head = new HashSet<>(); - Set> chest = new HashSet<>(); - Set> leg = new HashSet<>(); - Set> foot = new HashSet<>(); - - Set> axe = new HashSet<>(); - Set> pick = new HashSet<>(); - Set> tool = new HashSet<>(); - - // Assemble main lists based on item name. - // This helps catch issues like new materials not being included (copper), - // but it doesn't help with new categories (spears). - for (Field field : ItemTypeKeys.class.getFields()) { - if (field.getType() != TypedKey.class) { - continue; - } - @SuppressWarnings("unchecked") - TypedKey value = (TypedKey) field.get(null); - String name = field.getName(); - // Weapons - if (name.endsWith("_SPEAR")) { - lunge.add(value); - continue; - } else if (name.endsWith("_SWORD")) { - sweeping.add(value); - continue; - } - // Armor - if (name.endsWith("_HELMET")) { - head.add(value); - continue; - } else if (name.endsWith("_CHESTPLATE")) { - chest.add(value); - continue; - } else if (name.endsWith("_LEGGINGS")) { - leg.add(value); - continue; - } else if (name.endsWith("_BOOTS")) { - foot.add(value); - continue; - } - - // Tools - if (name.endsWith("_AXE")) { - axe.add(value); - tool.add(value); - } else if (name.endsWith("_SHOVEL") || name.endsWith("_HOE")) { - tool.add(value); - } else if (name.endsWith("_PICKAXE")) { - pick.add(value); - tool.add(value); - } - } - - setValues(ItemTypeTagKeys.ENCHANTABLE_LUNGE, lunge); - setValues(ItemTypeTagKeys.ENCHANTABLE_SWEEPING, sweeping); - Set> melee = new HashSet<>(); - melee.addAll(lunge); - melee.addAll(sweeping); - setValues(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON, melee); - Set> fireAspect = new HashSet<>(); - fireAspect.add(ItemTypeKeys.MACE); - fireAspect.addAll(melee); - setValues(ItemTypeTagKeys.ENCHANTABLE_FIRE_ASPECT, fireAspect); - Set> sharp = new HashSet<>(); - sharp.addAll(melee); - sharp.addAll(axe); - setValues(ItemTypeTagKeys.ENCHANTABLE_SHARP_WEAPON, sharp); - Set> weapon = new HashSet<>(); - weapon.add(ItemTypeKeys.MACE); - weapon.addAll(sharp); - setValues(ItemTypeTagKeys.ENCHANTABLE_WEAPON, weapon); - - setValues(ItemTypeTagKeys.ENCHANTABLE_HEAD_ARMOR, head); - setValues(ItemTypeTagKeys.ENCHANTABLE_CHEST_ARMOR, chest); - setValues(ItemTypeTagKeys.ENCHANTABLE_LEG_ARMOR, leg); - setValues(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR, foot); - Set> armor = new HashSet<>(); - armor.addAll(head); - armor.addAll(chest); - armor.addAll(leg); - armor.addAll(foot); - setValues(ItemTypeTagKeys.ENCHANTABLE_ARMOR, armor); - - setValues(ItemTypeTagKeys.ENCHANTABLE_MINING, tool); - setValues(ItemTypeTagKeys.ENCHANTABLE_MINING_LOOT, pick); - - Set> durability = new HashSet<>(); - durability.addAll(melee); - durability.addAll(armor); - durability.addAll(tool); - durability.addAll(Set.of( - ItemTypeKeys.BOW, - ItemTypeKeys.BRUSH, - ItemTypeKeys.CARROT_ON_A_STICK, - ItemTypeKeys.CROSSBOW, - ItemTypeKeys.ELYTRA, - ItemTypeKeys.FISHING_ROD, - ItemTypeKeys.FLINT_AND_STEEL, - ItemTypeKeys.MACE, - ItemTypeKeys.SHEARS, - ItemTypeKeys.SHIELD, - ItemTypeKeys.TRIDENT, - ItemTypeKeys.WARPED_FUNGUS_ON_A_STICK - )); - setValues(ItemTypeTagKeys.ENCHANTABLE_DURABILITY, durability); - } - - private static void setValues(TagKey key, Set> values) { - // We need to pull the tag first so the test registry has finished mocking it. - var tag = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(key); - // Then we can safely mock our values. - doReturn(values).when(tag).values(); - } - - @ParameterizedTest - @MethodSource("com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks#getEnchantingTableTags") - void verifySetup(Tag tag) { - // A test test. The rabbit hole goes deeper! - // This will help catch missed tags. - assertThat(tag.tagKey() + " is not empty", tag.values(), is(not(empty()))); - } - - @ParameterizedTest - @MethodSource("getMaterials") - void forMaterial(@NotNull Material material) { - var value = Enchantability.forMaterial(material); - if (material.isItem() && isTableEnchantable(material.asItemType())) { - assertThat("Enchantable material is listed", value, is(notNullValue())); - } else { - assertThat("Unenchantable material is not listed", value, is(nullValue())); - } - } - - @ParameterizedTest - @MethodSource("getTypes") - void forType(@NotNull ItemType material) { - var value = Enchantability.forType(material); - if (isTableEnchantable(material)) { - assertThat("Enchantable material is listed", value, is(notNullValue())); - } else { - assertThat("Unenchantable material is not listed", value, is(nullValue())); - } - } - - @Test - void forItemEnchantable() { - Enchantable enchantable = mock(); - doReturn(100).when(enchantable).value(); - var item = ItemType.DIAMOND.createItemStack(); - doReturn(enchantable).when(item).getData(DataComponentTypes.ENCHANTABLE); - - assertThat( - "ItemStack enchantable component must be used", - Enchantability.forItem(item), - is(new Enchantability(enchantable.value())) - ); - } - - @Test - void forItemUnenchantable() { - var item = ItemType.DIAMOND.createItemStack(); - doReturn(null).when(item).getData(DataComponentTypes.ENCHANTABLE); - - assertThat( - "ItemStack without enchantable component has no enchantability", - Enchantability.forItem(item), - is(nullValue()) - ); - } - - static Stream getMaterials() { - return Arrays.stream(Material.values()) - .filter(material -> !material.name().startsWith("LEGACY_")); - } - - static Stream getTypes() { - return getMaterials() - .filter(Material::isItem) - .map(Material::asItemType); - } - - static boolean isTableEnchantable(@Nullable ItemType material) { - if (material == null) { - return false; - } - return switch (material.key().value()) { - case "book" -> true; - // Wolf armor "enchantability" is a placeholder because ArmorMaterial requires one. - // It isn't actually used. - case "wolf_armor" -> false; - // Valid targets for unbreaking via anvil only. - case "brush", "carrot_on_a_stick", "elytra", "flint_and_steel", "shears", "shield", "warped_fungus_on_a_stick" -> false; - default -> { - TypedKey typedKey = TypedKey.create(RegistryKey.ITEM, material.key()); - yield EnchantmentMocks.getEnchantingTableTags().stream() - .anyMatch(tag -> tag.contains(typedKey)); - } - }; - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableTest.java deleted file mode 100644 index a291e55..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.github.jikoo.planarenchanting.table; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.both; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.is; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Random; -import org.bukkit.enchantments.Enchantment; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -/** - * Unit tests for enchantments. - * - *

As a developer, I want to be able to generate enchantments - * because I would like to support enchanting tables. - * - *

Feature: Calculate enchantments for special items - *
Given I am a user - *
When I attempt to enchant an item - *
And the item is a special item - *
Then the item should recieve applicable enchantments - */ -@DisplayName("Feature: Calculate enchantments for enchanting tables.") -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class EnchantingTableTest { - - private static final Random RANDOM = new Random(0); - private static Collection toolEnchants; - - @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); - toolEnchants = List.of( - Enchantment.EFFICIENCY, - Enchantment.UNBREAKING, - Enchantment.FORTUNE, - Enchantment.SILK_TOUCH - ); - } - - @DisplayName("Empty enchantment list yields empty enchantments.") - @Test - void testEmptyEnchantList() { - var operation = new EnchantingTable(List.of(), EnchantabilityCategory.STONE_TOOL); - - assertThat( - "Empty available enchants yields empty enchants", - operation.apply(RANDOM, 1), - is(anEmptyMap())); - } - - @DisplayName("Enchantment incompatibility can be customized.") - @Test - void testAllIncompatibleAlwaysSingle() { - var operation = new EnchantingTable(toolEnchants, EnchantabilityCategory.GOLD_ARMOR); - operation.setIncompatibility((a, b) -> true); - - assertThat( - "All enchantments incompatible with others yields single enchantment", - operation.apply(RANDOM, 1), - is(aMapWithSize(1))); - } - - @DisplayName("Enchantment level max can be modified.") - @Test - void testSetMaxLevel() { - Enchantment enchant = Enchantment.EFFICIENCY; - var operation = new EnchantingTable(List.of(enchant), EnchantabilityCategory.GOLD_ARMOR); - // Double max level for enchants that go over 1. - operation.setMaxLevel(enchant1 -> enchant1.getMaxLevel() > 1 ? enchant1.getMaxLevel() * 2 : 1); - - String assertation = "High level enchantment generates higher level enchantments"; - RANDOM.setSeed(assertation.hashCode()); - assertThat( - assertation, - operation.apply(RANDOM, 50), - both(hasEntry(is(enchant), greaterThan(enchant.getMaxLevel()))).and(aMapWithSize(1))); - } - - @DisplayName("When enchantments are selected") - @Nested - class EnchantmentAttempt { - - private Map selected; - - @BeforeEach - void beforeEach() { - var operation = new EnchantingTable(toolEnchants, EnchantabilityCategory.STONE_TOOL); - selected = operation.apply(RANDOM, RANDOM.nextInt(1, 31)); - } - - @DisplayName("One or more enchantments should be selected.") - @Test - void checkSize() { - var operation = new EnchantingTable(toolEnchants, EnchantabilityCategory.STONE_TOOL); - selected = operation.apply(RANDOM, 30); - assertThat( - "One or more enchantments must be selected", - selected, - is(aMapWithSize(greaterThan(0)))); - } - - @DisplayName("Enchantments should not conflict.") - @RepeatedTest(10) - void checkConflict() { - Enchantment[] enchantments = selected.keySet().toArray(new Enchantment[0]); - for (int i = 0; i < enchantments.length; ++i) { - for (int j = 0; j < enchantments.length; ++j) { - if (i == j) { - continue; - } - assertThat( - "Enchantments may not conflict", - conflicts(enchantments[i], enchantments[j]), - is(false)); - } - } - } - - private boolean conflicts( - @NotNull Enchantment enchantment1, - @NotNull Enchantment enchantment2) { - if (enchantment1.equals(enchantment2)) { - return true; - } - return enchantment1.conflictsWith(enchantment2) || enchantment2.conflictsWith(enchantment1); - } - - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableUtilTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableUtilTest.java deleted file mode 100644 index 0a94746..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/table/EnchantingTableUtilTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.github.jikoo.planarenchanting.table; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.both; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.everyItem; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThanOrEqualTo; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.enchantments.EnchantmentMocks; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -@DisplayName("Enchanting table utility methods") -@TestInstance(Lifecycle.PER_CLASS) -class EnchantingTableUtilTest { - - @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); - } - - @DisplayName("Enchanting table button levels should be calculated consistently.") - @ParameterizedTest - @CsvSource({"1,0", "10,0", "15,0", "1,12348", "10,98124", "15,23479"}) - void testGetButtonLevels(int shelves, int seed) { - Random random = new Random(seed); - int[] buttonLevels1 = EnchantingTable.getButtonLevels(random, shelves); - random.setSeed(seed); - int[] buttonLevels2 = EnchantingTable.getButtonLevels(random, shelves); - - assertThat("There are always three buttons", buttonLevels1.length, is(3)); - assertThat("There are always three buttons", buttonLevels2.length, is(3)); - - List buttonLevelsList1 = Arrays.stream(buttonLevels1).boxed().toList(); - - assertThat( - "Button levels should be generated consistently", - buttonLevelsList1, - contains(buttonLevels2[0], buttonLevels2[1], buttonLevels2[2])); - assertThat( - "Button levels must be positive integers that do not exceed 30", - buttonLevelsList1, - everyItem(is(both(lessThanOrEqualTo(30)).and(greaterThanOrEqualTo(0))))); - - } - -} \ No newline at end of file diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/CachedValueTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/CachedValueTest.java deleted file mode 100644 index 1272562..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/CachedValueTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.jikoo.planarenchanting.util; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.Arrays; -import java.util.Collection; -import java.util.function.Supplier; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.opentest4j.AssertionFailedError; - -@DisplayName("Cache value from supplier") -@TestInstance(Lifecycle.PER_METHOD) -class CachedValueTest { - - @ParameterizedTest - @MethodSource("getBooleans") - void testCachedValue(Boolean value) { - SingleUseSupplier singleUseSupplier = new SingleUseSupplier<>(value); - var cache = new CachedValue<>(singleUseSupplier); - - assertThat("Value must be supplied as expected", cache.get(), is(value)); - assertThrows( - AssertionFailedError.class, - singleUseSupplier::get, - "Supplier may only be used once"); - assertDoesNotThrow(cache::get, "Supplier must only be used once"); - } - - @Contract(pure = true) - private static @NotNull Collection getBooleans() { - return Arrays.asList( - Boolean.TRUE, - Boolean.FALSE, - null - ); - } - - private static class SingleUseSupplier implements Supplier { - private final T value; - private boolean used = false; - - private SingleUseSupplier(T value) { - this.value = value; - } - - @Override - public T get() { - if (used) { - fail("Supplier may only be used once!"); - } - used = true; - return value; - } - - } - -} \ No newline at end of file diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/ItemUtilTest.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/ItemUtilTest.java deleted file mode 100644 index f2fb100..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/ItemUtilTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.github.jikoo.planarenchanting.util; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.github.jikoo.planarenchanting.util.mock.ServerMocks; -import com.github.jikoo.planarenchanting.util.mock.inventory.ItemFactoryMocks; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.Repairable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; - -@DisplayName("Item utility methods") -@TestInstance(Lifecycle.PER_CLASS) -class ItemUtilTest { - - @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - } - - @Test - void testAirConstantImmutable() { - assertThrows(UnsupportedOperationException.class, () -> ItemUtil.AIR.setType(Material.DIRT)); - } - - @Test - void testEmptyIfNull() { - assertThat("Null ItemStack is empty", ItemUtil.isEmpty(null)); - } - - @Test - void testEmptyIfAir() { - assertThat("Air ItemStack is empty", ItemUtil.isEmpty(new ItemStack(Material.AIR))); - } - - @Test - void testEmptyIfNegative() { - var item = new ItemStack(Material.DIRT); - item.setAmount(-5); - assertThat("Negative ItemStack is empty", ItemUtil.isEmpty(item)); - } - - @Test - void testNotEmpty() { - assertThat( - "Normal ItemStack is not empty", - ItemUtil.isEmpty(new ItemStack(Material.DIRT)), - is(false)); - } - - @Test - void testRepairCostNonRepairable() { - assertThat("Non-repairable meta is 0 cost", ItemUtil.getRepairCost(null), is(0)); - } - - @Test - void testRepairCostRepairable() { - var value = 10; - var meta = ItemFactoryMocks.createMeta(Repairable.class); - meta.setRepairCost(value); - assertThat("Repairable returns cost", ItemUtil.getRepairCost(meta), is(value)); - } - -} \ No newline at end of file diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/matcher/ItemMatcher.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/matcher/ItemMatcher.java deleted file mode 100644 index 5b07ae2..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/matcher/ItemMatcher.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.github.jikoo.planarenchanting.util.matcher; - -import org.bukkit.Bukkit; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class ItemMatcher { - - /** - * Construct a new {@code ItemEqualMatcher} for the given {@link ItemStack}. - * - * @param other the matchable item - * @return the resulting matcher - */ - public static BaseMatcher isItem(@NotNull ItemStack other) { - return new ItemEqualMatcher(other); - } - - private static class ItemEqualMatcher extends BaseMatcher { - - private final @NotNull ItemStack other; - - public ItemEqualMatcher(@NotNull ItemStack other) { - this.other = other; - } - - @Override - public boolean matches(Object actual) { - // Cannot use .equals because the backing ItemStack impl is a mock. - // Instead, leverage our isSimilar implementation and compare the one remaining value. - return actual instanceof ItemStack actualItem - && other.isSimilar(actualItem) - && other.getAmount() == actualItem.getAmount(); - } - - @Override - public void describeTo(Description description) { - description.appendText("item ").appendValue(other.toString()); - } - } - - /** - * Construct a new {@code IsSimilarMatcher} for the given {@link ItemStack}. - * - * @param other the matchable item - * @return the resulting matcher - */ - public static BaseMatcher isSimilar(@NotNull ItemStack other) { - return new IsSimilarMatcher(other); - } - - private static class IsSimilarMatcher extends BaseMatcher { - - private final @NotNull ItemStack other; - - private IsSimilarMatcher(@NotNull ItemStack other) { - this.other = other; - } - - @Override - public boolean matches(@Nullable Object actual) { - return actual instanceof ItemStack actualItem && other.isSimilar(actualItem); - } - - @Override - public void describeTo(Description description) { - description.appendText(other.toString()); - } - - } - - /** - * Construct a new {@code MetaIsEqualMatcher} for the given {@link ItemMeta}. - * - * @param other the matchable meta - * @return the resulting matcher - */ - public static BaseMatcher isMetaEqual(@NotNull ItemMeta other) { - return new MetaIsEqualMatcher(other); - } - - private static class MetaIsEqualMatcher extends BaseMatcher { - - private final @NotNull ItemMeta other; - - private MetaIsEqualMatcher(@NotNull ItemMeta other) { - this.other = other; - } - - @Override - public boolean matches(@Nullable Object actual) { - return actual instanceof ItemMeta actualMeta && Bukkit.getItemFactory().equals(other, actualMeta); - } - - @Override - public void describeTo(Description description) { - description.appendText(other.toString()); - } - - } - -} diff --git a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/enchantments/EnchantmentMocks.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/enchantments/EnchantmentMocks.java deleted file mode 100644 index 57bc193..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/enchantments/EnchantmentMocks.java +++ /dev/null @@ -1,496 +0,0 @@ -package com.github.jikoo.planarenchanting.util.mock.enchantments; - -import static org.bukkit.enchantments.Enchantment.AQUA_AFFINITY; -import static org.bukkit.enchantments.Enchantment.BANE_OF_ARTHROPODS; -import static org.bukkit.enchantments.Enchantment.BINDING_CURSE; -import static org.bukkit.enchantments.Enchantment.BLAST_PROTECTION; -import static org.bukkit.enchantments.Enchantment.BREACH; -import static org.bukkit.enchantments.Enchantment.CHANNELING; -import static org.bukkit.enchantments.Enchantment.DENSITY; -import static org.bukkit.enchantments.Enchantment.DEPTH_STRIDER; -import static org.bukkit.enchantments.Enchantment.EFFICIENCY; -import static org.bukkit.enchantments.Enchantment.FEATHER_FALLING; -import static org.bukkit.enchantments.Enchantment.FIRE_ASPECT; -import static org.bukkit.enchantments.Enchantment.FIRE_PROTECTION; -import static org.bukkit.enchantments.Enchantment.FLAME; -import static org.bukkit.enchantments.Enchantment.FORTUNE; -import static org.bukkit.enchantments.Enchantment.FROST_WALKER; -import static org.bukkit.enchantments.Enchantment.IMPALING; -import static org.bukkit.enchantments.Enchantment.INFINITY; -import static org.bukkit.enchantments.Enchantment.KNOCKBACK; -import static org.bukkit.enchantments.Enchantment.LOOTING; -import static org.bukkit.enchantments.Enchantment.LOYALTY; -import static org.bukkit.enchantments.Enchantment.LUCK_OF_THE_SEA; -import static org.bukkit.enchantments.Enchantment.LUNGE; -import static org.bukkit.enchantments.Enchantment.LURE; -import static org.bukkit.enchantments.Enchantment.MENDING; -import static org.bukkit.enchantments.Enchantment.MULTISHOT; -import static org.bukkit.enchantments.Enchantment.PIERCING; -import static org.bukkit.enchantments.Enchantment.POWER; -import static org.bukkit.enchantments.Enchantment.PROJECTILE_PROTECTION; -import static org.bukkit.enchantments.Enchantment.PROTECTION; -import static org.bukkit.enchantments.Enchantment.PUNCH; -import static org.bukkit.enchantments.Enchantment.QUICK_CHARGE; -import static org.bukkit.enchantments.Enchantment.RESPIRATION; -import static org.bukkit.enchantments.Enchantment.RIPTIDE; -import static org.bukkit.enchantments.Enchantment.SHARPNESS; -import static org.bukkit.enchantments.Enchantment.SILK_TOUCH; -import static org.bukkit.enchantments.Enchantment.SMITE; -import static org.bukkit.enchantments.Enchantment.SOUL_SPEED; -import static org.bukkit.enchantments.Enchantment.SWEEPING_EDGE; -import static org.bukkit.enchantments.Enchantment.SWIFT_SNEAK; -import static org.bukkit.enchantments.Enchantment.THORNS; -import static org.bukkit.enchantments.Enchantment.UNBREAKING; -import static org.bukkit.enchantments.Enchantment.VANISHING_CURSE; -import static org.bukkit.enchantments.Enchantment.WIND_BURST; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; - -import 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 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; -import org.bukkit.NamespacedKey; -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 { - - private static final Map KEYS_TO_ENCHANTS = new HashMap<>(); - private static final Set> ENCHANTING_TABLE_TAGS = new HashSet<>(); - - public static void init() { - // 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); - - 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)); - - config(THORNS) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CHEST_ARMOR) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) - .weight(1) - .maxLevel(3) - .minModCost(perLvl(10, 20)) - .maxModCost(perLvl(60, 20)); - - 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); - - config(BINDING_CURSE) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_EQUIPPABLE) - .weight(1) - .minModCost(flat(25)) - .maxModCost(flat(50)); - - 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)); - - 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)); - - 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); - - 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); - - 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)); - - 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); - - config(LUNGE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_LUNGE) - .weight(5) - .maxLevel(3) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(25, 8)); - - config(CHANNELING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) - .weight(1) - .minModCost(flat(25)) - .maxModCost(flat(50)); - - 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); - - 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)); - - 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); - Enchantment stored = KEYS_TO_ENCHANTS.get(declaredEnchant.getKey()); - if (stored == null) { - missingInternalEnchants.add(declaredEnchant.getKey().toString()); - } - } - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - - 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); - } - - public static @NotNull @UnmodifiableView Set> getEnchantingTableTags() { - return Collections.unmodifiableSet(ENCHANTING_TABLE_TAGS); - } - - private static @NotNull IntUnaryOperator perLvl(int base, int perLevel) { - return level -> base + (level - 1) * perLevel; - } - - 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); - doReturn(target).when(enchantment).getPrimaryItems(); - 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/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/InventoryMocks.java b/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/InventoryMocks.java deleted file mode 100644 index 68b9347..0000000 --- a/enchanting-core/src/test/java/com/github/jikoo/planarenchanting/util/mock/inventory/InventoryMocks.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.jikoo.planarenchanting.util.mock.inventory; - -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.List; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.AnvilInventory; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.mockito.ArgumentMatchers; - -public class InventoryMocks { - - public static @NotNull AnvilInventory newAnvilMock() { - return newMock(AnvilInventory.class, InventoryType.ANVIL, 3); - } - - public static @NotNull T newMock(Class inventoryClass, InventoryType type, int slots) { - T inventory = mock(inventoryClass); - - when(inventory.getType()).thenReturn(type); - when(inventory.getSize()).thenReturn(slots); - - List items = new ArrayList<>(slots); - for (int i = 0; i < slots; ++i) { - items.add(null); - } - - when(inventory.getContents()).thenAnswer(invocation -> items.toArray(new ItemStack[0])); - when(inventory.getItem(ArgumentMatchers.anyInt())).thenAnswer(invocation -> { - int index = invocation.getArgument(0); - if (index < 0 || index >= slots) { - throw new IndexOutOfBoundsException(index); - } - return items.get(index); - }); - doAnswer(invocation -> { - int index = invocation.getArgument(0); - if (index < 0 || index >= slots) { - throw new IndexOutOfBoundsException(index); - } - items.set(index, invocation.getArgument(1)); - return null; - }).when(inventory).setItem(ArgumentMatchers.anyInt(), ArgumentMatchers.any()); - - return inventory; - } - -} diff --git a/enchanting-generator/build.gradle.kts b/enchanting-generator/build.gradle.kts index 389003f..b6aad62 100644 --- a/enchanting-generator/build.gradle.kts +++ b/enchanting-generator/build.gradle.kts @@ -1,22 +1,16 @@ import org.gradle.kotlin.dsl.register -repositories { - maven("https://repo.papermc.io/repository/maven-public/") - maven("https://jitpack.io/") -} - plugins { alias(libs.plugins.io.papermc.paperweight) } dependencies { implementation(libs.com.palantir.javapoet.javapoet) - implementation(libs.io.papermc.paper.paper.api) paperweight.paperDevBundle(libs.versions.io.papermc.paper.paper.api) } -var core: Project = project(":planarenchanting") -var generationDir: Directory = core.layout.projectDirectory.dir("src/generated/java") +var common: Project = project(":enchanting-common") +var generationDir: Directory = common.layout.projectDirectory.dir("src/generated/java") val generate = tasks.register("generate") { dependsOn("removeGeneratedFiles", "build") @@ -38,7 +32,7 @@ tasks.register("removeGeneratedLogs") { delete(layout.projectDirectory.dir("logs")) } -core.tasks.named("compileJava") { +common.tasks.named("compileJava") { // Require compilation to wait on generation to complete if run at the same time. mustRunAfter(generate) } diff --git a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/Main.java b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/Main.java index a53cd22..a19172b 100644 --- a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/Main.java +++ b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/Main.java @@ -1,6 +1,7 @@ package com.github.jikoo.planarenchanting.generator; -import com.github.jikoo.planarenchanting.generator.impl.EnchDataGenerator; +import com.github.jikoo.planarenchanting.generator.impl.EnchantDataGenerator; +import com.github.jikoo.planarenchanting.generator.impl.EnchantableGenerator; import com.github.jikoo.planarenchanting.generator.impl.EnchantabilityCategoryGenerator; import com.github.jikoo.planarenchanting.generator.impl.RepairMaterialsGenerator; import java.io.IOException; @@ -21,11 +22,12 @@ public static void main(String[] args) throws IOException { // Set up generators. List itemGens = List.of( - new EnchDataGenerator(), + new EnchantableGenerator(), new RepairMaterialsGenerator() ); List gens = new ArrayList<>(itemGens); gens.add(new EnchantabilityCategoryGenerator()); + gens.add(new EnchantDataGenerator()); // Initialize TypeSpec being built and any other data. for (Generator gen : gens) { diff --git a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchantDataGenerator.java b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchantDataGenerator.java new file mode 100644 index 0000000..36cde33 --- /dev/null +++ b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchantDataGenerator.java @@ -0,0 +1,164 @@ +package com.github.jikoo.planarenchanting.generator.impl; + +import static javax.lang.model.element.Modifier.FINAL; +import static javax.lang.model.element.Modifier.STATIC; + +import com.github.jikoo.planarenchanting.generator.Generator; +import com.palantir.javapoet.AnnotationSpec; +import com.palantir.javapoet.ClassName; +import com.palantir.javapoet.MethodSpec; +import com.palantir.javapoet.ParameterizedTypeName; +import com.palantir.javapoet.TypeName; +import com.palantir.javapoet.TypeSpec; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.IntUnaryOperator; +import net.minecraft.core.Holder.Reference; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.Enchantment.Cost; +import net.minecraft.world.item.enchantment.Enchantments; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class EnchantDataGenerator extends Generator { + + private final ClassName enchantData; + + public EnchantDataGenerator() { + // I know, it breaks the naming pattern, but BakedEnchantDataData felt a little too on the nose. + super("com.github.jikoo.planarenchanting.util", "BakedEnchantData"); + enchantData = ClassName.get(generatedClass.packageName(), "EnchantData"); + } + + @Override + protected TypeSpec.Builder create() { + builder = TypeSpec.classBuilder(generatedClass).addModifiers(FINAL) + .addJavadoc( + "Pre-baked data used for {@link $T}.\n\n", + ClassName.get(generatedClass.packageName(), "MetaEnchantProvider") + ); + addGet(builder); + addCreate(builder); + return builder; + } + + private void addGet(TypeSpec.Builder builder) { + TypeName mapType = ParameterizedTypeName.get( + ClassName.get(Map.class), + ClassName.get(NamespacedKey.class).annotated(AnnotationSpec.builder(Nullable.class).build()), + ParameterizedTypeName.get( + ClassName.get(Function.class), + ClassName.get(org.bukkit.enchantments.Enchantment.class), + enchantData + ) + ); + + MethodSpec.Builder getter = MethodSpec.methodBuilder("get") + .addModifiers(STATIC) + .returns(mapType) + .addStatement("$T map = new $T<>()", mapType, HashMap.class); + + getter.addComment( + "", + Enchantments.class.getName() + ); + + // Produces similar lines to the following for each enchant: + // load(NamespacedKey.fromString("minecraft:silk_touch"), create(1, 8, lvl -> 15, lvl -> 65)); + VanillaRegistries.createLookup() + .lookupOrThrow(Registries.ENCHANTMENT) + .listElements() + .sorted(Comparator.comparing(ref -> ref.key().identifier())) + .forEach(ref -> addEnchant(getter, ref)); + + getter.addComment(""); + getter.addStatement("return map"); + + builder.addMethod(getter.build()); + } + + private static void addEnchant(MethodSpec.Builder getter, Reference ref) { + Identifier identifier = ref.key().identifier(); + getter.addCode("map.put($T.fromString($S), ", NamespacedKey.class, identifier.toString()); + + Enchantment.EnchantmentDefinition def = ref.value().definition(); + getter.addCode("create($L, $L, ", def.weight(), def.anvilCost()); + addCost(getter, def.minCost()); + getter.addCode(", "); + addCost(getter, def.maxCost()); + + getter.addStatement("))"); + } + + private static void addCost(MethodSpec.Builder method, Cost cost) { + if (cost.perLevelAboveFirst() == 0) { + method.addCode("lvl -> $L", cost.base()); + } else { + method.addCode("lvl -> $L + $L * (lvl - 1)", cost.base(), cost.perLevelAboveFirst()); + } + } + + private void addCreate(TypeSpec.Builder builder) { + builder.addMethod( + MethodSpec.methodBuilder("create") + .addModifiers(STATIC) + .addParameter(int.class, "weight") + .addParameter(int.class, "anvilCost") + .addParameter(IntUnaryOperator.class, "minModCost") + .addParameter(IntUnaryOperator.class, "maxModCost") + .returns( + ParameterizedTypeName.get( + ClassName.get(Function.class), + ClassName.get(org.bukkit.enchantments.Enchantment.class), + enchantData + ) + ) + .addCode( + """ + return enchant -> new $T() { + @Override + public int getWeight() { + return weight; + } + + @Override + public int getAnvilCost() { + return anvilCost; + } + + @Override + public int getMinModifiedCost(int level) { + return minModCost.applyAsInt(level); + } + + @Override + public int getMaxModifiedCost(int level) { + return maxModCost.applyAsInt(level); + } + + @Override + public boolean isTridentEnchant() { + return enchant.canEnchantItem(new $T($T.TRIDENT)) + && !enchant.canEnchantItem(new $T($T.DIAMOND_SWORD)); + } + }; + """, + enchantData, + ItemStack.class, Material.class, + ItemStack.class, Material.class + ) + .build() + ); + } + +} diff --git a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchDataGenerator.java b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchantableGenerator.java similarity index 76% rename from enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchDataGenerator.java rename to enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchantableGenerator.java index 1a09063..af3fef9 100644 --- a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchDataGenerator.java +++ b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/EnchantableGenerator.java @@ -5,6 +5,7 @@ import static javax.lang.model.element.Modifier.STATIC; import com.github.jikoo.planarenchanting.generator.ItemConsumingGenerator; +import com.palantir.javapoet.AnnotationSpec; import com.palantir.javapoet.ClassName; import com.palantir.javapoet.MethodSpec; import com.palantir.javapoet.ParameterizedTypeName; @@ -17,27 +18,31 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import net.kyori.adventure.key.Key; import net.minecraft.core.component.DataComponents; import net.minecraft.resources.Identifier; import net.minecraft.world.item.Item; import net.minecraft.world.item.Items; import net.minecraft.world.item.enchantment.Enchantable; +import org.bukkit.NamespacedKey; import org.jetbrains.annotations.UnknownNullability; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; @NullMarked -public class EnchDataGenerator extends ItemConsumingGenerator { +public class EnchantableGenerator extends ItemConsumingGenerator { private MethodSpec.@UnknownNullability Builder getter; - public EnchDataGenerator() { + public EnchantableGenerator() { super("com.github.jikoo.planarenchanting.table", "Enchantable"); } @Override protected TypeSpec.Builder create() { - ParameterizedTypeName setType = ParameterizedTypeName.get(Set.class, Key.class); + ParameterizedTypeName setType = ParameterizedTypeName.get( + ClassName.get(Set.class), + ClassName.get(NamespacedKey.class).annotated(AnnotationSpec.builder(Nullable.class).build()) + ); ParameterizedTypeName returnType = ParameterizedTypeName.get( ClassName.get(Map.class), TypeName.INT.box(), // ClassName.BOXED_INT isn't visible :( @@ -62,8 +67,11 @@ protected TypeSpec.Builder create() { ); return TypeSpec.classBuilder(generatedClass) - .addJavadoc("Pre-baked enchantment data used as a fallthrough for Enchantability.\n\n") - // package-private; only Enchantability needs access. + .addJavadoc( + "Pre-baked data used as a fallthrough for {@link $T}.\n\n", + ClassName.get(generatedClass.packageName(), "Enchantable") + ) + // package-private; only Enchantabilities needs access. .addModifiers(FINAL) .addMethod(MethodSpec.constructorBuilder().addModifiers(PRIVATE).build()); } @@ -73,8 +81,8 @@ public void processItem(Identifier key, Item item) { Enchantable enchantable = item.components().get(DataComponents.ENCHANTABLE); if (enchantable != null) { getter.addStatement( - "map.computeIfAbsent($L, create).add($T.key($S, $S))", - enchantable.value(),Key.class, key.getNamespace(), key.getPath() + "map.computeIfAbsent($L, create).add($T.fromString($S))", + enchantable.value(), NamespacedKey.class, key.toString() ); } } diff --git a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/RepairMaterialsGenerator.java b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/RepairMaterialsGenerator.java index c203e61..9c71c06 100644 --- a/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/RepairMaterialsGenerator.java +++ b/enchanting-generator/src/main/java/com/github/jikoo/planarenchanting/generator/impl/RepairMaterialsGenerator.java @@ -4,6 +4,7 @@ import com.github.jikoo.planarenchanting.generator.ItemConsumingGenerator; import com.mojang.datafixers.util.Either; +import com.palantir.javapoet.AnnotationSpec; import com.palantir.javapoet.ClassName; import com.palantir.javapoet.MethodSpec; import com.palantir.javapoet.ParameterizedTypeName; @@ -14,7 +15,6 @@ import java.util.List; import java.util.Map; import javax.lang.model.element.Modifier; -import net.kyori.adventure.key.Key; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; @@ -23,8 +23,10 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.Items; import net.minecraft.world.item.enchantment.Repairable; +import org.bukkit.NamespacedKey; import org.jetbrains.annotations.UnknownNullability; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; @NullMarked public class RepairMaterialsGenerator extends ItemConsumingGenerator { @@ -38,7 +40,13 @@ public RepairMaterialsGenerator() { @Override protected TypeSpec.Builder create() { - ParameterizedTypeName returnType = ParameterizedTypeName.get(Map.class, Key.class, Key.class); + AnnotationSpec nullable = AnnotationSpec.builder(Nullable.class).build(); + + ParameterizedTypeName returnType = ParameterizedTypeName.get( + ClassName.get(Map.class), + ClassName.get(NamespacedKey.class).annotated(nullable), + ClassName.get(NamespacedKey.class).annotated(nullable) + ); tagGetter = MethodSpec.methodBuilder("getTags") .addModifiers(Modifier.STATIC) .returns(returnType) @@ -50,8 +58,11 @@ protected TypeSpec.Builder create() { returnType = ParameterizedTypeName.get( ClassName.get(Map.class), - ClassName.get(Key.class), - ParameterizedTypeName.get(List.class, Key.class) + ClassName.get(NamespacedKey.class).annotated(nullable), + ParameterizedTypeName.get( + ClassName.get(List.class), + ClassName.get(NamespacedKey.class).annotated(nullable) + ) ); listGetter = MethodSpec.methodBuilder("getLists") .addModifiers(Modifier.STATIC) @@ -63,7 +74,10 @@ protected TypeSpec.Builder create() { ); return TypeSpec.classBuilder(generatedClass) - .addJavadoc("Pre-baked data used as a fallthrough for RepairMaterial.\n\n") + .addJavadoc( + "Pre-baked data used for {@link $T}.\n\n", + ClassName.get(generatedClass.packageName(), "RepairMaterial") + ) // package-private; only RepairMaterial needs access. .addModifiers(Modifier.FINAL) .addMethod(MethodSpec.constructorBuilder().addModifiers(PRIVATE).build()); @@ -83,8 +97,8 @@ public void processItem(Identifier key, Item item) { tagKey -> { Identifier id = tagKey.location(); tagGetter.addStatement( - "map.put($T.key($S, $S), $T.key($S, $S))", - Key.class, key.getNamespace(), key.getPath(), Key.class, id.getNamespace(), id.getPath() + "map.put($T.fromString($S), $T.fromString($S))", + NamespacedKey.class, key.toString(), NamespacedKey.class, id.toString() ); return null; }, @@ -93,8 +107,8 @@ public void processItem(Identifier key, Item item) { return null; } listGetter.addCode( - "map.put($T.key($S, $S), $T.of(", - Key.class, key.getNamespace(), key.getPath(), List.class + "map.put($T.fromString($S), $T.of(", + NamespacedKey.class, key.toString(), List.class ); boolean first = true; @@ -107,7 +121,7 @@ public void processItem(Identifier key, Item item) { Item value = holder.value(); Identifier id = BuiltInRegistries.ITEM.getKey(value); - listGetter.addCode("$T.key($S, $S)", Key.class, id.getNamespace(), id.getPath()); + listGetter.addCode("$T.fromString($S)", NamespacedKey.class, id.toString()); } listGetter.addStatement("))"); @@ -129,5 +143,4 @@ public void generate(Path dir) throws IOException { super.generate(dir); } - } diff --git a/enchanting-meta/build.gradle.kts b/enchanting-meta/build.gradle.kts new file mode 100644 index 0000000..be7e56e --- /dev/null +++ b/enchanting-meta/build.gradle.kts @@ -0,0 +1,12 @@ +repositories { + maven("https://hub.spigotmc.org/nexus/content/groups/public/") +} + +dependencies { + compileOnly(libs.org.spigotmc.spigot.api) + implementation(project(":enchanting-common")) { + exclude(group = "com.github.jikoo", module = "planarwrappers") + } + + testImplementation(libs.org.spigotmc.spigot.api) +} diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaAnvilFunctions.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaAnvilFunctions.java new file mode 100644 index 0000000..7c3d5e1 --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaAnvilFunctions.java @@ -0,0 +1,293 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.Material; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * {@link ItemMeta}-based {@link AnvilFunctionsProvider}. + */ +@NullMarked +public final class MetaAnvilFunctions implements AnvilFunctionsProvider { + + public static final AnvilFunction PRIOR_WORK_LEVEL_COST = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + return true; + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return getRepairCost(state.getBase().getMeta()) + + getRepairCost(state.getAddition().getMeta()); + } + }; + } + }; + public static final AnvilFunction RENAME = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + var itemMeta = state.getBase().getMeta(); + + if (itemMeta == null) { + return false; + } + + // If names are not the same, can be applied. + String customName = itemMeta.hasDisplayName() ? itemMeta.getDisplayName() : null; + String anvilText = state.getAnvilView().getRenameText(); + if (customName == null) { + return anvilText != null; + } + return !customName.equals(anvilText); + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + // Renames always apply a level cost of 1. + return 1; + } + + @Override + public void modifyResult(MetaCachedStack item) { + ItemMeta itemMeta = item.getMeta(); + if (itemMeta == null) { + return; + } + + itemMeta.setDisplayName(state.getAnvilView().getRenameText()); + if (itemMeta instanceof Repairable repairable) { + int repairCost = Math.max( + getRepairCost(state.getBase().getMeta()), + getRepairCost(state.getAddition().getMeta())); + repairable.setRepairCost(repairCost); + } + } + }; + } + }; + public static final AnvilFunction UPDATE_PRIOR_WORK_COST = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + return state.getBase().getMeta() instanceof Repairable; + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + return new AnvilFunctionResult<>() { + @Override + public void modifyResult(MetaCachedStack item) { + if (item.getMeta() instanceof Repairable repairable) { + int priorRepairCost = Math.max( + getRepairCost(state.getBase().getMeta()), + getRepairCost(state.getAddition().getMeta()) + ); + repairable.setRepairCost(priorRepairCost * 2 + 1); + } + } + }; + } + }; + public static final AnvilFunction REPAIR_WITH_MATERIAL = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + MetaCachedStack base = state.getBase(); + return behavior.itemRepairedBy(base, state.getAddition()) + && base.getItem().getType().getMaxDurability() > 0 + && base.getMeta() instanceof Damageable damageable + && damageable.getDamage() > 0; + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + if (!(state.getBase().getMeta() instanceof Damageable damageable)) { + // If result is not damageable, it cannot be repaired. + return AnvilFunctionResult.empty(); + } + + int missingDurability = damageable.getDamage(); + + if (missingDurability < 1) { + // If result is not damaged, no repair. + return AnvilFunctionResult.empty(); + } + + int repairPerMaterial = state.getBase().getItem().getType().getMaxDurability() / 4; + int repairsNeeded = Math.ceilDiv(missingDurability, repairPerMaterial); + + final int repairsAvailable = Math.min(repairsNeeded, state.getAddition().getItem().getAmount()); + final int resultDamage = Math.max(0, missingDurability - (repairsAvailable * repairPerMaterial)); + + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return repairsAvailable; + } + + @Override + public int getMaterialCostIncrease() { + return repairsAvailable; + } + + @Override + public void modifyResult(MetaCachedStack stack) { + if (stack.getMeta() instanceof Damageable damageable) { + damageable.setDamage(resultDamage); + } + } + }; + } + }; + public static final AnvilFunction REPAIR_WITH_COMBINATION = new AnvilFunction<>() { + @Override + public boolean canApply( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + MetaCachedStack base = state.getBase(); + Material baseMat = base.getItem().getType(); + return baseMat == state.getAddition().getItem().getType() + && baseMat.getMaxDurability() > 0 + && base.getMeta() instanceof Damageable damageable + && damageable.getDamage() > 0; + } + + @Override + public AnvilFunctionResult getResult( + AnvilBehavior behavior, + ViewState state, + MetaCachedStack result + ) { + if (!(state.getBase().getMeta() instanceof Damageable baseDamageable + && state.getAddition().getMeta() instanceof Damageable additionDamageable)) { + return AnvilFunctionResult.empty(); + } + + int missingDurability = baseDamageable.getDamage(); + + if (missingDurability < 1) { + return AnvilFunctionResult.empty(); + } + + int maxDurability = state.getBase().getItem().getType().getMaxDurability(); + // Restore durability remaining in added item. + int restoredDurability = maxDurability - additionDamageable.getDamage(); + // Add a bonus 12% total tool durability to the repair. + restoredDurability += (int) (maxDurability * 0.12); + + // Finalize for later use. + int resultDamage = Math.max(0, missingDurability - restoredDurability); + + return new AnvilFunctionResult<>() { + @Override + public int getLevelCostIncrease() { + return 2; + } + + @Override + public void modifyResult(MetaCachedStack stack) { + if (stack.getMeta() instanceof Damageable damageable) { + damageable.setDamage(resultDamage); + } + } + }; + } + }; + public static final AnvilFunction COMBINE_ENCHANTMENTS_JAVA; + public static final AnvilFunction COMBINE_ENCHANTMENTS_BEDROCK; + + public static final MetaAnvilFunctions INSTANCE = new MetaAnvilFunctions(); + + static { + MetaEnchantmentAccess access = new MetaEnchantmentAccess(); + COMBINE_ENCHANTMENTS_JAVA = new CombineEnchants<>(CombineEnchants.Platform.JAVA, access); + COMBINE_ENCHANTMENTS_BEDROCK = new CombineEnchants<>(CombineEnchants.Platform.BEDROCK, access); + } + + private MetaAnvilFunctions() {} + + @Override + public AnvilFunction addPriorWorkLevelCost() { + return PRIOR_WORK_LEVEL_COST; + } + + @Override + public AnvilFunction rename() { + return RENAME; + } + + @Override + public AnvilFunction setItemPriorWork() { + return UPDATE_PRIOR_WORK_COST; + } + + @Override + public AnvilFunction repairWithMaterial() { + return REPAIR_WITH_MATERIAL; + } + + @Override + public AnvilFunction repairWithCombine() { + return REPAIR_WITH_COMBINATION; + } + + @Override + public AnvilFunction combineEnchantsJava() { + return COMBINE_ENCHANTMENTS_JAVA; + } + + @Override + public AnvilFunction combineEnchantsBedrock() { + return COMBINE_ENCHANTMENTS_BEDROCK; + } + + private static int getRepairCost(@Nullable ItemMeta itemMeta) { + if (itemMeta instanceof Repairable repairable) { + return repairable.getRepairCost(); + } + return 0; + } + +} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/MetaCachedStack.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaCachedStack.java similarity index 89% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/MetaCachedStack.java rename to enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaCachedStack.java index d36af6f..67b49d8 100644 --- a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/MetaCachedStack.java +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaCachedStack.java @@ -1,5 +1,7 @@ -package com.github.jikoo.planarenchanting.util; +package com.github.jikoo.planarenchanting.anvil; +import com.github.jikoo.planarenchanting.util.CachedValue; +import com.github.jikoo.planarenchanting.util.ItemUtil; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.NotNull; diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaEnchantmentAccess.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaEnchantmentAccess.java new file mode 100644 index 0000000..a6fd663 --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaEnchantmentAccess.java @@ -0,0 +1,45 @@ +package com.github.jikoo.planarenchanting.anvil; + +import com.github.jikoo.planarenchanting.util.EnchantmentAccess; +import java.util.Map; +import java.util.function.BiConsumer; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class MetaEnchantmentAccess implements EnchantmentAccess { + + @Override + public boolean isBook(MetaCachedStack metaCachedStack) { + return metaCachedStack.getItem().getType() == Material.ENCHANTED_BOOK; + } + + @Override + public Map getEnchantments(MetaCachedStack metaCachedStack) { + if (metaCachedStack.getMeta() instanceof EnchantmentStorageMeta storageMeta) { + return storageMeta.getStoredEnchants(); + } + ItemMeta itemMeta = metaCachedStack.getMeta(); + return itemMeta != null ? itemMeta.getEnchants() : Map.of(); + } + + @Override + public void addEnchantments(MetaCachedStack metaCachedStack, Map enchantments) { + BiConsumer consumer; + if (metaCachedStack.getMeta() instanceof EnchantmentStorageMeta storageMeta) { + consumer = (ench, lvl) -> storageMeta.addStoredEnchant(ench, lvl, true); + } else { + ItemMeta itemMeta = metaCachedStack.getMeta(); + if (itemMeta == null) { + return; + } + consumer = (ench, lvl) -> itemMeta.addEnchant(ench, lvl, true); + } + + enchantments.forEach(consumer); + } + +} diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaTemperer.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaTemperer.java new file mode 100644 index 0000000..f14f6a2 --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaTemperer.java @@ -0,0 +1,55 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class MetaTemperer implements Temperer { + + static final MetaTemperer INSTANCE = new MetaTemperer(); + + @Override + public boolean hasChanged( + MetaCachedStack base, + MetaCachedStack addition, + MetaCachedStack result + ) { + ItemMeta baseMeta = base.getMeta(); + ItemMeta resultMeta = result.getMeta(); + + // If the base or the result has no meta, it is empty. + if (baseMeta == null || resultMeta == null) { + return false; + } + + // Reset certain characteristics when verifying that changes have actually been performed. + resultMeta = resultMeta.clone(); + + // Ignore repair cost changes. + if (baseMeta instanceof Repairable baseRepairable + && resultMeta instanceof Repairable resultRepairable) { + resultRepairable.setRepairCost(baseRepairable.getRepairCost()); + } + // Ignore name changes if addition is not empty. + if (addition.getItem().getType() != Material.AIR) { + resultMeta.setDisplayName(baseMeta.getDisplayName()); + } + + // If reset meta is not identical then an operation is occurring. + // Note that meta must be compared via ItemFactory#equals(ItemMeta, ItemMeta) for test purposes. + return !Bukkit.getItemFactory().equals(baseMeta, resultMeta); + } + + @Override + public ItemStack temper(MetaCachedStack result) { + result.getItem().setItemMeta(result.getMeta()); + return result.getItem(); + } + + private MetaTemperer() {} + +} diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaVanillaBehavior.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaVanillaBehavior.java new file mode 100644 index 0000000..25df10f --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaVanillaBehavior.java @@ -0,0 +1,116 @@ +package com.github.jikoo.planarenchanting.anvil; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.NullMarked; + +/** + * {@link org.bukkit.inventory.meta.ItemMeta}-based {@link AnvilBehavior}. + * Uses pre-baked data to cover missing API. + */ +@NullMarked +public class MetaVanillaBehavior implements AnvilBehavior { + + @Override + public boolean enchantApplies(Enchantment enchantment, MetaCachedStack base) { + return enchantment.canEnchantItem(base.getItem()); + } + + @Override + public boolean itemsCombineEnchants(MetaCachedStack base, MetaCachedStack addition) { + Material addedType = addition.getItem().getType(); + return base.getItem().getType() == addedType || addedType == Material.ENCHANTED_BOOK; + } + + @Override + public boolean itemRepairedBy(MetaCachedStack repaired, MetaCachedStack repairMat) { + Predicate predicate = MATERIALS_TO_REPAIRABLE.get(repaired.getItem().getType()); + return predicate != null && predicate.test(repairMat.getItem().getType()); + } + + private static final Map> MATERIALS_TO_REPAIRABLE = new HashMap<>(); + + static { + loadTags(); + loadLists(); + } + + private static void loadTags() { + Map> tags = new HashMap<>(); + for (var entry : BakedRepairableData.getTags().entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + + Material mat = Registry.MATERIAL.get(entry.getKey()); + if (mat == null) { + continue; + } + + Predicate predicate = tags.computeIfAbsent( + entry.getValue(), + tagKey -> { + // Prefer Material tags as they're officially supported. + Tag matTag = Bukkit.getTag(Tag.REGISTRY_ITEMS, entry.getValue(), Material.class); + if (matTag != null) { + return matTag::isTagged; + } + + // Fall through to ItemType tags. + Tag typeTag = Bukkit.getTag(Tag.REGISTRY_ITEMS, entry.getValue(), ItemType.class); + if (typeTag == null) { + return null; + } + return localMat -> { + ItemType localType = localMat.asItemType(); + return localType != null && typeTag.isTagged(localType); + }; + + } + ); + + if (predicate != null) { + MATERIALS_TO_REPAIRABLE.put(mat, predicate); + } + } + } + + private static void loadLists() { + for (var entry : BakedRepairableData.getLists().entrySet()) { + if (entry.getKey() == null) { + continue; + } + + Material type = Registry.MATERIAL.get(entry.getKey()); + if (type == null || !type.isItem()) { + continue; + } + + Set values = new HashSet<>(); + for (NamespacedKey key : entry.getValue()) { + if (key == null) { + continue; + } + Material value = Registry.MATERIAL.get(key); + if (value != null) { + values.add(value); + } + } + + if (!values.isEmpty()) { + MATERIALS_TO_REPAIRABLE.put(type, values::contains); + } + } + } + +} diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaViewState.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaViewState.java new file mode 100644 index 0000000..dfb3eec --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/anvil/MetaViewState.java @@ -0,0 +1,39 @@ +package com.github.jikoo.planarenchanting.anvil; + +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class MetaViewState implements ViewState { + + private final AnvilView view; + private final MetaCachedStack base; + private final MetaCachedStack addition; + + MetaViewState(AnvilView view) { + this.view = view; + this.base = new MetaCachedStack(view.getItem(0)); + this.addition = new MetaCachedStack(view.getItem(1)); + } + + @Override + public AnvilView getAnvilView() { + return view; + } + + @Override + public MetaCachedStack getBase() { + return base; + } + + @Override + public MetaCachedStack getAddition() { + return addition; + } + + @Override + public MetaCachedStack createResult() { + return new MetaCachedStack(base.getItem().clone()); + } + +} diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/table/MetaEnchantabilities.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/table/MetaEnchantabilities.java new file mode 100644 index 0000000..1c32262 --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/table/MetaEnchantabilities.java @@ -0,0 +1,51 @@ +package com.github.jikoo.planarenchanting.table; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class MetaEnchantabilities implements EnchantabilityProvider { + + private final Map byKey; + + MetaEnchantabilities() { + byKey = new HashMap<>(); + for (Entry> entry : BakedEnchantableData.get().entrySet()) { + Enchantability enchantability = new Enchantability(entry.getKey()); + for (NamespacedKey key : entry.getValue()) { + if (key == null) { + continue; + } + Material material = Registry.MATERIAL.get(key); + if (material != null && material.isItem()) { + byKey.put(material.getKey(), enchantability); + } + } + } + } + + @Override + public @Nullable Enchantability of(Material material) { + return byKey.get(material.getKey()); + } + + @Override + public @Nullable Enchantability of(ItemType itemType) { + return byKey.get(itemType.getKey()); + } + + @Override + public @Nullable Enchantability of(ItemStack item) { + return byKey.get(item.getType().getKey()); + } + +} diff --git a/enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/CachedValue.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/util/CachedValue.java similarity index 100% rename from enchanting-core/src/main/java/com/github/jikoo/planarenchanting/util/CachedValue.java rename to enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/util/CachedValue.java diff --git a/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/util/MetaEnchantProvider.java b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/util/MetaEnchantProvider.java new file mode 100644 index 0000000..2d273aa --- /dev/null +++ b/enchanting-meta/src/main/java/com/github/jikoo/planarenchanting/util/MetaEnchantProvider.java @@ -0,0 +1,52 @@ +package com.github.jikoo.planarenchanting.util; + +import com.github.jikoo.planarenchanting.util.EnchantData.Provider; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.enchantments.Enchantment; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class MetaEnchantProvider implements Provider { + + private final Map data = new HashMap<>(); + + MetaEnchantProvider() { + load(); + } + + private void load() { + Registry registry = Objects.requireNonNull(Bukkit.getRegistry(Enchantment.class)); + for (var entry : BakedEnchantData.get().entrySet()) { + if (entry.getKey() == null) { + continue; + } + + Enchantment enchant = registry.get(entry.getKey()); + + if (enchant != null) { + data.put(enchant.getKey(), entry.getValue().apply(enchant)); + } + } + } + + @Override + public EnchantData of(Enchantment enchantment) { + return data.computeIfAbsent( + enchantment.getKey(), + // Since Spigot lacks APIs for most of these areas, model defaults off of Unbreaking. + // It's relatively middle-of-the-road across the board. + key -> BakedEnchantData.create( + 5, + 2, + lvl -> 5 + 8 * (lvl - 1), + lvl -> 55 + 8 * (lvl - 1) + ).apply(enchantment) + ); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaAnvilFunctionsTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaAnvilFunctionsTest.java new file mode 100644 index 0000000..14b1bb8 --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaAnvilFunctionsTest.java @@ -0,0 +1,869 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Stream; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.bukkit.inventory.view.AnvilView; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@DisplayName("Default basic AnvilFunctions") +@TestInstance(Lifecycle.PER_CLASS) +class MetaAnvilFunctionsTest { + + @Nested + class PriorWorkLevelCost { + + private final AnvilFunction function = MetaAnvilFunctions.PRIOR_WORK_LEVEL_COST; + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + assertThat( + "Prior work level cost always applies", + function.canApply(behavior, state, resultStack), + is(true) + ); + } + + @ParameterizedTest + @MethodSource("getPriorWork") + void getResult(int baseWork, int addedWork) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + Repairable meta = mock(); + doReturn(baseWork).when(meta).getRepairCost(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + meta = mock(); + doReturn(addedWork).when(meta).getRepairCost(); + metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getAddition(); + + var result = function.getResult(behavior, state, resultStack); + assertThat( + "Cost must be total prior work", + result.getLevelCostIncrease(), + is(baseWork + addedWork) + ); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + resultStack = mock(); + result.modifyResult(resultStack); + verifyNoInteractions(resultStack); + } + + @Test + void getResult() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + ItemMeta meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + meta = mock(); + metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getAddition(); + + var result = function.getResult(behavior, state, resultStack); + assertThat( + "Prior work must fall through to 0", + result.getLevelCostIncrease(), + is(0) + ); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + resultStack = mock(); + result.modifyResult(resultStack); + verifyNoInteractions(resultStack); + } + + private static @NonNull Collection getPriorWork() { + Collection arguments = new ArrayList<>(); + int [] values = { 0, 1, 3, 7, 15, 31 }; + + for (int base : values) { + for (int added : values) { + arguments.add(Arguments.of(base, added)); + } + } + + return arguments; + } + + } + + @Nested + class Rename { + + private final AnvilFunction function = MetaAnvilFunctions.RENAME; + + @DisplayName("Rename requires ItemMeta") + @Test + void canApplyRequiresMeta() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + MetaCachedStack metaStack = mock(); + doReturn(metaStack).when(state).getBase(); + + assertThat( + "Rename requires meta", + function.canApply(behavior, state, resultStack), + is(false) + ); + } + + @DisplayName("Rename requires different name") + @ParameterizedTest + @MethodSource("renameSituations") + void canApply(String anvilName, String baseName, boolean canApply) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + ItemMeta meta = mock(); + doReturn(baseName != null).when(meta).hasDisplayName(); + doReturn(baseName).when(meta).getDisplayName(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + AnvilView view = mock(); + doReturn(anvilName).when(view).getRenameText(); + doReturn(view).when(state).getAnvilView(); + + assertThat("Rename requires different name", function.canApply(behavior, state, resultStack), is(canApply)); + } + + @Test + void getResultNoMeta() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + AnvilView view = mock(); + doReturn("sample text").when(view).getRenameText(); + doReturn(view).when(state).getAnvilView(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Level cost increase is 1", result.getLevelCostIncrease(), is(1)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + MetaCachedStack stack = mock(); + result.modifyResult(stack); + verify(stack).getMeta(); + verifyNoMoreInteractions(stack); + } + + @Test + void getResultNotRepairable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + AnvilView view = mock(); + doReturn("sample text").when(view).getRenameText(); + doReturn(view).when(state).getAnvilView(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Level cost increase is 1", result.getLevelCostIncrease(), is(1)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + ItemMeta meta = mock(); + MetaCachedStack stack = mock(); + doReturn(meta).when(stack).getMeta(); + + result.modifyResult(stack); + + verify(stack).getMeta(); + verifyNoMoreInteractions(stack); + + verify(meta).setDisplayName(any()); + verifyNoMoreInteractions(meta); + } + + @Test + void getResult() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + AnvilView view = mock(); + doReturn("sample text").when(view).getRenameText(); + doReturn(view).when(state).getAnvilView(); + MetaCachedStack metaStack = mock(); + doReturn(metaStack).when(state).getBase(); + doReturn(metaStack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Level cost increase is 1", result.getLevelCostIncrease(), is(1)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + Repairable meta = mock(); + metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + result.modifyResult(metaStack); + + verify(metaStack).getMeta(); + verifyNoMoreInteractions(metaStack); + + verify(meta).setDisplayName(any()); + verify(meta).setRepairCost(anyInt()); + verifyNoMoreInteractions(meta); + } + + private static Stream renameSituations() { + String name1 = "Sample text"; + String name2 = "Example text"; + return Stream.of( + // NON-APPLICABLE + // Both unnamed + Arguments.of(null, null, false), + // Both identically named + Arguments.of(name1, name1, false), + + // APPLICABLE + // Only anvil named + Arguments.of(name1, null, true), + // Only item named + Arguments.of(null, name1, true), + // Both named differently + Arguments.of(name1, name2, true) + ); + } + + } + + @Nested + class UpdatePriorWorkCost { + + private final AnvilFunction function = MetaAnvilFunctions.UPDATE_PRIOR_WORK_COST; + + @Test + void canApplyRequiresRepairable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + MetaCachedStack metaStack = mock(); + doReturn(metaStack).when(state).getBase(); + + assertThat( + "Prior work update requires repairable", + function.canApply(behavior, state, resultStack), + is(false) + ); + } + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + Repairable meta = mock(); + MetaCachedStack stack = mock(); + doReturn(meta).when(stack).getMeta(); + doReturn(stack).when(state).getBase(); + + assertThat( + "Prior work update applies", + function.canApply(behavior, state, resultStack), + is(true) + ); + } + + @Test + void getResultNotRepairable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Result is not empty", result, is(not(AnvilFunctionResult.empty()))); + assertThat("Level cost must be unchanged", result.getLevelCostIncrease(), is(0)); + assertThat("Material cost must be unchanged", result.getMaterialCostIncrease(), is(0)); + + MetaCachedStack stack = mock(); + result.modifyResult(stack); + verify(stack).getMeta(); + verifyNoMoreInteractions(stack); + } + + @ParameterizedTest + @MethodSource("getPriorWork") + void getResult(int baseCost, int additionCost) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + Repairable meta = mock(); + doReturn(baseCost).when(meta).getRepairCost(); + MetaCachedStack stack = mock(); + doReturn(meta).when(stack).getMeta(); + doReturn(stack).when(state).getBase(); + meta = mock(); + doReturn(additionCost).when(meta).getRepairCost(); + stack = mock(); + doReturn(meta).when(stack).getMeta(); + doReturn(stack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + + assertThat("Result is not empty", result, is(not(AnvilFunctionResult.empty()))); + assertThat("Level cost must be unchanged", result.getLevelCostIncrease(), is(0)); + assertThat("Material cost must be unchanged", result.getMaterialCostIncrease(), is(0)); + + meta = mock(); + stack = mock(); + doReturn(meta).when(stack).getMeta(); + result.modifyResult(stack); + verify(stack).getMeta(); + verify(meta).setRepairCost(anyInt()); + } + + private static @NonNull Collection getPriorWork() { + Collection arguments = new ArrayList<>(); + int [] values = { 0, 1, 3, 7, 15, 31 }; + + for (int base : values) { + for (int added : values) { + arguments.add(Arguments.of(base, added)); + } + } + + return arguments; + } + + } + + @Nested + class RepairWithMaterial { + + private final AnvilFunction function = MetaAnvilFunctions.REPAIR_WITH_MATERIAL; + + @Test + void canApplyNotRepairedBy() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + assertThat("Must be repairable by item", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotDurable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + Material material = mock(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getBase(); + + assertThat("Must have durability", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotDamageable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + Material material = mock(); + doReturn((short) 100).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getBase(); + + assertThat("Must be damageable", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotDamaged() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + Material material = mock(); + doReturn((short) 100).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + Damageable meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + assertThat("Must have damage", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + Material material = mock(); + doReturn((short) 4).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + Damageable meta = mock(); + doReturn(4).when(meta).getDamage(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + assertThat("Base can be repaired", function.canApply(behavior, state, resultStack), is(true)); + } + + @Test + void getResultNotDamageable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + MetaCachedStack metaStack = mock(); + doReturn(metaStack).when(state).getBase(); + + assertThat( + "Undamageable base yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResultNotDamaged() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + Damageable meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + assertThat( + "Undamaged base yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @ParameterizedTest + @CsvSource({"1,1,3", "64,4,0"}) + void getResult( + int additionAmount, + int expectedRepairs, + int expectedDamage + ) { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + Material material = mock(); + doReturn((short) 4).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + Damageable meta = mock(); + doReturn(4).when(meta).getDamage(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + stack = mock(); + doReturn(additionAmount).when(stack).getAmount(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + assertThat("Cost is repair count", result.getLevelCostIncrease(), is(expectedRepairs)); + assertThat("Cost is repair count", result.getMaterialCostIncrease(), is(expectedRepairs)); + + final MetaCachedStack notDamageable = mock(); + assertDoesNotThrow(() -> result.modifyResult(notDamageable)); + + meta = mock(); + metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + result.modifyResult(metaStack); + verify(meta).setDamage(expectedDamage); + } + + } + + @Nested + class RepairWithCombination { + + private final AnvilFunction function = MetaAnvilFunctions.REPAIR_WITH_COMBINATION; + + @Test + void canApplyNotSameType() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + // Base + Material material = mock(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat("Non-like items cannot combine", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotDurable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + // Base + Material material = mock(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + doReturn(material).when(stack).getType(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat("Item without durability cannot combine", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotDamageable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + // Base + Material material = mock(); + doReturn((short) 4).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + doReturn(material).when(stack).getType(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat("Item must be Damageable", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApplyNotDamaged() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + // Base + Material material = mock(); + doReturn((short) 4).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + Damageable meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + doReturn(material).when(stack).getType(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat("Item must be damaged", function.canApply(behavior, state, resultStack), is(false)); + } + + @Test + void canApply() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + doReturn(true).when(behavior).itemRepairedBy(any(), any()); + + // Base + Material material = mock(); + doReturn((short) 4).when(material).getMaxDurability(); + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + Damageable meta = mock(); + doReturn(4).when(meta).getDamage(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + doReturn(material).when(stack).getType(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat("Items can be combined", function.canApply(behavior, state, resultStack), is(true)); + } + + + @Test + void getResultBaseNotRepairable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + // Base + ItemStack stack = mock(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat( + "Undamaged base yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResultAdditionNotRepairable() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + // Base + ItemStack stack = mock(); + Damageable meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(metaStack).when(state).getAddition(); + + assertThat( + "Undamaged base yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResultNoDamage() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + // Base + ItemStack stack = mock(); + Damageable meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + meta = mock(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getAddition(); + + assertThat( + "Undamaged base yields empty result", + function.getResult(behavior, state, resultStack), + is(AnvilFunctionResult.empty()) + ); + } + + @Test + void getResult() { + AnvilBehavior behavior = mock(); + ViewState state = mock(); + MetaCachedStack resultStack = mock(); + + Material material = mock(); + doReturn((short) 100).when(material).getMaxDurability(); + + // Base + ItemStack stack = mock(); + doReturn(material).when(stack).getType(); + Damageable meta = mock(); + doReturn(100).when(meta).getDamage(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getBase(); + + // Addition + stack = mock(); + doReturn(material).when(stack).getType(); + meta = mock(); + doReturn(100).when(meta).getDamage(); + metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + doReturn(meta).when(metaStack).getMeta(); + doReturn(metaStack).when(state).getAddition(); + + AnvilFunctionResult result = function.getResult(behavior, state, resultStack); + assertThat("Combine repair costs 2", result.getLevelCostIncrease(), is(2)); + assertThat("Material cost is unchanged", result.getMaterialCostIncrease(), is(0)); + + final MetaCachedStack notDamageable = mock(); + assertDoesNotThrow(() -> result.modifyResult(notDamageable)); + + meta = mock(); + metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + result.modifyResult(metaStack); + verify(meta).setDamage(88); + } + + } + + @Test + void gettersFetchConstants() { + // A bit of a silly test, but might prevent accidents. + MetaAnvilFunctions functions = MetaAnvilFunctions.INSTANCE; + assertThat( + "Provided function is constant", + functions.addPriorWorkLevelCost(), + is(sameInstance(MetaAnvilFunctions.PRIOR_WORK_LEVEL_COST)) + ); + assertThat( + "Provided function is constant", + functions.rename(), + is(sameInstance(MetaAnvilFunctions.RENAME)) + ); + assertThat( + "Provided function is constant", + functions.setItemPriorWork(), + is(sameInstance(MetaAnvilFunctions.UPDATE_PRIOR_WORK_COST)) + ); + assertThat( + "Provided function is constant", + functions.repairWithMaterial(), + is(sameInstance(MetaAnvilFunctions.REPAIR_WITH_MATERIAL)) + ); + assertThat( + "Provided function is constant", + functions.repairWithCombine(), + is(sameInstance(MetaAnvilFunctions.REPAIR_WITH_COMBINATION)) + ); + assertThat( + "Provided function is constant", + functions.combineEnchantsJava(), + is(sameInstance(MetaAnvilFunctions.COMBINE_ENCHANTMENTS_JAVA)) + ); + assertThat( + "Provided function is constant", + functions.combineEnchantsBedrock(), + is(sameInstance(MetaAnvilFunctions.COMBINE_ENCHANTMENTS_BEDROCK)) + ); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaCachedStackTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaCachedStackTest.java new file mode 100644 index 0000000..ef0fa77 --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaCachedStackTest.java @@ -0,0 +1,32 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.Test; + +class MetaCachedStackTest { + + @Test + void nullIsAir() { + MetaCachedStack stack = new MetaCachedStack(null); + assertThat("Null item is air", stack.getItem().getType(), is(Material.AIR)); + } + + @Test + void metaIsCached() { + ItemStack itemStack = mock(); + MetaCachedStack stack = new MetaCachedStack(itemStack); + + stack.getMeta(); + verify(itemStack).getItemMeta(); + stack.getMeta(); + verifyNoMoreInteractions(itemStack); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaEnchantmentAccessTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaEnchantmentAccessTest.java new file mode 100644 index 0000000..2b7dc6e --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaEnchantmentAccessTest.java @@ -0,0 +1,131 @@ +package com.github.jikoo.planarenchanting.anvil; + +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.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Map; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MetaEnchantmentAccessTest { + + private MockedStatic bukkit; + private MetaEnchantmentAccess access; + + @BeforeAll + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); + + access = new MetaEnchantmentAccess(); + } + + @AfterAll + void tearDown() { + bukkit.close(); + } + + @Test + void isBook() { + ItemStack stack = mock(); + doReturn(Material.ENCHANTED_BOOK).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + + assertThat("Item is book", access.isBook(metaStack)); + } + + @Test + void isNotBook() { + ItemStack stack = mock(); + doReturn(Material.DIRT).when(stack).getType(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + + assertThat("Item is not a book", access.isBook(metaStack), is(false)); + } + + @Test + void getEnchantments() { + ItemMeta meta = mock(); + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + doReturn(enchants).when(meta).getEnchants(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + assertThat("Item enchantments are fetched", access.getEnchantments(metaStack), is(enchants)); + } + + @Test + void getEnchantmentsStored() { + EnchantmentStorageMeta meta = mock(); + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + doReturn(enchants).when(meta).getStoredEnchants(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + assertThat("Stored enchantments are fetched", access.getEnchantments(metaStack), is(enchants)); + } + + @Test + void addEnchantments() { + ItemMeta meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + + access.addEnchantments(metaStack, enchants); + + verify(meta, times(enchants.size())).addEnchant(any(), anyInt(), eq(true)); + } + + @Test + void addEnchantmentsNullMeta() { + MetaCachedStack metaStack = mock(); + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + + assertDoesNotThrow(() -> access.addEnchantments(metaStack, enchants)); + } + + @Test + void addEnchantmentsStored() { + EnchantmentStorageMeta meta = mock(); + MetaCachedStack metaStack = mock(); + doReturn(meta).when(metaStack).getMeta(); + + Map enchants = Map.of(Enchantment.EFFICIENCY, 5); + + access.addEnchantments(metaStack, enchants); + + verify(meta, times(enchants.size())).addStoredEnchant(any(), anyInt(), eq(true)); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaTempererTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaTempererTest.java new file mode 100644 index 0000000..8e6f68b --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaTempererTest.java @@ -0,0 +1,218 @@ +package com.github.jikoo.planarenchanting.anvil; + +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.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.Repairable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MetaTempererTest { + + private MockedStatic bukkit; + + @BeforeAll + void setUp() { + bukkit = mockStatic(); + + bukkit.when(Bukkit::getItemFactory).thenAnswer(invocation -> { + ItemFactory factory = mock(); + doReturn(false).when(factory).equals(any(), any()); + return factory; + }); + } + + @AfterAll + void tearDownAll() { + bukkit.close(); + } + + @Test + void hasChangedBaseNullMeta() { + MetaCachedStack base = mock(); + + Repairable resultMeta = mock(); + MetaCachedStack result = mock(); + doReturn(resultMeta).when(result).getMeta(); + + MetaCachedStack addition = mock(); + + boolean hasChanged = assertDoesNotThrow( + () -> MetaTemperer.INSTANCE.hasChanged(base, addition, result) + ); + assertThat(hasChanged, is(false)); + } + + @Test + void hasChangedResultNullMeta() { + Repairable baseMeta = mock(); + MetaCachedStack base = mock(); + doReturn(baseMeta).when(base).getMeta(); + + MetaCachedStack result = mock(); + + MetaCachedStack addition = mock(); + + boolean hasChanged = assertDoesNotThrow( + () -> MetaTemperer.INSTANCE.hasChanged(base, addition, result) + ); + assertThat(hasChanged, is(false)); + } + + @Test + void hasChangedBaseResultNullMeta() { + MetaCachedStack base = mock(); + MetaCachedStack result = mock(); + MetaCachedStack addition = mock(); + + boolean hasChanged = assertDoesNotThrow( + () -> MetaTemperer.INSTANCE.hasChanged(base, addition, result) + ); + assertThat(hasChanged, is(false)); + } + + @Test + void hasChangedBaseNotRepairable() { + ItemMeta baseMeta = mock(); + MetaCachedStack base = mock(); + doReturn(baseMeta).when(base).getMeta(); + + Repairable resultMeta = mock(); + doReturn(resultMeta).when(resultMeta).clone(); + MetaCachedStack result = mock(); + doReturn(resultMeta).when(result).getMeta(); + + ItemStack additionStack = mock(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + boolean hasChanged = assertDoesNotThrow( + () -> MetaTemperer.INSTANCE.hasChanged(base, addition, result) + ); + assertThat(hasChanged, is(true)); + verify(resultMeta, never()).setRepairCost(anyInt()); + } + + @Test + void hasChangedResultNotRepairable() { + Repairable baseMeta = mock(); + MetaCachedStack base = mock(); + doReturn(baseMeta).when(base).getMeta(); + + ItemMeta resultMeta = mock(); + doReturn(resultMeta).when(resultMeta).clone(); + MetaCachedStack result = mock(); + doReturn(resultMeta).when(result).getMeta(); + + ItemStack additionStack = mock(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + boolean hasChanged = assertDoesNotThrow( + () -> MetaTemperer.INSTANCE.hasChanged(base, addition, result) + ); + assertThat(hasChanged, is(true)); + } + + @Test + void hasChangedBaseResultNotRepairable() { + ItemMeta baseMeta = mock(); + MetaCachedStack base = mock(); + doReturn(baseMeta).when(base).getMeta(); + + ItemMeta resultMeta = mock(); + doReturn(resultMeta).when(resultMeta).clone(); + MetaCachedStack result = mock(); + doReturn(resultMeta).when(result).getMeta(); + + ItemStack additionStack = mock(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + boolean hasChanged = assertDoesNotThrow( + () -> MetaTemperer.INSTANCE.hasChanged(base, addition, result) + ); + assertThat(hasChanged, is(true)); + } + + @Test + void hasChangedAirAddition() { + Repairable baseMeta = mock(); + MetaCachedStack base = mock(); + doReturn(baseMeta).when(base).getMeta(); + + Repairable resultMeta = mock(); + doReturn(resultMeta).when(resultMeta).clone(); + MetaCachedStack result = mock(); + doReturn(resultMeta).when(result).getMeta(); + + ItemStack additionStack = mock(); + doReturn(Material.AIR).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + assertThat( + "Differing result meta means a change", + MetaTemperer.INSTANCE.hasChanged(base, addition, result), + is(true) + ); + + verify(resultMeta, never()).setDisplayName(any()); + } + + @Test + void hasChanged() { + Repairable baseMeta = mock(); + MetaCachedStack base = mock(); + doReturn(baseMeta).when(base).getMeta(); + + Repairable resultMeta = mock(); + doReturn(resultMeta).when(resultMeta).clone(); + MetaCachedStack result = mock(); + doReturn(resultMeta).when(result).getMeta(); + + ItemStack additionStack = mock(); + doReturn(Material.DIRT).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + assertThat( + "Differing result meta means a change", + MetaTemperer.INSTANCE.hasChanged(base, addition, result), + is(true) + ); + + verify(resultMeta).setRepairCost(anyInt()); + verify(resultMeta).setDisplayName(any()); + } + + @Test + void temper() { + ItemStack stack = mock(); + MetaCachedStack metaStack = mock(); + doReturn(stack).when(metaStack).getItem(); + + MetaTemperer.INSTANCE.temper(metaStack); + + verify(metaStack).getMeta(); + verify(stack).setItemMeta(any()); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaVanillaBehaviorTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaVanillaBehaviorTest.java new file mode 100644 index 0000000..88de08f --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/anvil/MetaVanillaBehaviorTest.java @@ -0,0 +1,184 @@ +package com.github.jikoo.planarenchanting.anvil; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentMatcher; +import org.mockito.MockedStatic; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MetaVanillaBehaviorTest { + + private MockedStatic bukkit; + private MetaVanillaBehavior behavior; + + @BeforeAll + void setUp() { + bukkit = mockStatic(); + // Set up for Material tag for all diamond stuff + ArgumentMatcher isDiamondTag = key -> + key != null && key.getKey().contains("diamond"); + bukkit.when(() -> Bukkit.getTag(eq("items"), argThat(isDiamondTag), eq(Material.class))) + .thenAnswer(invocation -> { + Tag tag = mock(); + doAnswer(invIsTagged -> { + Material argument = invIsTagged.getArgument(0); + return argument.getKey().getKey().contains("diamond"); + }).when(tag).isTagged(any()); + return tag; + }); + // Set up for fallthrough to ItemType tag for all gold stuff + ArgumentMatcher isGoldTag = key -> + key != null && key.getKey().contains("gold"); + bukkit.when(() -> Bukkit.getTag(eq("items"), argThat(isGoldTag), eq(ItemType.class))) + .thenAnswer(invocation -> { + Tag tag = mock(); + doAnswer(invIsTagged -> { + ItemType argument = invIsTagged.getArgument(0); + return argument.getKey().getKey().contains("gold"); + }).when(tag).isTagged(any()); + return tag; + }); + // All other tags will be nonexistent + // Set up other registries (Registry.MATERIAL is backed by the enum and doesn't need mocking) + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> mock(Registry.class)); + } + + @AfterAll + void tearDownAll() { + bukkit.close(); + } + + @BeforeEach + void setUpEach() { + behavior = new MetaVanillaBehavior(); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void enchantApplies(boolean applies) { + Enchantment enchantment = mock(); + doReturn(applies).when(enchantment).canEnchantItem(any()); + assertThat( + "Behavior applies if enchant applies", + behavior.enchantApplies(enchantment, mock()), + is(applies) + ); + verify(enchantment).canEnchantItem(any()); + verifyNoMoreInteractions(enchantment); + } + + @ParameterizedTest + @CsvSource({ + "DIAMOND,DIAMOND,true", + "DIAMOND_PICKAXE,DIAMOND,false", + "DIAMOND,ENCHANTED_BOOK,true", + "ENCHANTED_BOOK,ENCHANTED_BOOK,true", + "ENCHANTED_BOOK,DIAMOND,false" + }) + void itemsCombineEnchants(Material baseMat, Material additionMat, boolean result) { + ItemStack baseStack = mock(); + doReturn(baseMat).when(baseStack).getType(); + MetaCachedStack base = mock(); + doReturn(baseStack).when(base).getItem(); + ItemStack additionStack = mock(); + doReturn(additionMat).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + assertThat( + "Like items and enchanted book additions combine enchantments", + behavior.itemsCombineEnchants(base, addition), + is(result) + ); + } + + @Test + void itemRepairedBy() { + ItemStack baseStack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(baseStack).getType(); + MetaCachedStack base = mock(); + doReturn(baseStack).when(base).getItem(); + ItemStack additionStack = mock(); + doReturn(Material.DIAMOND).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + assertThat("Item is repairable", behavior.itemRepairedBy(base, addition), is(true)); + } + + @Test + void repairsNotRepairMat() { + ItemStack baseStack = mock(); + doReturn(Material.DIAMOND_PICKAXE).when(baseStack).getType(); + MetaCachedStack base = mock(); + doReturn(baseStack).when(base).getItem(); + ItemStack additionStack = mock(); + doReturn(Material.DIRT).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + assertThat("Item is not repairable", behavior.itemRepairedBy(base, addition), is(false)); + } + + @Test + void repairsItemTypeFallthrough() { + ItemStack baseStack = mock(); + doReturn(Material.GOLDEN_PICKAXE).when(baseStack).getType(); + MetaCachedStack base = mock(); + doReturn(baseStack).when(base).getItem(); + + NamespacedKey key = mock(); + doReturn("gold").when(key).getKey(); + ItemType type = mock(); + doReturn(key).when(type).getKey(); + Material material = mock(); + doReturn(type).when(material).asItemType(); + ItemStack additionStack = mock(); + doReturn(material).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(additionStack).when(addition).getItem(); + + assertThat("Item is repairable", behavior.itemRepairedBy(base, addition), is(true)); + } + + @Test + void repairsNotRepairable() { + ItemStack baseStack = mock(); + doReturn(Material.WOODEN_PICKAXE).when(baseStack).getType(); + MetaCachedStack base = mock(); + doReturn(baseStack).when(base).getItem(); + ItemStack additionStack = mock(); + doReturn(Material.DIRT).when(additionStack).getType(); + MetaCachedStack addition = mock(); + doReturn(baseStack).when(addition).getItem(); + + assertThat("Item is not repairable", behavior.itemRepairedBy(base, addition), is(false)); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/table/MetaEnchantabilitiesTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/table/MetaEnchantabilitiesTest.java new file mode 100644 index 0000000..ddedaf3 --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/table/MetaEnchantabilitiesTest.java @@ -0,0 +1,97 @@ +package com.github.jikoo.planarenchanting.table; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +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.mockStatic; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MetaEnchantabilitiesTest { + + private MockedStatic bukkit; + private EnchantabilityProvider provider; + + @BeforeAll + void setUp() throws ClassNotFoundException { + bukkit = mockStatic(); + // Set up registries. + // Note that Registry.MATERIAL is an enum-based faux registry and cannot + // be mocked effectively without tricks not available through Mockito alone + // because it is initialized in place rather than fetched from the server. + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> mock(Registry.class)); + // Touch Registry to initialize static fields. + Class.forName("org.bukkit.Registry"); + + // Mock values for Registry.ITEM to allow Material#isItem on constants. + doAnswer(invocation -> { + NamespacedKey key = invocation.getArgument(0); + if (key.getKey().equals("AIR")) { + return null; + } + return mock(ItemType.class); + }).when(Registry.ITEM).get(any()); + + provider = new MetaEnchantabilities(); + } + + @AfterAll + void tearDown() { + bukkit.close(); + } + + @ParameterizedTest + @CsvSource({ "DIAMOND_PICKAXE,true", "AIR,false" }) + void ofMaterial(Material material, boolean isEnchantable) { + assertThat( + "Enchantability matches expectation", + provider.of(material), + is(isEnchantable ? not(nullValue()) : nullValue()) + ); + } + + @ParameterizedTest + @CsvSource({ "diamond_pickaxe,true", "air,false" }) + void ofItemType(String key, boolean isEnchantable) { + NamespacedKey namespacedKey = NamespacedKey.minecraft(key); + ItemType itemType = mock(ItemType.class); + doReturn(namespacedKey).when(itemType).getKey(); + + assertThat( + "Enchantability matches expectation", + provider.of(itemType), + is(isEnchantable ? not(nullValue()) : nullValue()) + ); + } + + @ParameterizedTest + @CsvSource({ "DIAMOND_PICKAXE,true", "AIR,false" }) + void ofItemStack(Material material, boolean isEnchantable) { + ItemStack itemStack = mock(); + doReturn(material).when(itemStack).getType(); + + assertThat( + "Enchantability matches expectation", + provider.of(itemStack), + is(isEnchantable ? not(nullValue()) : nullValue()) + ); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/util/CachedValueTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/util/CachedValueTest.java new file mode 100644 index 0000000..d5bce75 --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/util/CachedValueTest.java @@ -0,0 +1,43 @@ +package com.github.jikoo.planarenchanting.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Supplier; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@DisplayName("Cache value from supplier") +@TestInstance(Lifecycle.PER_METHOD) +class CachedValueTest { + + @ParameterizedTest + @MethodSource("getBooleans") + void testCachedValue(Boolean value) { + Supplier supplier = mock(); + doReturn(value).when(supplier).get(); + var cache = new CachedValue<>(supplier); + + assertThat("Value must be supplied as expected", cache.get(), is(value)); + assertThat("Value must be supplied as expected", cache.get(), is(value)); + verify(supplier, only()).get(); + } + + private static Collection getBooleans() { + return Arrays.asList( + Boolean.TRUE, + Boolean.FALSE, + null + ); + } + +} diff --git a/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/util/MetaEnchantProviderTest.java b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/util/MetaEnchantProviderTest.java new file mode 100644 index 0000000..2848a25 --- /dev/null +++ b/enchanting-meta/src/test/java/com/github/jikoo/planarenchanting/util/MetaEnchantProviderTest.java @@ -0,0 +1,98 @@ +package com.github.jikoo.planarenchanting.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +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.mockStatic; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.enchantments.Enchantment; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MetaEnchantProviderTest { + + private MockedStatic bukkit; + private MetaEnchantProvider provider; + + @BeforeAll + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + Answer getEnchant = invGet -> { + NamespacedKey key = invGet.getArgument(0); + if (key.getKey().equals("unbreaking")) { + // oops, all breaking + return null; + } + Enchantment enchant = mock(); + doReturn(key).when(enchant).getKey(); + return enchant; + }; + doAnswer(getEnchant).when(registry).getOrThrow(any()); + doAnswer(getEnchant).when(registry).get(any()); + } + return registry; + }); + + provider = new MetaEnchantProvider(); + } + + @AfterAll + void tearDown() { + bukkit.close(); + } + + @Test + void of() { + EnchantData enchantData = provider.of(Enchantment.SILK_TOUCH); + + assertThat("Enchantment is supported", enchantData, is(notNullValue())); + assertThat("Weight is not default", enchantData.getWeight(), is(not(5))); + assertThat("Trident enchant uses enchant definition", enchantData.isTridentEnchant(), is(false)); + } + + @ParameterizedTest + @CsvSource({ "true,true", "true,false", "false,true", "false,false" }) + void ofUnknown(boolean isTrident, boolean isTool) { + Enchantment enchantment = mock(Enchantment.class); + doReturn(NamespacedKey.minecraft("trident_" + isTrident + "_" + isTool)).when(enchantment).getKey(); + + doAnswer(invocation -> { + if (isTool) return true; + ItemStack stack = invocation.getArgument(0); + Material mat = stack.getType(); + return isTrident && mat == Material.TRIDENT; + }).when(enchantment).canEnchantItem(any()); + + EnchantData data = provider.of(enchantment); + + assertThat("Enchantment is supported", data, is(notNullValue())); + assertThat("Weight is default", data.getWeight(), is(5)); + assertThat("Anvil cost is default", data.getAnvilCost(), is(2)); + assertThat("Min cost uses unbreaking formula", data.getMinModifiedCost(1), is(5)); + assertThat("Min cost uses unbreaking formula", data.getMinModifiedCost(2), is(13)); + assertThat("Max cost uses unbreaking formula", data.getMaxModifiedCost(1), is(55)); + assertThat("Max cost uses unbreaking formula", data.getMaxModifiedCost(2), is(63)); + assertThat("Trident enchant uses enchant definition", data.isTridentEnchant(), is(isTrident && !isTool)); + } + +} diff --git a/gradle.properties b/gradle.properties index c189ca1..2fcc510 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,6 @@ version=3.0.0-SNAPSHOT description=A customizable system mimicking vanilla Minecraft's enchanting functionality. # Gradle configuration -#org.gradle.configuration-cache=true +org.gradle.configuration-cache=true org.gradle.parallel=true -#org.gradle.caching=true +org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b55d175..7f1557a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,22 +4,30 @@ com-github-jikoo-planarwrappers = "4.0.0" com-jparams-to-string-verifier = "1.4.8" io-papermc-paper-paper-api = "1.21.11-R0.1-SNAPSHOT" +# Spigot version is pinned at the last version before Paper development diverged. +# Even using 1.21.4 is frustrating because Spigot added RegistryAware to Enchantment after the fork. +org-spigotmc-spigot-api = "1.21.3-R0.1-SNAPSHOT" org-hamcrest-hamcrest = "3.0" +org-jspecify-jspecify = "1.0.0" org-jetbrains-annotations = "26.0.2-1" org-mockito-mockito-core = "5.21.0" org-sonarqube = "7.1.0.6387" paperweight = "2.0.0-beta.19" com-palantir-javapoet-javapoet = "0.11.0" +shadow = "9.3.2" [libraries] com-github-jikoo-planarwrappers = { module = "com.github.jikoo:planarwrappers", version.ref = "com-github-jikoo-planarwrappers" } com-jparams-to-string-verifier = { module = "com.jparams:to-string-verifier", version.ref = "com-jparams-to-string-verifier" } io-papermc-paper-paper-api = { module = "io.papermc.paper:paper-api", version.ref = "io-papermc-paper-paper-api" } +org-spigotmc-spigot-api = { module = "org.spigotmc:spigot-api", version.ref = "org-spigotmc-spigot-api" } org-hamcrest-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "org-hamcrest-hamcrest" } +org-jspecify-jspecify = { module = "org.jspecify:jspecify", version.ref = "org-jspecify-jspecify" } org-jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "org-jetbrains-annotations" } org-mockito-mockito-core = { module = "org.mockito:mockito-core", version.ref = "org-mockito-mockito-core" } com-palantir-javapoet-javapoet = { module = "com.palantir.javapoet:javapoet", version.ref = "com-palantir-javapoet-javapoet" } [plugins] +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } io-papermc-paperweight = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" } org-sonarqube = { id = "org.sonarqube", version.ref = "org-sonarqube" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 10454e8..c6c7f38 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,9 +1,10 @@ rootProject.name = "planarenchanting-parent" -include(":planarenchanting") -project(":planarenchanting").projectDir = file("enchanting-core") +include("enchanting-generator") +include("enchanting-common", "enchanting-components", "enchanting-meta") -include(":planarenchanting-generator") -project(":planarenchanting-generator").projectDir = file("enchanting-generator") +include(":planarenchanting") +project(":planarenchanting").projectDir = file("enchanting-bundler") -startParameter.excludedTaskNames.add(":planarenchanting-generator:build") +// Don't build generator unless generating. +startParameter.excludedTaskNames.add(":enchanting-generator:build")