diff --git a/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java b/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java index 05fc5f062e7d..01874fd3921c 100644 --- a/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java +++ b/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java @@ -27,6 +27,7 @@ import org.bukkit.damage.DamageEffect; import org.bukkit.damage.DamageSource; import org.bukkit.damage.DamageType; +import org.bukkit.enchantments.EnchantmentOffer; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; @@ -127,4 +128,6 @@ default VersionFetcher getVersionFetcher() { ComponentFlattener componentFlattener(); PersistentDataKey createPersistentDataKey(Key key, PersistentDataType type); + + @Nullable EnchantmentOffer[] rollEnchantmentOffers(ItemStack targetStack, int seed, int bookshelfCount, int offerAmount); } diff --git a/paper-api/src/main/java/org/bukkit/enchantments/EnchantmentOffer.java b/paper-api/src/main/java/org/bukkit/enchantments/EnchantmentOffer.java index 8111acaa1af5..c49950babeee 100644 --- a/paper-api/src/main/java/org/bukkit/enchantments/EnchantmentOffer.java +++ b/paper-api/src/main/java/org/bukkit/enchantments/EnchantmentOffer.java @@ -1,18 +1,26 @@ package org.bukkit.enchantments; import com.google.common.base.Preconditions; -import org.jetbrains.annotations.NotNull; +import io.papermc.paper.InternalAPIBridge; +import org.bukkit.inventory.ItemStack; +import java.util.Objects; +import org.checkerframework.checker.index.qual.Positive; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import static io.papermc.paper.util.BoundChecker.requirePositive; /** * A class for the available enchantment offers in the enchantment table. */ +@NullMarked public class EnchantmentOffer { private Enchantment enchantment; private int enchantmentLevel; private int cost; - public EnchantmentOffer(@NotNull Enchantment enchantment, int enchantmentLevel, int cost) { + public EnchantmentOffer(final Enchantment enchantment, final int enchantmentLevel, final int cost) { this.enchantment = enchantment; this.enchantmentLevel = enchantmentLevel; this.cost = cost; @@ -23,9 +31,8 @@ public EnchantmentOffer(@NotNull Enchantment enchantment, int enchantmentLevel, * * @return type of enchantment */ - @NotNull public Enchantment getEnchantment() { - return enchantment; + return this.enchantment; } /** @@ -33,7 +40,7 @@ public Enchantment getEnchantment() { * * @param enchantment type of the enchantment */ - public void setEnchantment(@NotNull Enchantment enchantment) { + public void setEnchantment(final Enchantment enchantment) { Preconditions.checkArgument(enchantment != null, "The enchantment may not be null!"); this.enchantment = enchantment; @@ -45,7 +52,7 @@ public void setEnchantment(@NotNull Enchantment enchantment) { * @return level of the enchantment */ public int getEnchantmentLevel() { - return enchantmentLevel; + return this.enchantmentLevel; } /** @@ -53,8 +60,8 @@ public int getEnchantmentLevel() { * * @param enchantmentLevel level of the enchantment */ - public void setEnchantmentLevel(int enchantmentLevel) { - Preconditions.checkArgument(enchantmentLevel > 0, "The enchantment level must be greater than 0!"); + public void setEnchantmentLevel(final @Positive int enchantmentLevel) { + requirePositive(enchantmentLevel, "enchantmentLevel"); this.enchantmentLevel = enchantmentLevel; } @@ -66,7 +73,7 @@ public void setEnchantmentLevel(int enchantmentLevel) { * @return cost for this enchantment */ public int getCost() { - return cost; + return this.cost; } /** @@ -75,9 +82,42 @@ public int getCost() { * * @param cost cost for this enchantment */ - public void setCost(int cost) { - Preconditions.checkArgument(cost > 0, "The cost must be greater than 0!"); + public void setCost(final @Positive int cost) { + requirePositive(cost, "cost"); this.cost = cost; } + + @Override + public boolean equals(final @Nullable Object o) { + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final EnchantmentOffer that = (EnchantmentOffer) o; + return this.getEnchantmentLevel() == that.getEnchantmentLevel() && this.getCost() == that.getCost() && Objects.equals(this.getEnchantment(), that.getEnchantment()); + } + + @Override + public int hashCode() { + return Objects.hash(this.getEnchantment(), this.getEnchantmentLevel(), this.getCost()); + } + + /** + * Rolls a new set of enchantment offers based on a target item stack, a seed and a bookshelf count. + * + * @param targetStack the itemstack for which the enchantment offers are rolled. + * @param seed the seed used for random selection of enchantments. + * Mirrors {@link org.bukkit.inventory.view.EnchantmentView#getEnchantmentSeed()}. + * @param bookshelfCount the number of virtual bookshelves to consider when rolling the enchantments. + * @param offerAmount the amount of enchantment offers to roll. + * @return an array of enchantment offers with size {@code offerAmount}. + */ + public static @Nullable EnchantmentOffer[] rollEnchantmentOffers( + final ItemStack targetStack, + final int seed, + final int bookshelfCount, + final int offerAmount + ) { + return InternalAPIBridge.get().rollEnchantmentOffers(targetStack, seed, bookshelfCount, offerAmount); + } } diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch index d00722102f64..42d1ce146367 100644 --- a/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch @@ -64,10 +64,32 @@ this.access.execute((level, pos) -> { IdMap> holders = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).asHolderIdMap(); int bookcases = 0; -@@ -119,6 +_,42 @@ +@@ -97,27 +_,42 @@ } } +- this.random.setSeed(this.enchantmentSeed.get()); +- +- for (int ixx = 0; ixx < 3; ixx++) { +- this.costs[ixx] = EnchantmentHelper.getEnchantmentCost(this.random, ixx, bookcases, itemStack); +- this.enchantClue[ixx] = -1; +- this.levelClue[ixx] = -1; +- if (this.costs[ixx] < ixx + 1) { +- this.costs[ixx] = 0; +- } +- } +- +- for (int ix = 0; ix < 3; ix++) { +- if (this.costs[ix] > 0) { +- List list = this.getEnchantmentList(level.registryAccess(), itemStack, ix, this.costs[ix]); +- if (!list.isEmpty()) { +- EnchantmentInstance ench = list.get(this.random.nextInt(list.size())); +- this.enchantClue[ix] = holders.getId(ench.enchantment()); +- this.levelClue[ix] = ench.level(); +- } +- } +- } ++ fillEnchantmentOffers(this.random, this.enchantmentSeed.get(), bookcases, itemStack, level.registryAccess(), this.costs, this.enchantClue, this.levelClue); // Paper - extracted into net.minecraft.world.inventory.EnchantmentMenu.fillEnchantmentOffers + // CraftBukkit start + org.bukkit.inventory.ItemStack craftItemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitMirror(itemStack); + org.bukkit.enchantments.EnchantmentOffer[] offers = new org.bukkit.enchantments.EnchantmentOffer[3]; @@ -103,10 +125,46 @@ + } + } + // CraftBukkit end -+ + this.broadcastChanges(); }); - } else { +@@ -131,6 +_,36 @@ + } + } + ++ // Paper start - extract offer generation ++ // Extracted over modifications in place given nearly every line has diff to account for non-instance fields for ++ // cost, enchantment and level clues. ++ public static void fillEnchantmentOffers(final RandomSource random, final int seed, final int bookcases, final ItemStack itemStack, final RegistryAccess registryAccess, final int[] costs, final int[] enchantClue, final int[] levelClue) { ++ random.setSeed(seed); ++ ++ for (int ixx = 0; ixx < costs.length; ixx++) { ++ costs[ixx] = EnchantmentHelper.getEnchantmentCost(random, ixx, bookcases, itemStack); ++ enchantClue[ixx] = -1; ++ levelClue[ixx] = -1; ++ if (costs[ixx] < ixx + 1) { ++ costs[ixx] = 0; ++ } ++ } ++ ++ final IdMap> holders = registryAccess.lookupOrThrow(Registries.ENCHANTMENT).asHolderIdMap(); ++ ++ for (int ix = 0; ix < costs.length; ix++) { ++ if (costs[ix] > 0) { ++ List list = getEnchantmentList(registryAccess, itemStack, ix, costs[ix], random, seed); ++ if (!list.isEmpty()) { ++ EnchantmentInstance ench = list.get(random.nextInt(list.size())); ++ enchantClue[ix] = holders.getId(ench.enchantment()); ++ levelClue[ix] = ench.level(); ++ } ++ } ++ } ++ } ++ // Paper end - extract offer generation ++ + @Override + public boolean clickMenuButton(final Player player, final int buttonId) { + if (buttonId >= 0 && buttonId < this.costs.length) { @@ -148,19 +_,52 @@ } @@ -167,6 +225,30 @@ currency.consume(enchantmentCost, player); if (currency.isEmpty()) { this.enchantSlots.setItem(1, ItemStack.EMPTY); +@@ -185,15 +_,20 @@ + } + + private List getEnchantmentList(final RegistryAccess access, final ItemStack itemStack, final int slot, final int enchantmentCost) { +- this.random.setSeed(this.enchantmentSeed.get() + slot); ++ // Paper start - make static and public ++ return getEnchantmentList(access, itemStack, slot, enchantmentCost, this.random, this.enchantmentSeed.get()); ++ } ++ public static List getEnchantmentList(final RegistryAccess access, final ItemStack itemStack, final int slot, final int enchantmentCost, final RandomSource random, final int seed) { ++ random.setSeed(seed + slot); ++ // Paper end - make static and public + Optional> tag = access.lookupOrThrow(Registries.ENCHANTMENT).get(EnchantmentTags.IN_ENCHANTING_TABLE); + if (tag.isEmpty()) { + return List.of(); + } + +- List list = EnchantmentHelper.selectEnchantment(this.random, itemStack, enchantmentCost, tag.get().stream()); ++ List list = EnchantmentHelper.selectEnchantment(random, itemStack, enchantmentCost, tag.get().stream()); // Paper - make static and public + if (itemStack.is(Items.BOOK) && list.size() > 1) { +- list.remove(this.random.nextInt(list.size())); ++ list.remove(random.nextInt(list.size())); // Paper - make static and public + } + + return list; @@ -204,6 +_,12 @@ return goldStack.isEmpty() ? 0 : goldStack.getCount(); } diff --git a/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java b/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java index 24721e4b0128..1f0ad3f6855a 100644 --- a/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java +++ b/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java @@ -22,16 +22,21 @@ import io.papermc.paper.world.damagesource.FallLocationType; import io.papermc.paper.world.damagesource.PaperCombatEntryWrapper; import io.papermc.paper.world.damagesource.PaperCombatTrackerWrapper; +import java.util.Random; +import java.util.function.Predicate; import java.io.IOException; import java.util.Objects; import java.util.Set; import java.util.function.BooleanSupplier; import java.util.function.Function; -import java.util.function.Predicate; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.flattener.ComponentFlattener; import net.minecraft.Optionull; +import net.minecraft.core.Holder; +import net.minecraft.core.IdMap; +import net.minecraft.core.registries.Registries; +import net.minecraft.util.RandomSource; import net.minecraft.commands.Commands; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; @@ -42,11 +47,14 @@ import net.minecraft.world.entity.ai.attributes.DefaultAttributes; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.entity.decoration.Mannequin; +import net.minecraft.world.inventory.EnchantmentMenu; +import net.minecraft.world.item.enchantment.Enchantment; +import org.bukkit.block.Biome; +import org.bukkit.craftbukkit.CraftRegistry; import org.bukkit.GameRule; import org.bukkit.NamespacedKey; import org.bukkit.Statistic; import org.bukkit.attribute.Attributable; -import org.bukkit.block.Biome; import org.bukkit.command.CommandSender; import org.bukkit.craftbukkit.CraftGameRule; import org.bukkit.craftbukkit.CraftStatistic; @@ -54,24 +62,27 @@ import org.bukkit.craftbukkit.command.VanillaCommandWrapper; import org.bukkit.craftbukkit.damage.CraftDamageEffect; import org.bukkit.craftbukkit.damage.CraftDamageSource; +import org.bukkit.craftbukkit.enchantments.CraftEnchantment; +import org.bukkit.craftbukkit.entity.CraftLivingEntity; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.RandomSourceWrapper; +import org.bukkit.damage.DamageEffect; +import org.bukkit.damage.DamageSource; +import org.bukkit.enchantments.EnchantmentOffer; +import org.bukkit.entity.LivingEntity; +import org.bukkit.inventory.ItemStack; import org.bukkit.craftbukkit.damage.CraftDamageSourceBuilder; import org.bukkit.craftbukkit.entity.CraftEntity; import org.bukkit.craftbukkit.entity.CraftEntityType; -import org.bukkit.craftbukkit.entity.CraftLivingEntity; import org.bukkit.craftbukkit.entity.CraftMannequin; -import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.util.CraftMagicNumbers; import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.craftbukkit.util.CraftSpawnCategory; -import org.bukkit.damage.DamageEffect; -import org.bukkit.damage.DamageSource; import org.bukkit.damage.DamageType; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; -import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Pose; import org.bukkit.entity.SpawnCategory; -import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; @@ -269,4 +280,29 @@ public PersistentDataKey createPersistentDataKey(final Key key, final Per : new NamespacedKey(key.namespace(), key.value()); return new PaperPersistentDataKey<>(namespacedKey, type); } + + @Override + public @Nullable EnchantmentOffer[] rollEnchantmentOffers(final ItemStack targetStack, final int seed, final int bookshelfCount, final int offerAmount) { + final int[] costs = new int[offerAmount]; + final int[] enchantClue = new int[offerAmount]; + final int[] levelClue = new int[offerAmount]; + + final RandomSource randomSource = new RandomSourceWrapper(new Random()); + EnchantmentMenu.fillEnchantmentOffers(randomSource, seed, bookshelfCount, CraftItemStack.unwrap(targetStack), CraftRegistry.getMinecraftRegistry(), costs, enchantClue, levelClue); + + final IdMap> holderIdMap = CraftRegistry.getMinecraftRegistry() + .lookupOrThrow(Registries.ENCHANTMENT) + .asHolderIdMap(); + + final @Nullable EnchantmentOffer[] offers = new EnchantmentOffer[offerAmount]; + for (int i = 0; i < costs.length; i++) { + if (enchantClue[i] < 0) { + continue; + } + + offers[i] = new EnchantmentOffer(CraftEnchantment.minecraftHolderToBukkit(holderIdMap.byId(enchantClue[i])), levelClue[i], costs[i]); + } + + return offers; + } }