diff --git a/patches/api/0490-Improve-LootContext-API.patch b/patches/api/0490-Improve-LootContext-API.patch new file mode 100644 index 0000000000..1e0de768c8 --- /dev/null +++ b/patches/api/0490-Improve-LootContext-API.patch @@ -0,0 +1,381 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Tue, 22 Mar 2022 22:14:10 -0700 +Subject: [PATCH] Improve LootContext API + + +diff --git a/src/main/java/io/papermc/paper/loot/LootContextKey.java b/src/main/java/io/papermc/paper/loot/LootContextKey.java +new file mode 100644 +index 0000000000000000000000000000000000000000..67f8d027d68c0af5c26ee23d625da6917f6f1642 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/loot/LootContextKey.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.loot; ++ ++import io.papermc.paper.math.Position; ++import net.kyori.adventure.key.Keyed; ++import org.bukkit.block.TileState; ++import org.bukkit.block.data.BlockData; ++import org.bukkit.damage.DamageSource; ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.Player; ++import org.bukkit.inventory.ItemStack; ++ ++import static io.papermc.paper.loot.LootContextKeyImpl.create; ++ ++/** ++ * A key to a possible value in a {@link org.bukkit.loot.LootContext}. ++ * ++ * @param the value type ++ */ ++@SuppressWarnings("unused") ++public interface LootContextKey extends Keyed { ++ ++ LootContextKey THIS_ENTITY = create("this_entity"); ++ LootContextKey LAST_DAMAGE_PLAYER = create("last_damage_player"); ++ LootContextKey DAMAGE_SOURCE = create("damage_source"); ++ LootContextKey ATTACKING_ENTITY = create("attacking_entity"); ++ LootContextKey DIRECT_ATTACKING_ENTITY = create("direct_attacking_entity"); ++ LootContextKey ORIGIN = create("origin"); ++ LootContextKey BLOCK_DATA = create("block_state"); ++ LootContextKey TILE_STATE = create("block_entity"); ++ LootContextKey TOOL = create("tool"); ++ LootContextKey EXPLOSION_RADIUS = create("explosion_radius"); ++ LootContextKey ENCHANTMENT_LEVEL = create("enchantment_level"); ++ LootContextKey ENCHANTMENT_ACTIVE = create("enchantment_active"); ++} +diff --git a/src/main/java/io/papermc/paper/loot/LootContextKeyImpl.java b/src/main/java/io/papermc/paper/loot/LootContextKeyImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..941a490e1b8f865a6ec9fdfb57215f1674ce40ff +--- /dev/null ++++ b/src/main/java/io/papermc/paper/loot/LootContextKeyImpl.java +@@ -0,0 +1,24 @@ ++package io.papermc.paper.loot; ++ ++import java.util.HashSet; ++import java.util.Set; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.key.KeyPattern; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.ApiStatus; ++ ++@ApiStatus.Internal ++@DefaultQualifier(NonNull.class) ++record LootContextKeyImpl(Key key) implements LootContextKey { ++ ++ static final Set> KEYS = new HashSet<>(); ++ ++ static LootContextKey create(@KeyPattern final String name) { ++ final LootContextKeyImpl key = new LootContextKeyImpl<>(Key.key(name)); ++ if (!KEYS.add(key)) { ++ throw new IllegalStateException("Already registered " + name); ++ } ++ return key; ++ } ++} +diff --git a/src/main/java/org/bukkit/loot/LootContext.java b/src/main/java/org/bukkit/loot/LootContext.java +index 9c1ccaed727ec5e5dad93146bbfda798e3f536e7..43e7839157c37745a3623512b35b89fd6839ab3c 100644 +--- a/src/main/java/org/bukkit/loot/LootContext.java ++++ b/src/main/java/org/bukkit/loot/LootContext.java +@@ -15,30 +15,124 @@ public final class LootContext { + + public static final int DEFAULT_LOOT_MODIFIER = -1; + +- private final Location location; ++ // Paper start - loot context overhaul ++ private final org.bukkit.World world; + private final float luck; +- private final int lootingModifier; +- private final Entity lootedEntity; +- private final HumanEntity killer; +- +- private LootContext(@NotNull Location location, float luck, int lootingModifier, @Nullable Entity lootedEntity, @Nullable HumanEntity killer) { +- Preconditions.checkArgument(location != null, "LootContext location cannot be null"); +- Preconditions.checkArgument(location.getWorld() != null, "LootContext World cannot be null"); +- this.location = location; ++ private final java.util.Random random; ++ private final java.util.Map, Object> contextMap; ++ // TODO dynamic drops API ++ @Deprecated ++ private @org.checkerframework.checker.nullness.qual.MonotonicNonNull Location legacyLocation; ++ @Deprecated // has no functionality ++ private int lootingModifier; ++ private final boolean isLegacy; ++ ++ private LootContext(@NotNull org.bukkit.World world, float luck, @NotNull java.util.Random random, @NotNull java.util.Map, Object> contextMap, boolean isLegacy, @Deprecated int lootingModifier) { ++ this.world = world; + this.luck = luck; ++ this.random = random; ++ this.contextMap = java.util.Map.copyOf(contextMap); ++ this.isLegacy = isLegacy; + this.lootingModifier = lootingModifier; +- this.lootedEntity = lootedEntity; +- this.killer = killer; ++ } ++ ++ @org.jetbrains.annotations.ApiStatus.Internal ++ public boolean isLegacy() { ++ return this.isLegacy; ++ } ++ ++ /** ++ * Checks if this context contains a value for the key ++ * ++ * @param contextKey the key to check ++ * @return true if this context has a value for that key ++ */ ++ public boolean hasKey(final io.papermc.paper.loot.@NotNull LootContextKey contextKey) { ++ return this.contextMap.containsKey(contextKey); ++ } ++ ++ /** ++ * Gets the value for a context key ++ * ++ * @param contextKey the key for the value ++ * @return the value or null if this context doesn't have a value for the key ++ * @param value type ++ * @see #hasKey(io.papermc.paper.loot.LootContextKey) ++ * @see #getOrThrow(io.papermc.paper.loot.LootContextKey) ++ */ ++ @SuppressWarnings("unchecked") ++ public @Nullable T get(final io.papermc.paper.loot.@NotNull LootContextKey contextKey) { ++ return (T) this.contextMap.get(contextKey); + } + ++ /** ++ * Gets the value of a context key, throwing an exception ++ * if one isn't found ++ * ++ * @param contextKey the key for the value ++ * @return the value ++ * @param value type ++ * @throws java.util.NoSuchElementException if no value is found for that key ++ */ ++ public @NotNull T getOrThrow(final io.papermc.paper.loot.@NotNull LootContextKey contextKey) { ++ final T value = this.get(contextKey); ++ if (value == null) { ++ throw new java.util.NoSuchElementException("No value found for " + contextKey); ++ } ++ return value; ++ } ++ ++ /** ++ * Gets the random instance used for this context. ++ * ++ * @return the random ++ */ ++ public java.util.@NotNull Random getRandom() { ++ return this.random; ++ } ++ ++ /** ++ * Gets the world for this context. ++ * ++ * @return the world ++ */ ++ public org.bukkit.@NotNull World getWorld() { ++ return this.world; ++ } ++ ++ /** ++ * Gets the context map for this loot context. ++ * ++ * @return an unmodifiable map ++ */ ++ ++ public java.util.@NotNull @org.jetbrains.annotations.Unmodifiable Map, Object> getContextMap() { ++ return this.contextMap; ++ } ++ // Paper end ++ + /** + * The {@link Location} to store where the loot will be generated. + * + * @return the Location of where the loot will be generated ++ * @deprecated use {@link #get(io.papermc.paper.loot.LootContextKey)} methods + */ + @NotNull ++ @Deprecated // Paper + public Location getLocation() { +- return location; ++ // Paper start - fallback to legacy location ++ if (this.legacyLocation == null) { ++ if (contextMap.containsKey(io.papermc.paper.loot.LootContextKey.ORIGIN)) { ++ io.papermc.paper.math.Position pos = this.getOrThrow(io.papermc.paper.loot.LootContextKey.ORIGIN); ++ this.legacyLocation = new Location(this.world, pos.x(), pos.y(), pos.z()); ++ } else if (contextMap.containsKey(io.papermc.paper.loot.LootContextKey.THIS_ENTITY)) { ++ this.legacyLocation = this.getOrThrow(io.papermc.paper.loot.LootContextKey.THIS_ENTITY).getLocation(); ++ } else { ++ throw new IllegalStateException("All known context key sets require \"origin\" or \"this_entity\" and this one doesn't have either"); ++ } ++ } ++ return this.legacyLocation; ++ // Paper end + } + + /** +@@ -73,10 +167,12 @@ public final class LootContext { + * Get the {@link Entity} that was killed. Can be null. + * + * @return the looted entity or null ++ * @deprecated use {@link #get(io.papermc.paper.loot.LootContextKey)} methods + */ + @Nullable ++ @Deprecated // Paper + public Entity getLootedEntity() { +- return lootedEntity; ++ return this.get(io.papermc.paper.loot.LootContextKey.THIS_ENTITY); // Paper + } + + /** +@@ -84,41 +180,89 @@ public final class LootContext { + * Can be null. + * + * @return the killer entity, or null. ++ * @deprecated use {@link #get(io.papermc.paper.loot.LootContextKey)} methods + */ + @Nullable ++ @Deprecated // Paper + public HumanEntity getKiller() { +- return killer; ++ return this.get(io.papermc.paper.loot.LootContextKey.ATTACKING_ENTITY) instanceof HumanEntity humanEntity ? humanEntity : null; // Paper + } + + /** + * Utility class to make building {@link LootContext} easier. The only +- * required argument is {@link Location} with a valid (non-null) +- * {@link org.bukkit.World}. ++ * required argument is {@link org.bukkit.World}. + */ + public static class Builder { + +- private final Location location; ++ private final org.bukkit.World world; // Paper + private float luck; ++ @Deprecated // Paper - not functional + private int lootingModifier = LootContext.DEFAULT_LOOT_MODIFIER; +- private Entity lootedEntity; +- private HumanEntity killer; ++ private java.util.Random random = java.util.concurrent.ThreadLocalRandom.current(); // Paper ++ private final java.util.Map, Object> contextMap = new java.util.IdentityHashMap<>(); // Paper ++ private boolean isLegacy = false; // Paper + + /** + * Creates a new LootContext.Builder instance to facilitate easy + * creation of {@link LootContext}s. + * + * @param location the location the LootContext should use ++ * @deprecated not all loot contexts have locations + */ ++ @Deprecated // Paper + public Builder(@NotNull Location location) { +- this.location = location; ++ // Paper start ++ com.google.common.base.Preconditions.checkArgument(location.getWorld() != null, "location missing world"); ++ this.world = location.getWorld(); ++ this.contextMap.put(io.papermc.paper.loot.LootContextKey.ORIGIN, io.papermc.paper.math.Position.fine(location)); ++ this.isLegacy = true; ++ // Paper end ++ } ++ ++ // Paper start ++ public Builder(@NotNull org.bukkit.World world) { ++ this.world = world; ++ } ++ ++ /** ++ * Sets the random instance to use for this context. ++ * Defaults to {@link java.util.concurrent.ThreadLocalRandom#current()}. ++ * ++ * @param random the random to use ++ * @return the builder ++ */ ++ @org.jetbrains.annotations.Contract(value = "_ -> this", mutates = "this") ++ public @NotNull Builder withRandom(@NotNull java.util.Random random) { ++ this.random = random; ++ return this; + } + ++ /** ++ * Sets or clears context values. ++ * ++ * @param contextKey the key to set or clear for ++ * @param context the value to set, or null to clear ++ * @param the value type ++ * @return the builder ++ */ ++ @org.jetbrains.annotations.Contract(value = "_, _ -> this", mutates = "this") ++ public @NotNull Builder with(@NotNull io.papermc.paper.loot.LootContextKey contextKey, @Nullable T context) { ++ if (context == null) { ++ this.contextMap.remove(contextKey); ++ } else { ++ this.contextMap.put(contextKey, context); ++ } ++ return this; ++ } ++ // Paper end ++ + /** + * Set how much luck to have when generating loot. + * + * @param luck the luck level + * @return the Builder + */ ++ @org.jetbrains.annotations.Contract(value = "_ -> this", mutates = "this") // Paper + @NotNull + public Builder luck(float luck) { + this.luck = luck; +@@ -138,6 +282,7 @@ public final class LootContext { + @NotNull + @Deprecated + public Builder lootingModifier(int modifier) { ++ this.isLegacy = true; // Paper + this.lootingModifier = modifier; + return this; + } +@@ -147,11 +292,14 @@ public final class LootContext { + * + * @param lootedEntity the looted entity + * @return the Builder ++ * @deprecated use {@link #with(io.papermc.paper.loot.LootContextKey, Object)} + */ + @NotNull ++ @org.jetbrains.annotations.Contract(value = "_ -> this", mutates = "this") // Paper ++ @Deprecated // Paper + public Builder lootedEntity(@Nullable Entity lootedEntity) { +- this.lootedEntity = lootedEntity; +- return this; ++ this.isLegacy = true; // Paper ++ return this.with(io.papermc.paper.loot.LootContextKey.THIS_ENTITY, lootedEntity); // Paper + } + + /** +@@ -161,11 +309,14 @@ public final class LootContext { + * + * @param killer the killer entity + * @return the Builder ++ * @deprecated use {@link #with(io.papermc.paper.loot.LootContextKey, Object)} + */ + @NotNull ++ @org.jetbrains.annotations.Contract(value = "_ -> this", mutates = "this") // Paper ++ @Deprecated // Paper + public Builder killer(@Nullable HumanEntity killer) { +- this.killer = killer; +- return this; ++ this.isLegacy = true; // Paper ++ return this.with(io.papermc.paper.loot.LootContextKey.ATTACKING_ENTITY, killer); // Paper + } + + /** +@@ -175,8 +326,9 @@ public final class LootContext { + * @return a new {@link LootContext} instance + */ + @NotNull ++ @org.jetbrains.annotations.Contract("-> new") // Paper + public LootContext build() { +- return new LootContext(location, luck, lootingModifier, lootedEntity, killer); ++ return new LootContext(this.world, luck, this.random, this.contextMap, this.isLegacy, this.lootingModifier); // Paper + } + } + } diff --git a/patches/server/1060-Improve-LootContext-API.patch b/patches/server/1060-Improve-LootContext-API.patch new file mode 100644 index 0000000000..ce2fbfca60 --- /dev/null +++ b/patches/server/1060-Improve-LootContext-API.patch @@ -0,0 +1,248 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Tue, 22 Mar 2022 22:17:13 -0700 +Subject: [PATCH] Improve LootContext API + +== AT == +public net.minecraft.world.level.storage.loot.LootContext params + +diff --git a/src/main/java/io/papermc/paper/loot/PaperLootContextKey.java b/src/main/java/io/papermc/paper/loot/PaperLootContextKey.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9f0729c56608696d10e5b3cba5b01b85a1fb0b6a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/loot/PaperLootContextKey.java +@@ -0,0 +1,115 @@ ++package io.papermc.paper.loot; ++ ++import com.google.common.collect.BiMap; ++import com.google.common.collect.HashBiMap; ++import io.papermc.paper.util.MCUtil; ++import java.util.HashSet; ++import java.util.IdentityHashMap; ++import java.util.Map; ++import java.util.Set; ++import java.util.function.Function; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.storage.loot.LootParams; ++import net.minecraft.world.level.storage.loot.parameters.LootContextParam; ++import net.minecraft.world.level.storage.loot.parameters.LootContextParamSet; ++import net.minecraft.world.level.storage.loot.parameters.LootContextParams; ++import org.bukkit.craftbukkit.block.CraftBlockEntityState; ++import org.bukkit.craftbukkit.block.CraftBlockStates; ++import org.bukkit.craftbukkit.block.data.CraftBlockData; ++import org.bukkit.craftbukkit.damage.CraftDamageSource; ++import org.bukkit.craftbukkit.entity.CraftEntity; ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.entity.Entity; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class PaperLootContextKey { ++ ++ public static final BiMap, LootContextKey> KEY_BI_MAP = HashBiMap.create(); ++ private static final Set> CONVERTERS = new HashSet<>(); ++ private static final Map, Converter> NMS_KEY_MAP = new IdentityHashMap<>(); ++ private static final Map, Converter> API_KEY_MAP = new IdentityHashMap<>(); ++ ++ static { ++ CONVERTERS.add(entity(LootContextParams.THIS_ENTITY, LootContextKey.THIS_ENTITY)); ++ CONVERTERS.add(entity(LootContextParams.LAST_DAMAGE_PLAYER, LootContextKey.LAST_DAMAGE_PLAYER)); ++ CONVERTERS.add(new LambdaConverter<>(LootContextParams.DAMAGE_SOURCE, LootContextKey.DAMAGE_SOURCE, ds -> ((CraftDamageSource) ds).getHandle(), CraftDamageSource::new)); ++ CONVERTERS.add(entity(LootContextParams.ATTACKING_ENTITY, LootContextKey.ATTACKING_ENTITY)); ++ CONVERTERS.add(entity(LootContextParams.DIRECT_ATTACKING_ENTITY, LootContextKey.DIRECT_ATTACKING_ENTITY)); ++ CONVERTERS.add(new LambdaConverter<>(LootContextParams.ORIGIN, LootContextKey.ORIGIN, MCUtil::toVec3, MCUtil::toPosition)); ++ CONVERTERS.add(new LambdaConverter<>(LootContextParams.BLOCK_STATE, LootContextKey.BLOCK_DATA, bd -> ((CraftBlockData) bd).getState(), BlockState::createCraftBlockData)); ++ CONVERTERS.add(new LambdaConverter<>(LootContextParams.BLOCK_ENTITY, LootContextKey.TILE_STATE, ts -> ((CraftBlockEntityState) ts).getTileEntity(), CraftBlockStates::getTileState)); ++ CONVERTERS.add(new LambdaConverter<>(LootContextParams.TOOL, LootContextKey.TOOL, CraftItemStack::asNMSCopy, net.minecraft.world.item.ItemStack::asBukkitCopy)); ++ CONVERTERS.add(identity(LootContextParams.EXPLOSION_RADIUS, LootContextKey.EXPLOSION_RADIUS)); ++ CONVERTERS.add(identity(LootContextParams.ENCHANTMENT_LEVEL, LootContextKey.ENCHANTMENT_LEVEL)); ++ CONVERTERS.add(identity(LootContextParams.ENCHANTMENT_ACTIVE, LootContextKey.ENCHANTMENT_ACTIVE)); ++ for (final Converter converter : CONVERTERS) { ++ KEY_BI_MAP.put(converter.nmsKey, converter.apiKey); ++ NMS_KEY_MAP.put(converter.nmsKey, converter); ++ API_KEY_MAP.put(converter.apiKey, converter); ++ } ++ } ++ ++ private PaperLootContextKey() { ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static void applyToNmsBuilder(final LootContextParamSet paramSet, final LootParams.Builder builder, final LootContextKey apiKey, final Object object) { ++ final LootContextParam nmsParam = (LootContextParam) KEY_BI_MAP.inverse().get(apiKey); ++ if (paramSet.getAllowed().contains(nmsParam) || paramSet.getRequired().contains(nmsParam)) { ++ builder.withOptionalParameter(nmsParam, ((Converter) API_KEY_MAP.get(apiKey)).toMinecraft((API) object)); ++ } ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static void applyToApiBuilder(final org.bukkit.loot.LootContext.Builder builder, final LootContextParam nmsKey, final Object object) { ++ builder.with(((LootContextKey) KEY_BI_MAP.get(nmsKey)), ((Converter) NMS_KEY_MAP.get(nmsKey)).toApi((MINECRAFT) object)); ++ } ++ ++ abstract static class Converter { ++ ++ final LootContextParam nmsKey; ++ final LootContextKey apiKey; ++ ++ private Converter(final LootContextParam nmsKey, final LootContextKey apiKey) { ++ this.nmsKey = nmsKey; ++ this.apiKey = apiKey; ++ } ++ ++ protected abstract MINECRAFT toMinecraft(API api); ++ ++ protected abstract API toApi(MINECRAFT minecraft); ++ } ++ ++ static class LambdaConverter extends Converter { ++ ++ private final Function toMinecraft; ++ private final Function toApi; ++ ++ private LambdaConverter(final LootContextParam nmsKey, final LootContextKey apiKey, final Function toMinecraft, final Function toApi) { ++ super(nmsKey, apiKey); ++ this.toMinecraft = toMinecraft; ++ this.toApi = toApi; ++ } ++ ++ @Override ++ protected MINECRAFT toMinecraft(final API api) { ++ return this.toMinecraft.apply(api); ++ } ++ ++ @Override ++ protected API toApi(final MINECRAFT minecraft) { ++ return this.toApi.apply(minecraft); ++ } ++ } ++ ++ private static LambdaConverter identity(final LootContextParam nmsKey, final LootContextKey apiKey) { ++ return new LambdaConverter<>(nmsKey, apiKey, Function.identity(), Function.identity()); ++ } ++ ++ @SuppressWarnings("unchecked") ++ private static LambdaConverter entity(final LootContextParam nmsKey, final LootContextKey apiKey) { ++ return new LambdaConverter<>(nmsKey, apiKey, e -> (MINECRAFT) ((CraftEntity) e).getHandle(), e -> (API) e.getBukkitEntity()); ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java b/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java +index f028daa4f23a1f1868c9922991259739cadc5da2..3d37937186eb8c2bd950816ccecd52912d3956e4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java +@@ -101,6 +101,22 @@ public class CraftLootTable implements org.bukkit.loot.LootTable { + + private LootParams convertContext(LootContext context, Random random) { + Preconditions.checkArgument(context != null, "LootContext cannot be null"); ++ // Paper start ++ if (!context.isLegacy()) { ++ final LootParams.Builder paramsBuilder = new LootParams.Builder(((CraftWorld) context.getWorld()).getHandle()).withLuck(context.getLuck()); ++ context.getContextMap().forEach((lootContextKey, o) -> io.papermc.paper.loot.PaperLootContextKey.applyToNmsBuilder(this.handle.getParamSet(), paramsBuilder, lootContextKey, o)); ++ ++ return paramsBuilder.create(this.handle.getParamSet()); ++ // final net.minecraft.world.level.storage.loot.LootContext.Builder contextBuilder = new net.minecraft.world.level.storage.loot.LootContext.Builder(paramsBuilder.create(this.handle.getParamSet())); ++ // // .withRandom(new RandomSourceWrapper(random != null ? random : context.getRandom())) ++ // return contextBuilder.create(java.util.Optional.empty()); ++ } else { ++ return this.convertLegacyContext(context, random); ++ } ++ } ++ @Deprecated ++ private LootParams convertLegacyContext(final LootContext context, final Random random) { ++ // Paper end + Location loc = context.getLocation(); + Preconditions.checkArgument(loc.getWorld() != null, "LootContext.getLocation#getWorld cannot be null"); + ServerLevel handle = ((CraftWorld) loc.getWorld()).getHandle(); +@@ -151,6 +167,20 @@ public class CraftLootTable implements org.bukkit.loot.LootTable { + } + + public static LootContext convertContext(net.minecraft.world.level.storage.loot.LootContext info) { ++ // Paper start ++ final LootContext.Builder builder = new LootContext.Builder(info.getLevel().getWorld()) ++ .withRandom(new org.bukkit.craftbukkit.util.RandomSourceWrapper.RandomWrapper(info.getRandom())) ++ .luck(info.getLuck()); ++ for (final LootContextParam nmsParam : io.papermc.paper.loot.PaperLootContextKey.KEY_BI_MAP.keySet()) { ++ if (info.hasParam(nmsParam)) { ++ io.papermc.paper.loot.PaperLootContextKey.applyToApiBuilder(builder, nmsParam, info.getParam(nmsParam)); ++ } ++ } ++ return builder.build(); ++ } ++ @Deprecated @io.papermc.paper.annotation.DoNotUse ++ public static LootContext convertLegacyContext(net.minecraft.world.level.storage.loot.LootContext info) { ++ // Paper end + Vec3 position = info.getParamOrNull(LootContextParams.ORIGIN); + if (position == null) { + position = info.getParamOrNull(LootContextParams.THIS_ENTITY).position(); // Every vanilla context has origin or this_entity, see LootContextParameterSets +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +index b7ff7af2513204b151340538d50a65c850bdb75f..63f1f55bea16aece9d50e0eaed4deca050f05279 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +@@ -292,6 +292,13 @@ public final class CraftBlockStates { + return CraftBlockStates.getBlockState(null, blockPosition, blockData, tileEntity); + } + ++ // Paper start ++ public static org.bukkit.block.TileState getTileState(final BlockEntity blockEntity) { ++ Preconditions.checkArgument(blockEntity.getLevel() != null, "blockEntity has no level"); ++ return (org.bukkit.block.TileState) getBlockState(blockEntity.getLevel().getWorld(), blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity); ++ } ++ // Paper end ++ + // See BlockStateFactory#createBlockState(World, BlockPosition, IBlockData, TileEntity) + public static CraftBlockState getBlockState(World world, BlockPos blockPosition, net.minecraft.world.level.block.state.BlockState blockData, BlockEntity tileEntity) { + Material material = CraftBlockType.minecraftToBukkit(blockData.getBlock()); +diff --git a/src/test/java/io/papermc/paper/loot/LootContextKeyTest.java b/src/test/java/io/papermc/paper/loot/LootContextKeyTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4aa515b62245d0c20dcc5352aff1984036922932 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/loot/LootContextKeyTest.java +@@ -0,0 +1,47 @@ ++package io.papermc.paper.loot; ++ ++import java.lang.reflect.Field; ++import java.lang.reflect.Modifier; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.world.level.storage.loot.parameters.LootContextParam; ++import net.minecraft.world.level.storage.loot.parameters.LootContextParams; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertNotEquals; ++ ++class LootContextKeyTest { ++ ++ static Map> vanillaParams = new HashMap<>(); ++ ++ @BeforeAll ++ static void collectVanillaContextParams() throws ReflectiveOperationException { ++ Class.forName(LootContextKey.class.getName()); // force-load class ++ for (final Field field : LootContextParams.class.getDeclaredFields()) { ++ if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()) && field.getType().equals(LootContextParam.class)) { ++ vanillaParams.put(field.getName(), (LootContextParam) field.get(null)); ++ } ++ } ++ } ++ ++ @Test ++ void testMinecraftToApi() { ++ vanillaParams.forEach((fieldName, lootContextParam) -> { ++ final List> matching = LootContextKeyImpl.KEYS.stream().filter(k -> k.key().asString().equals(lootContextParam.getName().toString())).toList(); ++ assertEquals(1, matching.size(), "Did not find 1 matching context key for " + lootContextParam.getName()); ++ }); ++ } ++ ++ @Test ++ void testApiToMinecraft() { ++ assertNotEquals(0, LootContextKeyImpl.KEYS.size()); ++ LootContextKeyImpl.KEYS.forEach(lootContextKey -> { ++ final List> matching = vanillaParams.values().stream().filter(p -> p.getName().toString().equals(lootContextKey.key().asString())).toList(); ++ assertEquals(1, matching.size(), "Did not find 1 matching loot param for " + lootContextKey.key()); ++ }); ++ } ++}