From 22ff9c4afecdf3f0cbdae8896aca1ac4ccffafce Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Wed, 8 Dec 2021 16:49:56 -0800 Subject: [PATCH] BlockProperty API --- patches/api/0501-BlockProperty-API.patch | 1063 +++++++++++++++++++ patches/server/1073-BlockProperty-API.patch | 619 +++++++++++ 2 files changed, 1682 insertions(+) create mode 100644 patches/api/0501-BlockProperty-API.patch create mode 100644 patches/server/1073-BlockProperty-API.patch diff --git a/patches/api/0501-BlockProperty-API.patch b/patches/api/0501-BlockProperty-API.patch new file mode 100644 index 0000000000..84822c9e74 --- /dev/null +++ b/patches/api/0501-BlockProperty-API.patch @@ -0,0 +1,1063 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Wed, 8 Dec 2021 16:49:33 -0800 +Subject: [PATCH] BlockProperty API + + +diff --git a/src/main/java/io/papermc/paper/block/fluid/FluidData.java b/src/main/java/io/papermc/paper/block/fluid/FluidData.java +index 0750219fc68261e5c396636967e0b633ae17b72e..09dd83cd595e7d4be18809e1ba0aec3a72d0232d 100644 +--- a/src/main/java/io/papermc/paper/block/fluid/FluidData.java ++++ b/src/main/java/io/papermc/paper/block/fluid/FluidData.java +@@ -1,5 +1,6 @@ + package io.papermc.paper.block.fluid; + ++import io.papermc.paper.block.property.BlockPropertyHolder; + import org.bukkit.Fluid; + import org.bukkit.Location; + import org.bukkit.util.Vector; +@@ -11,7 +12,7 @@ import org.jspecify.annotations.NullMarked; + * This type is not linked to a specific location and hence mostly resembles a {@link org.bukkit.block.data.BlockData}. + */ + @NullMarked +-public interface FluidData extends Cloneable { ++public interface FluidData extends Cloneable, BlockPropertyHolder { + + /** + * Gets the fluid type of this fluid data. +diff --git a/src/main/java/io/papermc/paper/block/property/AsIntegerProperty.java b/src/main/java/io/papermc/paper/block/property/AsIntegerProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..326aeca88e7c2ae1eed58b22c6468b80f8e0c3ec +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/AsIntegerProperty.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.collect.BiMap; ++import com.google.common.collect.ImmutableBiMap; ++import java.util.function.IntFunction; ++import org.jetbrains.annotations.ApiStatus; ++ ++@ApiStatus.Internal ++sealed interface AsIntegerProperty> extends BlockProperty permits NoteBlockProperty, RotationBlockProperty { ++ ++ static > BiMap createCache(final int max, final IntFunction function) { ++ final ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); ++ for (int i = 0; i <= max; i++) { ++ builder.put(i, function.apply(i)); ++ } ++ return builder.buildOrThrow(); ++ } ++ ++ BiMap cache(); ++ ++ default int toIntValue(final T value) { ++ if (!this.cache().inverse().containsKey(value)) { ++ throw ExceptionCreator.INSTANCE.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return this.cache().inverse().get(value); ++ } ++ ++ default T fromIntValue(final int value) { ++ if (!this.cache().containsKey(value)) { ++ throw ExceptionCreator.INSTANCE.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return this.cache().get(value); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/BlockProperties.java b/src/main/java/io/papermc/paper/block/property/BlockProperties.java +new file mode 100644 +index 0000000000000000000000000000000000000000..80490b03bd5fcf8dc30a14683442534b69f138b0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BlockProperties.java +@@ -0,0 +1,193 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.collect.HashMultimap; ++import com.google.common.collect.Multimap; ++import java.util.Arrays; ++import java.util.Set; ++import java.util.function.Predicate; ++import java.util.stream.Collectors; ++import org.bukkit.Axis; ++import org.bukkit.Instrument; ++import org.bukkit.Note; ++import org.bukkit.block.BlockFace; ++import org.bukkit.block.data.Bisected; ++import org.bukkit.block.data.FaceAttachable; ++import org.bukkit.block.data.Rail; ++import org.bukkit.block.data.type.Bamboo; ++import org.bukkit.block.data.type.Bed; ++import org.bukkit.block.data.type.Bell; ++import org.bukkit.block.data.type.BigDripleaf; ++import org.bukkit.block.data.type.Chest; ++import org.bukkit.block.data.type.Comparator; ++import org.bukkit.block.data.type.CreakingHeart; ++import org.bukkit.block.data.type.Door; ++import org.bukkit.block.data.type.Jigsaw; ++import org.bukkit.block.data.type.PointedDripstone; ++import org.bukkit.block.data.type.RedstoneWire; ++import org.bukkit.block.data.type.SculkSensor; ++import org.bukkit.block.data.type.Slab; ++import org.bukkit.block.data.type.Stairs; ++import org.bukkit.block.data.type.StructureBlock; ++import org.bukkit.block.data.type.TechnicalPiston; ++import org.bukkit.block.data.type.TrialSpawner; ++import org.bukkit.block.data.type.Vault; ++import org.bukkit.block.data.type.Wall; ++ ++/** ++ * All block properties applicable to {@link BlockPropertyHolder}s. ++ */ ++public final class BlockProperties { ++ ++ static final Multimap> PROPERTIES = HashMultimap.create(); ++ ++ public static final BooleanBlockProperty ATTACHED = bool("attached"); ++ public static final BooleanBlockProperty BOTTOM = bool("bottom"); ++ public static final BooleanBlockProperty CONDITIONAL = bool("conditional"); ++ public static final BooleanBlockProperty CRACKED = bool("cracked"); ++ public static final BooleanBlockProperty DISARMED = bool("disarmed"); ++ public static final BooleanBlockProperty DRAG = bool("drag"); ++ public static final BooleanBlockProperty ENABLED = bool("enabled"); ++ public static final BooleanBlockProperty EXTENDED = bool("extended"); ++ public static final BooleanBlockProperty EYE = bool("eye"); ++ public static final BooleanBlockProperty FALLING = bool("falling"); ++ public static final BooleanBlockProperty HANGING = bool("hanging"); ++ public static final BooleanBlockProperty HAS_BOTTLE_0 = bool("has_bottle_0"); ++ public static final BooleanBlockProperty HAS_BOTTLE_1 = bool("has_bottle_1"); ++ public static final BooleanBlockProperty HAS_BOTTLE_2 = bool("has_bottle_2"); ++ public static final BooleanBlockProperty HAS_RECORD = bool("has_record"); ++ public static final BooleanBlockProperty HAS_BOOK = bool("has_book"); ++ public static final BooleanBlockProperty INVERTED = bool("inverted"); ++ public static final BooleanBlockProperty IN_WALL = bool("in_wall"); ++ public static final BooleanBlockProperty LIT = bool("lit"); ++ public static final BooleanBlockProperty LOCKED = bool("locked"); ++ public static final BooleanBlockProperty OCCUPIED = bool("occupied"); ++ public static final BooleanBlockProperty OPEN = bool("open"); ++ public static final BooleanBlockProperty PERSISTENT = bool("persistent"); ++ public static final BooleanBlockProperty POWERED = bool("powered"); ++ public static final BooleanBlockProperty SHORT = bool("short"); ++ public static final BooleanBlockProperty SIGNAL_FIRE = bool("signal_fire"); ++ public static final BooleanBlockProperty SNOWY = bool("snowy"); ++ public static final BooleanBlockProperty TRIGGERED = bool("triggered"); ++ public static final BooleanBlockProperty UNSTABLE = bool("unstable"); ++ public static final BooleanBlockProperty WATERLOGGED = bool("waterlogged"); ++ public static final BooleanBlockProperty BERRIES = bool("berries"); ++ public static final BooleanBlockProperty BLOOM = bool("bloom"); ++ public static final BooleanBlockProperty SHRIEKING = bool("shrieking"); ++ public static final BooleanBlockProperty CAN_SUMMON = bool("can_summon"); ++ public static final BooleanBlockProperty UP = bool("up"); ++ public static final BooleanBlockProperty DOWN = bool("down"); ++ public static final BooleanBlockProperty NORTH = bool("north"); ++ public static final BooleanBlockProperty EAST = bool("east"); ++ public static final BooleanBlockProperty SOUTH = bool("south"); ++ public static final BooleanBlockProperty WEST = bool("west"); ++ public static final BooleanBlockProperty CHISELED_BOOKSHELF_SLOT_0_OCCUPIED = bool("slot_0_occupied"); ++ public static final BooleanBlockProperty CHISELED_BOOKSHELF_SLOT_1_OCCUPIED = bool("slot_1_occupied"); ++ public static final BooleanBlockProperty CHISELED_BOOKSHELF_SLOT_2_OCCUPIED = bool("slot_2_occupied"); ++ public static final BooleanBlockProperty CHISELED_BOOKSHELF_SLOT_3_OCCUPIED = bool("slot_3_occupied"); ++ public static final BooleanBlockProperty CHISELED_BOOKSHELF_SLOT_4_OCCUPIED = bool("slot_4_occupied"); ++ public static final BooleanBlockProperty CHISELED_BOOKSHELF_SLOT_5_OCCUPIED = bool("slot_5_occupied"); ++ public static final BooleanBlockProperty CRAFTING = bool("crafting"); ++ public static final BooleanBlockProperty OMINOUS = bool("ominous"); ++ public static final BooleanBlockProperty TIP = bool("tip"); ++ ++ public static final IntegerBlockProperty AGE_1 = integer("age", 0, 1); ++ public static final IntegerBlockProperty AGE_2 = integer("age", 0, 2); ++ public static final IntegerBlockProperty AGE_3 = integer("age", 0, 3); ++ public static final IntegerBlockProperty AGE_4 = integer("age", 0, 4); ++ public static final IntegerBlockProperty AGE_5 = integer("age", 0, 5); ++ public static final IntegerBlockProperty AGE_7 = integer("age", 0, 7); ++ public static final IntegerBlockProperty AGE_15 = integer("age", 0, 15); ++ public static final IntegerBlockProperty AGE_25 = integer("age", 0, 25); ++ public static final IntegerBlockProperty BITES = integer("bites", 0, 6); ++ public static final IntegerBlockProperty CANDLES = integer("candles", 1, 4); ++ public static final IntegerBlockProperty DELAY = integer("delay", 1, 4); ++ public static final IntegerBlockProperty DISTANCE = integer("distance", 1, 7); ++ public static final IntegerBlockProperty DUSTED = integer("dusted", 0, 3); ++ public static final IntegerBlockProperty EGGS = integer("eggs", 1, 4); ++ public static final IntegerBlockProperty FLOWER_AMOUNT = integer("flower_amount", 1, 4); ++ public static final IntegerBlockProperty HATCH = integer("hatch", 0, 2); ++ public static final IntegerBlockProperty LAYERS = integer("layers", 1, 8); ++ public static final IntegerBlockProperty LEVEL_CAULDRON = integer("level", 1, 3); ++ public static final IntegerBlockProperty LEVEL_COMPOSTER = integer("level", 0, 8); ++ public static final IntegerBlockProperty LEVEL_FLOWING = integer("level", 1, 8); ++ public static final IntegerBlockProperty LEVEL_HONEY = integer("honey_level", 0, 5); ++ public static final IntegerBlockProperty LEVEL = integer("level", 0, 15); ++ public static final IntegerBlockProperty MOISTURE = integer("moisture", 0, 7); ++ public static final BlockProperty NOTE = register(new NoteBlockProperty("note")); // is stored as int, but represented as object ++ public static final IntegerBlockProperty PICKLES = integer("pickles", 1, 4); ++ public static final IntegerBlockProperty POWER = integer("power", 0, 15); ++ public static final IntegerBlockProperty STAGE = integer("stage", 0, 1); ++ public static final IntegerBlockProperty STABILITY_DISTANCE = integer("distance", 0, 7); ++ public static final IntegerBlockProperty RESPAWN_ANCHOR_CHARGES = integer("charges", 0, 4); ++ ++ public static final EnumBlockProperty ROTATION_16 = register(new RotationBlockProperty("rotation")); // is stored as int, but represented as enum ++ public static final EnumBlockProperty HORIZONTAL_AXIS = enumeration("axis", Axis.class, Axis.X, Axis.Z); ++ public static final EnumBlockProperty AXIS = enumeration("axis", Axis.class); ++ public static final EnumBlockProperty FACING = enumeration("facing", BlockFace.class, BlockFace::isCartesian); ++ public static final EnumBlockProperty FACING_HOPPER = enumeration("facing", BlockFace.class, ((Predicate) BlockFace::isCartesian).and(face -> face != BlockFace.UP)); ++ public static final EnumBlockProperty HORIZONTAL_FACING = enumeration("facing", BlockFace.class, BlockFace::isCardinal); ++ public static final EnumBlockProperty ORIENTATION = enumeration("orientation", Jigsaw.Orientation.class); ++ public static final EnumBlockProperty ATTACH_FACE = enumeration("face", FaceAttachable.AttachedFace.class); ++ public static final EnumBlockProperty BELL_ATTACHMENT = enumeration("attachment", Bell.Attachment.class); ++ public static final EnumBlockProperty EAST_WALL = enumeration("east", Wall.Height.class); ++ public static final EnumBlockProperty NORTH_WALL = enumeration("north", Wall.Height.class); ++ public static final EnumBlockProperty SOUTH_WALL = enumeration("south", Wall.Height.class); ++ public static final EnumBlockProperty WEST_WALL = enumeration("west", Wall.Height.class); ++ public static final EnumBlockProperty EAST_REDSTONE = enumeration("east", RedstoneWire.Connection.class); ++ public static final EnumBlockProperty NORTH_REDSTONE = enumeration("north", RedstoneWire.Connection.class); ++ public static final EnumBlockProperty SOUTH_REDSTONE = enumeration("south", RedstoneWire.Connection.class); ++ public static final EnumBlockProperty WEST_REDSTONE = enumeration("west", RedstoneWire.Connection.class); ++ public static final EnumBlockProperty DOUBLE_BLOCK_HALF = enumeration("half", Bisected.Half.class); ++ public static final EnumBlockProperty HALF = enumeration("half", Bisected.Half.class); ++ public static final EnumBlockProperty RAIL_SHAPE = enumeration("shape", Rail.Shape.class); ++ public static final EnumBlockProperty RAIL_SHAPE_STRAIGHT = enumeration("shape", Rail.Shape.class, Rail.Shape::isStraight); ++ public static final EnumBlockProperty BED_PART = enumeration("part", Bed.Part.class); ++ public static final EnumBlockProperty CHEST_TYPE = enumeration("type", Chest.Type.class); ++ public static final EnumBlockProperty MODE_COMPARATOR = enumeration("mode", Comparator.Mode.class); ++ public static final EnumBlockProperty DOOR_HINGE = enumeration("hinge", Door.Hinge.class); ++ public static final EnumBlockProperty NOTEBLOCK_INSTRUMENT = enumeration("instrument", Instrument.class); ++ public static final EnumBlockProperty PISTON_TYPE = enumeration("type", TechnicalPiston.Type.class); ++ public static final EnumBlockProperty SLAB_TYPE = enumeration("type", Slab.Type.class); ++ public static final EnumBlockProperty STAIRS_SHAPE = enumeration("shape", Stairs.Shape.class); ++ public static final EnumBlockProperty STRUCTUREBLOCK_MODE = enumeration("mode", StructureBlock.Mode.class); ++ public static final EnumBlockProperty BAMBOO_LEAVES = enumeration("leaves", Bamboo.Leaves.class); ++ public static final EnumBlockProperty TILT = enumeration("tilt", BigDripleaf.Tilt.class); ++ public static final EnumBlockProperty VERTICAL_DIRECTION = enumeration("vertical_direction", BlockFace.class, face -> Math.abs(face.getModY()) > 0); ++ public static final EnumBlockProperty DRIPSTONE_THICKNESS = enumeration("thickness", PointedDripstone.Thickness.class); ++ public static final EnumBlockProperty SCULK_SENSOR_PHASE = enumeration("sculk_sensor_phase", SculkSensor.Phase.class); ++ public static final EnumBlockProperty TRIAL_SPAWNER_STATE = enumeration("trial_spawner_state", TrialSpawner.State.class); ++ public static final EnumBlockProperty VAULT_STATE = enumeration("vault_state", Vault.State.class); ++ public static final EnumBlockProperty CREAKING = enumeration("creaking", CreakingHeart.Creaking.class); ++ ++ private BlockProperties() { ++ } ++ ++ // ++ private static IntegerBlockProperty integer(final String name, final int min, final int max) { ++ return register(new IntegerBlockPropertyImpl(name, min, max)); ++ } ++ ++ private static BooleanBlockProperty bool(final String name) { ++ return register(new BooleanBlockPropertyImpl(name)); ++ } ++ ++ private static > EnumBlockProperty enumeration(final String name, final Class enumClass) { ++ return register(new EnumBlockPropertyImpl<>(name, enumClass, Set.of(enumClass.getEnumConstants()))); ++ } ++ ++ @SuppressWarnings("SameParameterValue") ++ @SafeVarargs ++ private static > EnumBlockProperty enumeration(final String name, final Class enumClass, final E... values) { ++ return register(new EnumBlockPropertyImpl<>(name, enumClass, Set.of(values))); ++ } ++ ++ private static > EnumBlockProperty enumeration(final String name, final Class enumClass, final Predicate test) { ++ return register(new EnumBlockPropertyImpl<>(name, enumClass, Arrays.stream(enumClass.getEnumConstants()).filter(test).collect(Collectors.toSet()))); ++ } ++ ++ private static , P extends BlockProperty> P register(final P property) { ++ PROPERTIES.put(property.name(), property); ++ return property; ++ } ++ // ++} +diff --git a/src/main/java/io/papermc/paper/block/property/BlockProperty.java b/src/main/java/io/papermc/paper/block/property/BlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c46c13cd3d81e15d876d3227a54b34f796e68a19 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BlockProperty.java +@@ -0,0 +1,142 @@ ++package io.papermc.paper.block.property; ++ ++import java.util.Optional; ++import java.util.Set; ++import org.jetbrains.annotations.Unmodifiable; ++ ++/** ++ * A property that applies to a {@link BlockPropertyHolder} such ++ * as {@link org.bukkit.block.data.BlockData} or {@link io.papermc.paper.block.fluid.FluidData}. ++ * ++ * @param the value type ++ * @see BlockProperties ++ */ ++public sealed interface BlockProperty> permits AsIntegerProperty, BooleanBlockProperty, EnumBlockProperty, IntegerBlockProperty { ++ ++ /** ++ * Gets the name of this property. ++ * ++ * @return the name ++ */ ++ String name(); ++ ++ /** ++ * Gets the value type of this property. ++ * ++ * @return the value type ++ */ ++ Class type(); ++ ++ /** ++ * Gets the string name for a value of this property. ++ * ++ * @param value the value to get the string name of ++ * @return the string name of the value ++ * @throws IllegalArgumentException if the value is not valid for ++ * @see #value(String) ++ */ ++ String name(T value); ++ ++ /** ++ * Checks if the name is a valid name ++ * for a value of this property. ++ * ++ * @param name the name to check ++ * @return true if valid ++ * @see #value(String) ++ */ ++ boolean isValidName(String name); ++ ++ /** ++ * Gets the value of this property from the string name. ++ * Throws an exception if no value is found with that name. ++ * ++ * @param name the name of the value ++ * @return the property with the specified name ++ * @see #isValidName(String) ++ */ ++ T value(String name); ++ ++ /** ++ * Checks if the object is a valid value for this property. ++ * ++ * @param object the object to check ++ * @return true if it's a valid value ++ * @see #setValue(BlockPropertyHolder.Mutable, Object) ++ */ ++ boolean isValue(Object object); ++ ++ /** ++ * Gets an immutable collection of possible values for this property. ++ * ++ * @return an immutable collection of values ++ */ ++ @Unmodifiable Set values(); ++ ++ /** ++ * Checks if a {@link BlockPropertyHolder} has this property. ++ * ++ * @param holder the holder of a set of properties (like {@link org.bukkit.block.data.BlockData}) ++ * @return true if this property is present ++ * @see BlockPropertyHolder#hasProperty(BlockProperty) ++ */ ++ default boolean hasValueOn(final BlockPropertyHolder holder) { ++ return holder.hasProperty(this); ++ } ++ ++ /** ++ * Gets the value from a {@link BlockPropertyHolder} for this property. ++ * ++ * @param holder the holder of a set of properties (like {@link org.bukkit.block.data.BlockData}) ++ * @return the non-null value ++ * @throws IllegalArgumentException if this property is not present ++ * @see #hasValueOn(BlockPropertyHolder) ++ * @see BlockPropertyHolder#getValue(BlockProperty) ++ */ ++ default T getValue(final BlockPropertyHolder holder) { ++ return holder.getValue(this); ++ } ++ ++ /** ++ * Gets the optional of the value for this property. ++ * ++ * @param holder the holder of a set of properties (like {@link org.bukkit.block.data.BlockData}) ++ * @return the optional of the value, will be empty if the property is not present ++ * @see #getValue(BlockPropertyHolder) ++ * @see BlockPropertyHolder#getOptionalValue(BlockProperty) ++ */ ++ default Optional getOptionalValue(final BlockPropertyHolder holder) { ++ return holder.getOptionalValue(this); ++ } ++ ++ /** ++ * Sets the value on a {@link BlockPropertyHolder} for this property. ++ * ++ * @param holder the mutable holder of a set of properties (like {@link org.bukkit.block.data.BlockData}) ++ * @param value the value for this property ++ * @throws IllegalArgumentException if this property is not present ++ * @see #hasValueOn(BlockPropertyHolder) ++ * @see BlockPropertyHolder#hasProperty(BlockProperty) ++ */ ++ default void setValue(final BlockPropertyHolder.Mutable holder, final T value) { ++ holder.setValue(this, value); ++ } ++ ++ /** ++ * Sets the value on a {@link BlockPropertyHolder} for this property. ++ * ++ * @param holder the mutable holder of a set of properties (like {@link org.bukkit.block.data.BlockData}) ++ * @param value the value for this property ++ * @throws IllegalArgumentException if this property is not present ++ * @see #hasValueOn(BlockPropertyHolder) ++ * @see BlockPropertyHolder#hasProperty(BlockProperty) ++ */ ++ @SuppressWarnings("unchecked") ++ default void setValue(final BlockPropertyHolder.Mutable holder, final Object value) { ++ if (!this.isValue(value)) { ++ throw ExceptionCreator.INSTANCE.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ this.setValue(holder, (T) value); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/block/property/BlockPropertyHolder.java b/src/main/java/io/papermc/paper/block/property/BlockPropertyHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f2d61a7a831e770d2988a81470f822e1af08446e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BlockPropertyHolder.java +@@ -0,0 +1,95 @@ ++package io.papermc.paper.block.property; ++ ++import java.util.Collection; ++import java.util.Optional; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Represents an object that holds {@link BlockProperty}s. ++ * @see BlockProperties ++ */ ++public interface BlockPropertyHolder { ++ ++ /** ++ * Gets the property with this name on the holder (if it exists) ++ * ++ * @param propertyName name of the property ++ * @param property type; ++ * @return the property, if one is found with that name ++ */ ++ > @Nullable BlockProperty getProperty(String propertyName); ++ ++ /** ++ * Checks if this has the property. ++ * ++ * @param property the property to check for ++ * @param property type ++ * @return true if property is present ++ * @see BlockProperty#hasValueOn(BlockPropertyHolder) ++ */ ++ > boolean hasProperty(BlockProperty property); ++ ++ /** ++ * Gets the value for the specified property ++ * ++ * @param property the property ++ * @param property type ++ * @return the non-null value ++ * @throws IllegalArgumentException if the property is not present ++ * @see #hasProperty(BlockProperty) ++ * @see BlockProperty#getValue(BlockPropertyHolder) ++ */ ++ > T getValue(BlockProperty property); ++ ++ /** ++ * Gets the optional of the value for the specified property. ++ * ++ * @param property the property ++ * @param property type ++ * @return the optional of the value, will be empty if the property is not present ++ * @see #getValue(BlockProperty) ++ * @see BlockProperty#getOptionalValue(BlockPropertyHolder) ++ */ ++ > Optional getOptionalValue(BlockProperty property); ++ ++ /** ++ * Get all properties present on this. ++ * ++ * @return an unmodifiable collection of properties ++ */ ++ @Unmodifiable Collection> getProperties(); ++ ++ /** ++ * Represents an object that holds {@link BlockProperty}s that can be changed. ++ * @see BlockProperties ++ */ ++ interface Mutable extends BlockPropertyHolder { ++ ++ /** ++ * Sets the value of the specified property. ++ * ++ * @param property the property ++ * @param value the value for the property ++ * @param property type ++ * @throws IllegalArgumentException if the property is not present or if the value is invalid ++ * @see #hasProperty(BlockProperty) ++ * @see BlockProperty#setValue(Mutable, Comparable) ++ */ ++ > void setValue(BlockProperty property, T value); ++ ++ /** ++ * Sets the value of the specified property. ++ * ++ * @param property the property ++ * @param value the value for the property ++ * @param property type ++ * @throws IllegalArgumentException if the property is not present or if the value is invalid ++ * @see #hasProperty(BlockProperty) ++ * @see BlockProperty#setValue(Mutable, Comparable) ++ */ ++ default > void setValue(final BlockProperty property, final Object value) { ++ property.setValue(this, value); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/BooleanBlockProperty.java b/src/main/java/io/papermc/paper/block/property/BooleanBlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..10506b71a2448d6684a6b4284fa0dd946bafddd4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BooleanBlockProperty.java +@@ -0,0 +1,8 @@ ++package io.papermc.paper.block.property; ++ ++/** ++ * A block data property for a {@code boolean} value. ++ * @see BlockProperties ++ */ ++public sealed interface BooleanBlockProperty extends BlockProperty permits BooleanBlockPropertyImpl { ++} +diff --git a/src/main/java/io/papermc/paper/block/property/BooleanBlockPropertyImpl.java b/src/main/java/io/papermc/paper/block/property/BooleanBlockPropertyImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..192fb86fd08c7e8ae5979cf596928e696bebd35a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BooleanBlockPropertyImpl.java +@@ -0,0 +1,45 @@ ++package io.papermc.paper.block.property; ++ ++import it.unimi.dsi.fastutil.booleans.BooleanSet; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Unmodifiable; ++ ++@ApiStatus.Internal ++record BooleanBlockPropertyImpl(String name) implements BooleanBlockProperty { ++ ++ private static final BooleanSet VALUES = BooleanSet.of(true, false); ++ ++ @Override ++ public Class type() { ++ return Boolean.class; ++ } ++ ++ @Override ++ public String name(final Boolean value) { ++ return value.toString(); ++ } ++ ++ @Override ++ public boolean isValidName(final String name) { ++ return "true".equals(name) || "false".equals(name); ++ } ++ ++ @Override ++ public Boolean value(final String name) { ++ return switch (name) { ++ case "true" -> true; ++ case "false" -> false; ++ default -> throw ExceptionCreator.INSTANCE.create(name, ExceptionCreator.Type.NAME, this); ++ }; ++ } ++ ++ @Override ++ public boolean isValue(final Object object) { ++ return object instanceof Boolean; ++ } ++ ++ @Override ++ public @Unmodifiable BooleanSet values() { ++ return VALUES; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/EnumBlockProperty.java b/src/main/java/io/papermc/paper/block/property/EnumBlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c4dad0f77e96a9107760fbcc25ba36ad34aee09a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/EnumBlockProperty.java +@@ -0,0 +1,8 @@ ++package io.papermc.paper.block.property; ++ ++/** ++ * A block data property for an {@code enum} value. ++ * @see BlockProperties ++ */ ++public sealed interface EnumBlockProperty> extends BlockProperty permits EnumBlockPropertyImpl { ++} +diff --git a/src/main/java/io/papermc/paper/block/property/EnumBlockPropertyImpl.java b/src/main/java/io/papermc/paper/block/property/EnumBlockPropertyImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..16cde540a10e0d0e4da04d2380ed652777e1677c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/EnumBlockPropertyImpl.java +@@ -0,0 +1,80 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.base.Suppliers; ++import com.google.common.collect.Sets; ++import java.util.Collection; ++import java.util.List; ++import java.util.Set; ++import java.util.function.Supplier; ++import net.kyori.adventure.util.Index; ++import org.bukkit.Bukkit; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Unmodifiable; ++ ++@ApiStatus.Internal ++sealed class EnumBlockPropertyImpl> implements EnumBlockProperty permits RotationBlockProperty { ++ ++ private final String name; ++ private final Class type; ++ private final Set values; ++ private final Supplier> byNameIndex; ++ ++ EnumBlockPropertyImpl(final String name, final Class type, final Collection values) { ++ this.name = name; ++ this.type = type; ++ this.values = Sets.immutableEnumSet(values); ++ this.byNameIndex = Suppliers.memoize(this::createByNameIndex); ++ } ++ ++ @Override ++ public String name() { ++ return this.name; ++ } ++ ++ @Override ++ public Class type() { ++ return this.type; ++ } ++ ++ private Index byNameIndex() { ++ return this.byNameIndex.get(); ++ } ++ ++ @SuppressWarnings("deprecation") // valid unsafe use ++ private Index createByNameIndex() { ++ return Index.create(e -> Bukkit.getUnsafe().getPropertyEnumName(this, e), List.copyOf(this.values)); ++ } ++ ++ @Override ++ public String name(final E value) { ++ final String valueName = this.byNameIndex().key(value); ++ if (valueName == null) { ++ throw ExceptionCreator.INSTANCE.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return valueName; ++ } ++ ++ @Override ++ public boolean isValidName(final String name) { ++ return this.byNameIndex().value(name) != null; ++ } ++ ++ @Override ++ public E value(final String name) { ++ final E value = this.byNameIndex().value(name); ++ if (value == null) { ++ throw ExceptionCreator.INSTANCE.create(name, ExceptionCreator.Type.NAME, this); ++ } ++ return value; ++ } ++ ++ @Override ++ public boolean isValue(final Object object) { ++ return this.type().isInstance(object) && this.values.contains(object); ++ } ++ ++ @Override ++ public @Unmodifiable Set values() { ++ return this.values; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/ExceptionCreator.java b/src/main/java/io/papermc/paper/block/property/ExceptionCreator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c25d45db150380835d79904afbddfed1b373c345 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/ExceptionCreator.java +@@ -0,0 +1,18 @@ ++package io.papermc.paper.block.property; ++ ++import java.util.Locale; ++import org.jetbrains.annotations.ApiStatus; ++ ++@ApiStatus.Internal ++@FunctionalInterface ++interface ExceptionCreator { ++ ++ ExceptionCreator INSTANCE = (value, type, property) -> new IllegalArgumentException(String.format("%s (%s) is not a valid %s for %s", value, value.getClass().getSimpleName(), type.name().toLowerCase(Locale.ENGLISH), property)); ++ ++ IllegalArgumentException create(Object value, Type type, BlockProperty property); ++ ++ enum Type { ++ NAME, ++ VALUE, ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/IntegerBlockProperty.java b/src/main/java/io/papermc/paper/block/property/IntegerBlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bddfa8680a41261b381048ec9842088f3341db7b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/IntegerBlockProperty.java +@@ -0,0 +1,22 @@ ++package io.papermc.paper.block.property; ++ ++/** ++ * A block data property for an {@code int} value. ++ * @see BlockProperties ++ */ ++public sealed interface IntegerBlockProperty extends BlockProperty permits IntegerBlockPropertyImpl { ++ ++ /** ++ * Gets the min value for this property. ++ * ++ * @return the min value ++ */ ++ int min(); ++ ++ /** ++ * Gets the max value for this property. ++ * ++ * @return the max value ++ */ ++ int max(); ++} +diff --git a/src/main/java/io/papermc/paper/block/property/IntegerBlockPropertyImpl.java b/src/main/java/io/papermc/paper/block/property/IntegerBlockPropertyImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..999be2ca26e6d9231e0cf1768cf9cca76af92087 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/IntegerBlockPropertyImpl.java +@@ -0,0 +1,88 @@ ++package io.papermc.paper.block.property; ++ ++import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.ints.IntSet; ++import it.unimi.dsi.fastutil.ints.IntSets; ++import org.jetbrains.annotations.ApiStatus; ++ ++@ApiStatus.Internal ++record IntegerBlockPropertyImpl(String name, IntSet values, int min, int max) implements IntegerBlockProperty { ++ ++ IntegerBlockPropertyImpl(final String name, final int min, final int max) { ++ this(name, createValues(min, max), min, max); ++ } ++ ++ static IntSet createValues(final int min, final int max) { ++ if (min < 0 || max <= min) { ++ throw new IllegalArgumentException("Invalid range. Min: " + min + ", Max: " + max); ++ } ++ final IntSet set = new IntLinkedOpenHashSet(); ++ for (int i = min; i <= max; i++) { ++ set.add(i); ++ } ++ return IntSets.unmodifiable(set); // use unmodifiable to preserve order (but in reality its immutable) ++ } ++ ++ @Override ++ public Class type() { ++ return Integer.class; ++ } ++ ++ /** ++ * Gets the min value for this property. ++ * ++ * @return the min value ++ */ ++ @Override ++ public int min() { ++ return this.min; ++ } ++ ++ /** ++ * Gets the max value for this property. ++ * ++ * @return the max value ++ */ ++ @Override ++ public int max() { ++ return this.max; ++ } ++ ++ @Override ++ public String name(final Integer value) { ++ if (value > this.max || value < this.min) { ++ throw ExceptionCreator.INSTANCE.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return value.toString(); ++ } ++ ++ @Override ++ public boolean isValidName(final String name) { ++ try { ++ final int value = Integer.parseInt(name); ++ if (this.values.contains(value)) { ++ return true; ++ } ++ } catch (final NumberFormatException ignored) { ++ } ++ return false; ++ } ++ ++ @Override ++ public Integer value(final String name) { ++ try { ++ final int value = Integer.parseInt(name); ++ if (this.values.contains(value)) { ++ return value; ++ } ++ throw ExceptionCreator.INSTANCE.create(name, ExceptionCreator.Type.NAME, this); ++ } catch (final NumberFormatException exception) { ++ throw ExceptionCreator.INSTANCE.create(name, ExceptionCreator.Type.NAME, this); ++ } ++ } ++ ++ @Override ++ public boolean isValue(final Object object) { ++ return object instanceof final Integer num && num >= this.min && num <= this.max; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/NoteBlockProperty.java b/src/main/java/io/papermc/paper/block/property/NoteBlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..25f98e3fee291c43df9f55243b5088e0d895043d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/NoteBlockProperty.java +@@ -0,0 +1,61 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.collect.BiMap; ++import java.util.Collections; ++import java.util.Set; ++import org.bukkit.Note; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Unmodifiable; ++ ++@ApiStatus.Internal ++record NoteBlockProperty(String name, BiMap cache) implements AsIntegerProperty { ++ ++ NoteBlockProperty(final String name) { ++ this(name, AsIntegerProperty.createCache(24, Note::new)); ++ } ++ ++ @Override ++ public Class type() { ++ return Note.class; ++ } ++ ++ @Override ++ public String name(final Note value) { ++ return String.valueOf(value.getId()); ++ } ++ ++ @Override ++ public boolean isValidName(final String name) { ++ try { ++ final Integer value = Integer.valueOf(name); ++ if (this.cache.containsKey(value)) { ++ return true; ++ } ++ } catch (final NumberFormatException ignored) { ++ } ++ return false; ++ } ++ ++ @Override ++ public Note value(final String name) { ++ try { ++ final Integer value = Integer.valueOf(name); ++ if (this.cache.containsKey(value)) { ++ return this.cache.get(value); ++ } ++ throw ExceptionCreator.INSTANCE.create(name, ExceptionCreator.Type.NAME, this); ++ } catch (final NumberFormatException exception) { ++ throw ExceptionCreator.INSTANCE.create(name, ExceptionCreator.Type.NAME, this); ++ } ++ } ++ ++ @Override ++ public boolean isValue(final Object object) { ++ return object instanceof final Note note && this.cache.inverse().containsKey(note); ++ } ++ ++ @Override ++ public @Unmodifiable Set values() { ++ return Collections.unmodifiableSet(this.cache.values()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/RotationBlockProperty.java b/src/main/java/io/papermc/paper/block/property/RotationBlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5eb653b71199ea5ffcbf12f69387f163f5821617 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/RotationBlockProperty.java +@@ -0,0 +1,64 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.collect.BiMap; ++import com.google.common.collect.Sets; ++import java.util.LinkedHashSet; ++import java.util.Set; ++import org.bukkit.block.BlockFace; ++import org.jetbrains.annotations.ApiStatus; ++ ++/** ++ * Special exception for the {@link BlockProperties#ROTATION_16} property because ++ * in the API it's represented as an enum, but stored as an integer. ++ */ ++@ApiStatus.Internal ++final class RotationBlockProperty extends EnumBlockPropertyImpl implements AsIntegerProperty { ++ ++ private static final Set VALUES; ++ ++ static { ++ final Set values = new LinkedHashSet<>(); ++ for (final BlockFace face : BlockFace.values()) { ++ if (face.getModY() == 0 && (face.getModZ() != 0 || face.getModX() != 0)) { ++ values.add(face); ++ } ++ } ++ Preconditions.checkArgument(values.size() == 16, "Expected 16 enum values"); ++ VALUES = Sets.immutableEnumSet(values); ++ } ++ ++ private final BiMap cache; ++ ++ RotationBlockProperty(final String name) { ++ super(name, BlockFace.class, VALUES); ++ this.cache = AsIntegerProperty.createCache(0xF, RotationBlockProperty::intToEnum); ++ } ++ ++ @Override ++ public BiMap cache() { ++ return this.cache; ++ } ++ ++ private static BlockFace intToEnum(final int value) { ++ return switch (value) { ++ case 0x0 -> BlockFace.SOUTH; ++ case 0x1 -> BlockFace.SOUTH_SOUTH_WEST; ++ case 0x2 -> BlockFace.SOUTH_WEST; ++ case 0x3 -> BlockFace.WEST_SOUTH_WEST; ++ case 0x4 -> BlockFace.WEST; ++ case 0x5 -> BlockFace.WEST_NORTH_WEST; ++ case 0x6 -> BlockFace.NORTH_WEST; ++ case 0x7 -> BlockFace.NORTH_NORTH_WEST; ++ case 0x8 -> BlockFace.NORTH; ++ case 0x9 -> BlockFace.NORTH_NORTH_EAST; ++ case 0xA -> BlockFace.NORTH_EAST; ++ case 0xB -> BlockFace.EAST_NORTH_EAST; ++ case 0xC -> BlockFace.EAST; ++ case 0xD -> BlockFace.EAST_SOUTH_EAST; ++ case 0xE -> BlockFace.SOUTH_EAST; ++ case 0xF -> BlockFace.SOUTH_SOUTH_EAST; ++ default -> throw new IllegalArgumentException("Illegal value " + value); ++ }; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/package-info.java b/src/main/java/io/papermc/paper/block/property/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..359cc04d2e8d6f7eb2ea373e19aff4339f0a431c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/package-info.java +@@ -0,0 +1,4 @@ ++@NullMarked ++package io.papermc.paper.block.property; ++ ++import org.jspecify.annotations.NullMarked; +diff --git a/src/main/java/org/bukkit/Note.java b/src/main/java/org/bukkit/Note.java +index aff858346776386f1288b648b221404f7f412399..5fd00e6f5ecd9fd0bcc2bca5675955deb4fcf2f1 100644 +--- a/src/main/java/org/bukkit/Note.java ++++ b/src/main/java/org/bukkit/Note.java +@@ -9,7 +9,14 @@ import org.jetbrains.annotations.Nullable; + /** + * A note class to store a specific note. + */ +-public class Note { ++// Paper start - implement Comparable ++public class Note implements Comparable { ++ ++ @Override ++ public int compareTo(@NotNull Note other) { ++ return Byte.compare(this.note, other.note); ++ } ++ // Paper end + + /** + * An enum holding tones. +diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java +index a491dc40093e19b8d1900443ad613223fd7f3119..72259479c152d14effc9b72625242e5791124f7a 100644 +--- a/src/main/java/org/bukkit/UnsafeValues.java ++++ b/src/main/java/org/bukkit/UnsafeValues.java +@@ -288,6 +288,18 @@ public interface UnsafeValues { + + String getStatisticCriteriaKey(@NotNull org.bukkit.Statistic statistic); // Paper - fix custom stats criteria creation + ++ // Paper start - block property API ++ /** ++ * Gets the string representation for this bukkit enum. ++ * ++ * @param enumProperty the enum data property ++ * @param bukkitEnum the enum to get the string representation of ++ * @param the bukkit enum type ++ * @return the string representation of the supplied enum ++ */ ++ > @org.jetbrains.annotations.NotNull String getPropertyEnumName(@org.jetbrains.annotations.NotNull io.papermc.paper.block.property.EnumBlockProperty enumProperty, @org.jetbrains.annotations.NotNull B bukkitEnum); ++ // Paper end ++ + // Paper start - spawn egg color visibility + /** + * Obtains the underlying color informating for a spawn egg of a given +diff --git a/src/main/java/org/bukkit/block/BlockFace.java b/src/main/java/org/bukkit/block/BlockFace.java +index fe83ed9bf6b6288991b044bb992bd8b2f00edc24..d476385fcb0a1c62d85cb69398b01998d5734644 100644 +--- a/src/main/java/org/bukkit/block/BlockFace.java ++++ b/src/main/java/org/bukkit/block/BlockFace.java +@@ -104,6 +104,15 @@ public enum BlockFace { + } + } + ++ // Paper start ++ public boolean isCardinal() { ++ return switch (this) { ++ case NORTH, SOUTH, EAST, WEST -> true; ++ default -> false; ++ }; ++ } ++ // Paper end ++ + @NotNull + public BlockFace getOppositeFace() { + switch (this) { +diff --git a/src/main/java/org/bukkit/block/data/BlockData.java b/src/main/java/org/bukkit/block/data/BlockData.java +index 0ecc54bd810a2805b7209d9433b76743500e45a8..4eb92e7d20ee41f38444d90d26b1dba7de2212ac 100644 +--- a/src/main/java/org/bukkit/block/data/BlockData.java ++++ b/src/main/java/org/bukkit/block/data/BlockData.java +@@ -17,7 +17,7 @@ import org.jetbrains.annotations.ApiStatus; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; + +-public interface BlockData extends Cloneable { ++public interface BlockData extends Cloneable, io.papermc.paper.block.property.BlockPropertyHolder.Mutable { // Paper - property API + + /** + * Get the Material represented by this block data. +diff --git a/src/main/java/org/bukkit/block/data/Rail.java b/src/main/java/org/bukkit/block/data/Rail.java +index c8bdab081a316a9fd227d78a70c43c502e4085ef..027ebe6d015b52a112dba353e20f0ca563c7f537 100644 +--- a/src/main/java/org/bukkit/block/data/Rail.java ++++ b/src/main/java/org/bukkit/block/data/Rail.java +@@ -83,5 +83,10 @@ public interface Rail extends Waterlogged { + * block. + */ + NORTH_EAST; ++ // Paper start ++ public boolean isStraight() { ++ return this != SOUTH_EAST && this != SOUTH_WEST && this != NORTH_WEST && this != NORTH_EAST; ++ } ++ // Paper end + } + } diff --git a/patches/server/1073-BlockProperty-API.patch b/patches/server/1073-BlockProperty-API.patch new file mode 100644 index 0000000000..e86525961d --- /dev/null +++ b/patches/server/1073-BlockProperty-API.patch @@ -0,0 +1,619 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Wed, 8 Dec 2021 16:49:37 -0800 +Subject: [PATCH] BlockProperty API + + +diff --git a/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java b/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java +index 479bc32241ebadf8bbc1080b601f61391ad37fa4..b66f1a6e896ae4c5242620fbdc804a40ab5cb8e2 100644 +--- a/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java ++++ b/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java +@@ -3,10 +3,14 @@ package io.papermc.paper.block.fluid; + import com.google.common.base.Preconditions; + import io.papermc.paper.block.fluid.type.PaperFallingFluidData; + import io.papermc.paper.block.fluid.type.PaperFlowingFluidData; ++import io.papermc.paper.block.property.PaperBlockPropertyHolder; + import io.papermc.paper.util.MCUtil; + import java.util.HashMap; + import java.util.Map; + import java.util.function.Function; ++import net.minecraft.world.level.block.state.StateDefinition; ++import net.minecraft.world.level.block.state.properties.EnumProperty; ++import net.minecraft.world.level.block.state.properties.Property; + import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.level.material.LavaFluid; + import net.minecraft.world.level.material.WaterFluid; +@@ -14,11 +18,12 @@ import org.bukkit.Fluid; + import org.bukkit.Location; + import org.bukkit.craftbukkit.CraftFluid; + import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.craftbukkit.block.data.CraftBlockData; + import org.bukkit.craftbukkit.util.CraftVector; + import org.bukkit.util.Vector; + import org.jetbrains.annotations.NotNull; + +-public class PaperFluidData implements FluidData { ++public class PaperFluidData implements FluidData, PaperBlockPropertyHolder { + + private final FluidState state; + +@@ -99,6 +104,21 @@ public class PaperFluidData implements FluidData { + // + } + ++ @Override ++ public StateDefinition getStateDefinition() { ++ return this.state.getType().getStateDefinition(); ++ } ++ ++ @Override ++ public > T get(final Property ibs) { ++ return this.state.getValue(ibs); ++ } ++ ++ @Override ++ public > B get(final EnumProperty nms, final Class bukkit) { ++ return CraftBlockData.toBukkit(this.state.getValue(nms), bukkit); ++ } ++ + static void register(final Class fluid, final Function creator) { + Preconditions.checkState(MAP.put(fluid, creator) == null, "Duplicate mapping %s->%s", fluid, creator); + MAP.put(fluid, creator); +diff --git a/src/main/java/io/papermc/paper/block/property/PaperBlockProperties.java b/src/main/java/io/papermc/paper/block/property/PaperBlockProperties.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ad1e17fe4fa8ab410c5443f0912b63f89e67d8ef +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/PaperBlockProperties.java +@@ -0,0 +1,82 @@ ++package io.papermc.paper.block.property; ++ ++import java.util.Collection; ++import net.minecraft.world.level.block.state.properties.BlockStateProperties; ++import net.minecraft.world.level.block.state.properties.Property; ++import org.bukkit.craftbukkit.block.data.CraftBlockData; ++ ++public final class PaperBlockProperties { ++ ++ public static void setup() { ++ // ++ registerProp(BlockStateProperties.DOUBLE_BLOCK_HALF, BlockProperties.DOUBLE_BLOCK_HALF); ++ registerProp(BlockStateProperties.HALF, BlockProperties.HALF); ++ registerProp(BlockStateProperties.HORIZONTAL_AXIS, BlockProperties.HORIZONTAL_AXIS); ++ registerProp(BlockStateProperties.AXIS, BlockProperties.AXIS); ++ registerProp(BlockStateProperties.CHEST_TYPE, BlockProperties.CHEST_TYPE); ++ registerProp(BlockStateProperties.PISTON_TYPE, BlockProperties.PISTON_TYPE); ++ registerProp(BlockStateProperties.SLAB_TYPE, BlockProperties.SLAB_TYPE); ++ registerProp(BlockStateProperties.MODE_COMPARATOR, BlockProperties.MODE_COMPARATOR); ++ registerProp(BlockStateProperties.STRUCTUREBLOCK_MODE, BlockProperties.STRUCTUREBLOCK_MODE); ++ registerProp(BlockStateProperties.WEST, BlockProperties.WEST); ++ registerProp(BlockStateProperties.WEST_WALL, BlockProperties.WEST_WALL); ++ registerProp(BlockStateProperties.WEST_REDSTONE, BlockProperties.WEST_REDSTONE); ++ registerProp(BlockStateProperties.EAST, BlockProperties.EAST); ++ registerProp(BlockStateProperties.EAST_WALL, BlockProperties.EAST_WALL); ++ registerProp(BlockStateProperties.EAST_REDSTONE, BlockProperties.EAST_REDSTONE); ++ registerProp(BlockStateProperties.NORTH, BlockProperties.NORTH); ++ registerProp(BlockStateProperties.NORTH_WALL, BlockProperties.NORTH_WALL); ++ registerProp(BlockStateProperties.NORTH_REDSTONE, BlockProperties.NORTH_REDSTONE); ++ registerProp(BlockStateProperties.SOUTH, BlockProperties.SOUTH); ++ registerProp(BlockStateProperties.SOUTH_WALL, BlockProperties.SOUTH_WALL); ++ registerProp(BlockStateProperties.SOUTH_REDSTONE, BlockProperties.SOUTH_REDSTONE); ++ registerProp(BlockStateProperties.RAIL_SHAPE, BlockProperties.RAIL_SHAPE); ++ registerProp(BlockStateProperties.RAIL_SHAPE_STRAIGHT, BlockProperties.RAIL_SHAPE_STRAIGHT); ++ registerProp(BlockStateProperties.STAIRS_SHAPE, BlockProperties.STAIRS_SHAPE); ++ registerProp(BlockStateProperties.LEVEL_CAULDRON, BlockProperties.LEVEL_CAULDRON); ++ registerProp(BlockStateProperties.LEVEL_COMPOSTER, BlockProperties.LEVEL_COMPOSTER); ++ registerProp(BlockStateProperties.LEVEL_FLOWING, BlockProperties.LEVEL_FLOWING); ++ registerProp(BlockStateProperties.LEVEL, BlockProperties.LEVEL); ++ registerProp(BlockStateProperties.DISTANCE, BlockProperties.DISTANCE); ++ registerProp(BlockStateProperties.STABILITY_DISTANCE, BlockProperties.STABILITY_DISTANCE); ++ registerProp(BlockStateProperties.FACING, BlockProperties.FACING); ++ registerProp(BlockStateProperties.FACING_HOPPER, BlockProperties.FACING_HOPPER); ++ registerProp(BlockStateProperties.HORIZONTAL_FACING, BlockProperties.HORIZONTAL_FACING); ++ registerProp(BlockStateProperties.AGE_1, BlockProperties.AGE_1); ++ registerProp(BlockStateProperties.AGE_2, BlockProperties.AGE_2); ++ registerProp(BlockStateProperties.AGE_3, BlockProperties.AGE_3); ++ registerProp(BlockStateProperties.AGE_4, BlockProperties.AGE_4); ++ registerProp(BlockStateProperties.AGE_5, BlockProperties.AGE_5); ++ registerProp(BlockStateProperties.AGE_7, BlockProperties.AGE_7); ++ registerProp(BlockStateProperties.AGE_15, BlockProperties.AGE_15); ++ registerProp(BlockStateProperties.AGE_25, BlockProperties.AGE_25); ++ // ++ } ++ ++ private static void registerProp(final Property nmsProperty, final BlockProperty paperProperty) { ++ CraftBlockData.DATA_PROPERTY_CACHE_MAP.put(nmsProperty, paperProperty); ++ } ++ ++ ++ public static BlockProperty convertToPaperProperty(final Property nmsProperty) { ++ return CraftBlockData.DATA_PROPERTY_CACHE_MAP.computeIfAbsent(nmsProperty, prop -> { ++ final Collection> properties = BlockProperties.PROPERTIES.get(prop.getName()); ++ if (properties.size() == 1) { ++ return properties.iterator().next(); ++ } else { ++ throw new IllegalArgumentException(nmsProperty + " should already be present in DATA_PROPERTY_CACHE_MAP"); ++ } ++ }); ++ } ++ ++ public static Property convertToNmsProperty(final BlockProperty paperProperty) { ++ return CraftBlockData.DATA_PROPERTY_CACHE_MAP.inverse().computeIfAbsent(paperProperty, prop -> { ++ final Collection> properties = Property.PROPERTY_MULTIMAP.get(prop.name()); ++ if (properties.size() == 1) { ++ return properties.iterator().next(); ++ } else { ++ throw new IllegalArgumentException(paperProperty + " should already be present in DATA_PROPERTY_CACHE_MAP"); ++ } ++ }); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/PaperBlockPropertyHolder.java b/src/main/java/io/papermc/paper/block/property/PaperBlockPropertyHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2d06aa85b261c964908f5d778a429e28ebcc34dd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/PaperBlockPropertyHolder.java +@@ -0,0 +1,107 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.base.Preconditions; ++import java.util.Collection; ++import java.util.Collections; ++import net.minecraft.util.StringRepresentable; ++import net.minecraft.world.level.block.state.StateDefinition; ++import net.minecraft.world.level.block.state.StateHolder; ++import net.minecraft.world.level.block.state.properties.BooleanProperty; ++import net.minecraft.world.level.block.state.properties.EnumProperty; ++import net.minecraft.world.level.block.state.properties.IntegerProperty; ++import net.minecraft.world.level.block.state.properties.Property; ++import org.jspecify.annotations.Nullable; ++ ++public interface PaperBlockPropertyHolder> extends BlockPropertyHolder { ++ ++ StateHolder getState(); ++ ++ StateDefinition getStateDefinition(); ++ ++ > T get(Property ibs); ++ ++ > B get(EnumProperty nms, Class bukkit); ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ default > @Nullable BlockProperty getProperty(final String propertyName) { ++ final Property nmsProperty = this.getStateDefinition().getProperty(propertyName); ++ if (nmsProperty != null) { ++ return (BlockProperty) PaperBlockProperties.convertToPaperProperty(nmsProperty); ++ } ++ return null; ++ } ++ ++ @Override ++ default > boolean hasProperty(final BlockProperty property) { ++ return this.getState().hasProperty(PaperBlockProperties.convertToNmsProperty(property)); ++ } ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ default > T getValue(final BlockProperty property) { ++ final Property nmsProperty = PaperBlockProperties.convertToNmsProperty(property); ++ Preconditions.checkArgument(this.getState().hasProperty(nmsProperty), property.name() + " is not present on " + this); ++ switch (nmsProperty) { ++ case final IntegerProperty nmsIntProperty -> { ++ final T value; ++ if (property instanceof final AsIntegerProperty intRepresented) { ++ value = ((AsIntegerProperty) intRepresented).fromIntValue(this.get(nmsIntProperty)); ++ } else { ++ value = this.get((Property) nmsIntProperty); ++ } ++ return value; ++ } ++ case final BooleanProperty ignored -> { ++ return this.get((Property) nmsProperty); ++ } ++ case final EnumProperty enumProperty when property instanceof final EnumBlockProperty enumDataProperty -> { ++ return (T) this.get(enumProperty, enumDataProperty.type()); ++ } ++ default -> throw new IllegalArgumentException("Did not recognize " + property + " and " + nmsProperty); ++ } ++ } ++ ++ @Override ++ default > java.util.Optional getOptionalValue(final BlockProperty property) { ++ if (!this.hasProperty(property)) { ++ return java.util.Optional.empty(); ++ } else { ++ return java.util.Optional.of(this.getValue(property)); ++ } ++ } ++ ++ @Override ++ default Collection> getProperties() { ++ return Collections.unmodifiableCollection(com.google.common.collect.Collections2.transform(this.getState().getProperties(), PaperBlockProperties::convertToPaperProperty)); ++ } ++ ++ interface PaperMutable> extends PaperBlockPropertyHolder, BlockPropertyHolder.Mutable { ++ ++ , V extends T> void set(Property nmsProperty, V value); ++ ++ , N extends Enum & StringRepresentable> void set(EnumProperty nms, Enum bukkit); ++ ++ @Override ++ default > void setValue(final BlockProperty property, final T value) { ++ Preconditions.checkNotNull(value, "Cannot set a data property to null"); ++ final Property nmsProperty = PaperBlockProperties.convertToNmsProperty(property); ++ Preconditions.checkArgument(this.getState().hasProperty(nmsProperty), property.name() + " is not present on " + this); ++ switch (nmsProperty) { ++ case final IntegerProperty nmsIntProperty -> { ++ final int intValue; ++ if (property instanceof final AsIntegerProperty intRepresented) { ++ intValue = intRepresented.toIntValue(value); ++ } else { ++ intValue = (Integer) value; ++ } ++ this.set(nmsIntProperty, intValue); ++ } ++ case final BooleanProperty nmsBoolProperty -> this.set(nmsBoolProperty, (Boolean) value); ++ case final EnumProperty enumProperty when value instanceof final Enum enumValue -> this.set(enumProperty, enumValue); ++ default -> ++ throw new IllegalArgumentException("Did not recognize " + property + " with value " + value + " (" + value.getClass().getSimpleName() + ") for " + nmsProperty); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/property/package-info.java b/src/main/java/io/papermc/paper/block/property/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..359cc04d2e8d6f7eb2ea373e19aff4339f0a431c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/package-info.java +@@ -0,0 +1,4 @@ ++@NullMarked ++package io.papermc.paper.block.property; ++ ++import org.jspecify.annotations.NullMarked; +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index 0b116160924300a9d62ad5948bfaf276f0386e4d..50c34f79ab957830dfadb6bc98956471701e21d1 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -51,11 +51,13 @@ public abstract class Property> implements ca.spottedlea + @Override + public abstract int moonrise$getIdFor(final T value); + // Paper end - optimise blockstate property access ++ public static final com.google.common.collect.Multimap> PROPERTY_MULTIMAP = com.google.common.collect.Multimaps.newSetMultimap(new java.util.HashMap<>(), com.google.common.collect.Sets::newIdentityHashSet); // Paper - property API + + protected Property(String name, Class type) { + this.clazz = type; + this.name = name; + this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access ++ PROPERTY_MULTIMAP.put(this.name, this); // Paper - property API + } + + public Property.Value value(T value) { +diff --git a/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java b/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java +index 50fb7edd25c1b38f5c463b78d21d4583bdc89229..62f514601de2d0d103a5e9766d8e504857cd8086 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java +@@ -48,7 +48,7 @@ import org.bukkit.craftbukkit.util.CraftLocation; + import org.bukkit.inventory.ItemStack; + import org.jetbrains.annotations.NotNull; + +-public class CraftBlockData implements BlockData { ++public class CraftBlockData implements BlockData, io.papermc.paper.block.property.PaperBlockPropertyHolder.PaperMutable { // Paper + + private net.minecraft.world.level.block.state.BlockState state; + private Map, Comparable> parsedStates; +@@ -78,7 +78,7 @@ public class CraftBlockData implements BlockData { + * @param the type + * @return the matching Bukkit type + */ +- protected > B get(EnumProperty nms, Class bukkit) { ++ public > B get(EnumProperty nms, Class bukkit) { // Paper + return CraftBlockData.toBukkit(this.state.getValue(nms), bukkit); + } + +@@ -110,7 +110,7 @@ public class CraftBlockData implements BlockData { + * @param the Bukkit type + * @param the NMS type + */ +- protected , N extends Enum & StringRepresentable> void set(EnumProperty nms, Enum bukkit) { ++ public , N extends Enum & StringRepresentable> void set(EnumProperty nms, Enum bukkit) { // Paper + this.parsedStates = null; + this.state = this.state.setValue(nms, CraftBlockData.toNMS(bukkit, nms.getValueClass())); + } +@@ -165,7 +165,7 @@ public class CraftBlockData implements BlockData { + * @throws IllegalStateException if the Enum could not be converted + */ + @SuppressWarnings("unchecked") +- private static > B toBukkit(Enum nms, Class bukkit) { ++ public static > B toBukkit(Enum nms, Class bukkit) { // Paper - private -> public + if (nms instanceof Direction) { + return (B) CraftBlock.notchToBlockFace((Direction) nms); + } +@@ -195,7 +195,7 @@ public class CraftBlockData implements BlockData { + * @param the type + * @return the current value of the given state + */ +- protected > T get(Property ibs) { ++ public > T get(Property ibs) { // Paper + // Straight integer or boolean getter + return this.state.getValue(ibs); + } +@@ -358,6 +358,7 @@ public class CraftBlockData implements BlockData { + + // + private static final Map, Function> MAP = new HashMap<>(); ++ public static final com.google.common.collect.BiMap, io.papermc.paper.block.property.BlockProperty> DATA_PROPERTY_CACHE_MAP = com.google.common.collect.HashBiMap.create(); // Paper + + static { + // +@@ -537,8 +538,16 @@ public class CraftBlockData implements BlockData { + register(net.minecraft.world.level.block.piston.PistonHeadBlock.class, org.bukkit.craftbukkit.block.impl.CraftPistonExtension::new); + register(net.minecraft.world.level.block.piston.MovingPistonBlock.class, org.bukkit.craftbukkit.block.impl.CraftPistonMoving::new); + // ++ io.papermc.paper.block.property.PaperBlockProperties.setup(); // Paper + } + ++ // Paper start - block property API ++ @Override ++ public net.minecraft.world.level.block.state.StateDefinition getStateDefinition() { ++ return this.getState().getBlock().getStateDefinition(); ++ } ++ // Paper end - block property API ++ + private static void register(Class nms, Function bukkit) { + Preconditions.checkState(CraftBlockData.MAP.put(nms, bukkit) == null, "Duplicate mapping %s->%s", nms, bukkit); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 507f908916cbeb592496f963b46e4c2121a7b5e3..cfebd80f87ef5a52bd5b6af36ef428a92538015f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -702,6 +702,18 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end - proxy ItemStack + ++ // Paper start - block property API ++ @SuppressWarnings({"unchecked", "rawtypes"}) ++ @Override ++ public > String getPropertyEnumName(io.papermc.paper.block.property.EnumBlockProperty enumProperty, B bukkitEnum) { ++ final net.minecraft.world.level.block.state.properties.Property nmsProperty = io.papermc.paper.block.property.PaperBlockProperties.convertToNmsProperty(enumProperty); ++ if (!(nmsProperty instanceof net.minecraft.world.level.block.state.properties.EnumProperty nmsEnumProperty)) { ++ throw new IllegalArgumentException("Could not convert " + enumProperty + " to an nms EnumProperty"); ++ } ++ return nmsEnumProperty.getName(CraftBlockData.toNMS(bukkitEnum, nmsEnumProperty.getValueClass())); ++ } ++ // Paper end - block property API ++ + /** + * This helper class represents the different NBT Tags. + *

+diff --git a/src/test/java/io/papermc/paper/block/property/BlockPropertyTest.java b/src/test/java/io/papermc/paper/block/property/BlockPropertyTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..22d1c3f2815c4091432a72654daca7fde3c45c55 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/block/property/BlockPropertyTest.java +@@ -0,0 +1,226 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.collect.ImmutableMap; ++import java.io.PrintStream; ++import java.lang.reflect.Field; ++import java.lang.reflect.Modifier; ++import java.util.ArrayList; ++import java.util.LinkedHashMap; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.core.Direction; ++import net.minecraft.core.FrontAndTop; ++import net.minecraft.util.StringRepresentable; ++import net.minecraft.world.level.block.CreakingHeartBlock; ++import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerState; ++import net.minecraft.world.level.block.entity.vault.VaultState; ++import net.minecraft.world.level.block.state.properties.AttachFace; ++import net.minecraft.world.level.block.state.properties.BambooLeaves; ++import net.minecraft.world.level.block.state.properties.BedPart; ++import net.minecraft.world.level.block.state.properties.BellAttachType; ++import net.minecraft.world.level.block.state.properties.BlockStateProperties; ++import net.minecraft.world.level.block.state.properties.BooleanProperty; ++import net.minecraft.world.level.block.state.properties.ChestType; ++import net.minecraft.world.level.block.state.properties.ComparatorMode; ++import net.minecraft.world.level.block.state.properties.DoorHingeSide; ++import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; ++import net.minecraft.world.level.block.state.properties.DripstoneThickness; ++import net.minecraft.world.level.block.state.properties.EnumProperty; ++import net.minecraft.world.level.block.state.properties.Half; ++import net.minecraft.world.level.block.state.properties.IntegerProperty; ++import net.minecraft.world.level.block.state.properties.NoteBlockInstrument; ++import net.minecraft.world.level.block.state.properties.PistonType; ++import net.minecraft.world.level.block.state.properties.Property; ++import net.minecraft.world.level.block.state.properties.RailShape; ++import net.minecraft.world.level.block.state.properties.RedstoneSide; ++import net.minecraft.world.level.block.state.properties.SculkSensorPhase; ++import net.minecraft.world.level.block.state.properties.SlabType; ++import net.minecraft.world.level.block.state.properties.StairsShape; ++import net.minecraft.world.level.block.state.properties.StructureMode; ++import net.minecraft.world.level.block.state.properties.Tilt; ++import net.minecraft.world.level.block.state.properties.WallSide; ++import org.bukkit.Axis; ++import org.bukkit.Instrument; ++import org.bukkit.block.BlockFace; ++import org.bukkit.block.data.Bisected; ++import org.bukkit.block.data.FaceAttachable; ++import org.bukkit.block.data.Rail; ++import org.bukkit.block.data.type.Bamboo; ++import org.bukkit.block.data.type.Bed; ++import org.bukkit.block.data.type.Bell; ++import org.bukkit.block.data.type.BigDripleaf; ++import org.bukkit.block.data.type.Chest; ++import org.bukkit.block.data.type.Comparator; ++import org.bukkit.block.data.type.CreakingHeart; ++import org.bukkit.block.data.type.Door; ++import org.bukkit.block.data.type.Jigsaw; ++import org.bukkit.block.data.type.PointedDripstone; ++import org.bukkit.block.data.type.RedstoneWire; ++import org.bukkit.block.data.type.SculkSensor; ++import org.bukkit.block.data.type.Slab; ++import org.bukkit.block.data.type.Stairs; ++import org.bukkit.block.data.type.StructureBlock; ++import org.bukkit.block.data.type.TechnicalPiston; ++import org.bukkit.block.data.type.TrialSpawner; ++import org.bukkit.block.data.type.Vault; ++import org.bukkit.block.data.type.Wall; ++import org.bukkit.support.environment.AllFeatures; ++import org.junit.jupiter.api.AfterEach; ++import org.junit.jupiter.api.BeforeEach; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertFalse; ++import static org.junit.jupiter.api.Assertions.assertNotNull; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++import static org.junit.jupiter.api.Assertions.fail; ++import static org.junit.jupiter.api.Assumptions.assumeTrue; ++ ++@AllFeatures ++public class BlockPropertyTest { ++ ++ private static final Map>, Class>> ENUM_MAPPING = ImmutableMap.>, Class>>builder() ++ .put(DoorHingeSide.class, Door.Hinge.class) ++ .put(SlabType.class, Slab.Type.class) ++ .put(StructureMode.class, StructureBlock.Mode.class) ++ .put(FrontAndTop.class, Jigsaw.Orientation.class) ++ .put(DripstoneThickness.class, PointedDripstone.Thickness.class) ++ .put(WallSide.class, Wall.Height.class) ++ .put(BellAttachType.class, Bell.Attachment.class) ++ .put(NoteBlockInstrument.class, Instrument.class) ++ .put(StairsShape.class, Stairs.Shape.class) ++ .put(Direction.class, BlockFace.class) ++ .put(ComparatorMode.class, Comparator.Mode.class) ++ .put(PistonType.class, TechnicalPiston.Type.class) ++ .put(BedPart.class, Bed.Part.class) ++ .put(Half.class, Bisected.Half.class) ++ .put(AttachFace.class, FaceAttachable.AttachedFace.class) ++ .put(RailShape.class, Rail.Shape.class) ++ .put(SculkSensorPhase.class, SculkSensor.Phase.class) ++ .put(DoubleBlockHalf.class, Bisected.Half.class) ++ .put(Tilt.class, BigDripleaf.Tilt.class) ++ .put(ChestType.class, Chest.Type.class) ++ .put(RedstoneSide.class, RedstoneWire.Connection.class) ++ .put(Direction.Axis.class, Axis.class) ++ .put(BambooLeaves.class, Bamboo.Leaves.class) ++ .put(TrialSpawnerState.class, TrialSpawner.State.class) ++ .put(VaultState.class, Vault.State.class) ++ .put(CreakingHeartBlock.CreakingHeartState.class, CreakingHeart.Creaking.class) ++ .build(); ++ ++ private PrintStream old; ++ @BeforeEach ++ public void beforeEach() { ++ old = System.out; ++ } ++ ++ @AfterEach ++ public void afterEach() { ++ System.setOut(this.old); ++ } ++ ++ @SuppressWarnings("rawtypes") ++ @Test ++ public void testEnumMapping() throws IllegalAccessException { ++ final Map nmsPropertyMap = collectProperties(BlockStateProperties.class, EnumProperty.class); ++ for (final EnumProperty value : nmsPropertyMap.values()) { ++ assertNotNull(ENUM_MAPPING.get(value.getValueClass()), "Missing enum mapping for " + value.getValueClass()); ++ } ++ } ++ ++ @SuppressWarnings("rawtypes") ++ @Test ++ public void validateProperties() throws IllegalAccessException { ++ final Map nmsPropertyMap = collectProperties(BlockStateProperties.class, Property.class); ++ final Map paperPropertyMap = collectProperties(BlockProperties.class, BlockProperty.class); ++ final List missing = new ArrayList<>(); ++ final List invalid = new ArrayList<>(); ++ nmsPropertyMap.forEach((name, prop) -> { ++ if (paperPropertyMap.containsKey(name)) { ++ if (!isEqual(prop, paperPropertyMap.get(name))) { ++ invalid.add(name + ": \n\t" + paperPropertyMap.get(name) + "\n\t" + prop); ++ } ++ paperPropertyMap.remove(name); ++ } else { ++ missing.add(stringifyPropertyField(name, prop)); ++ } ++ }); ++ ++ assertEquals(0, invalid.size(), "Invalid Property: \n" + String.join("\n", invalid) + "\n"); ++ assertEquals(0, paperPropertyMap.size(), "Extra Property: \n" + String.join("\n", paperPropertyMap.keySet()) + "\n"); ++ assertEquals(0, missing.size(), "Missing Property: \n" + String.join("\n", missing) + "\n"); ++ } ++ ++ ++ @Test ++ public void testToNmsPropertyConversion() { ++ assertFalse(BlockProperties.PROPERTIES.isEmpty(), "no paper properties found"); ++ for (final BlockProperty property : BlockProperties.PROPERTIES.values()) { ++ final Property nmsProperty = PaperBlockProperties.convertToNmsProperty(property); ++ assertNotNull(nmsProperty, "Could not convert " + property + " to its nms counterpart"); ++ assertTrue(isEqual(nmsProperty, property), property.name() + " is not equal to " + nmsProperty.getName()); ++ } ++ } ++ ++ @Test ++ public void testToPaperPropertyConversion() { ++ assertFalse(Property.PROPERTY_MULTIMAP.isEmpty(), "no nms properties found"); ++ for (final Property nmsProp : Property.PROPERTY_MULTIMAP.values()) { ++ final BlockProperty paperProp = PaperBlockProperties.convertToPaperProperty(nmsProp); ++ assertNotNull(paperProp, "Could not convert " + nmsProp + " to its paper counterpart"); ++ assertTrue(isEqual(nmsProp, paperProp), nmsProp.getName() + " is not equal to " + paperProp.name()); ++ } ++ } ++ ++ private static boolean isEqual(final Property nmsProperty, final BlockProperty property) { ++ return switch (property) { ++ // special cases ++ case final RotationBlockProperty rotation when nmsProperty instanceof IntegerProperty && "rotation".equals(nmsProperty.getName()) -> ++ nmsProperty.getPossibleValues().size() == rotation.values().size(); ++ case final NoteBlockProperty note when nmsProperty instanceof IntegerProperty && "note".equals(nmsProperty.getName()) -> ++ nmsProperty.getPossibleValues().size() == note.values().size(); ++ // end special cases ++ case final BooleanBlockProperty ignored when nmsProperty instanceof BooleanProperty -> true; ++ case final IntegerBlockProperty apiIntProp when nmsProperty instanceof final IntegerProperty nmsIntProp -> ++ nmsIntProp.min == apiIntProp.min() && nmsIntProp.max == apiIntProp.max() && nmsIntProp.getPossibleValues().size() == apiIntProp.values().size(); ++ case final EnumBlockProperty apiEnumProp when nmsProperty instanceof final EnumProperty nmsEnumProp -> ++ ENUM_MAPPING.get(nmsEnumProp.getValueClass()) == apiEnumProp.type() && nmsEnumProp.getPossibleValues().size() == apiEnumProp.values().size(); ++ default -> false; ++ }; ++ } ++ ++ private static String stringifyPropertyField(final String fieldName, final Property property) { ++ if (property instanceof final IntegerProperty intProp) { ++ // special cases ++ if (property == BlockStateProperties.ROTATION_16) { /** see {@link RotationBlockProperty} */ ++ return "public static final EnumBlockProperty " + fieldName + " = new RotationBlockProperty(\"" + property.getName() + "\");"; ++ } else if (property == BlockStateProperties.NOTE) { /** see {@link NoteBlockProperty} */ ++ return "public static final BlockProperty " + fieldName + " = new NoteBlockProperty(\"" + property.getName() + "\");"; ++ } ++ // end special cases ++ return "public static final IntegerBlockProperty " + fieldName + " = integer(\"" + property.getName() + "\", " + intProp.min + ", " + intProp.max + ");"; ++ } else if (property instanceof net.minecraft.world.level.block.state.properties.BooleanProperty) { ++ return "public static final BooleanBlockProperty " + fieldName + " = bool(\"" + property.getName() + "\");"; ++ } else if (property instanceof final net.minecraft.world.level.block.state.properties.EnumProperty enumProp) { ++ final String value; ++ assumeTrue(ENUM_MAPPING.containsKey(enumProp.getValueClass()), "Missing enum mappings!"); ++ final Class> bukkitEnum = ENUM_MAPPING.get(enumProp.getValueClass()); ++ final String name = (bukkitEnum.isMemberClass() ? bukkitEnum.getEnclosingClass().getSimpleName() + "." : "") + bukkitEnum.getSimpleName(); ++ value = "public static final EnumBlockProperty<" + name + "> " + fieldName + " = enumeration(\"" + property.getName() + "\", " + name + ".class);"; ++ return value; ++ } ++ fail(property + " is not a recognized property type"); ++ return ""; ++ } ++ ++ private static

Map collectProperties(final Class containerClass, final Class

propertyClass) throws IllegalAccessException { ++ final Map propertyMap = new LinkedHashMap<>(); ++ for (final Field field : containerClass.getFields()) { ++ if (!Modifier.isStatic(field.getModifiers()) || !propertyClass.isAssignableFrom(field.getType())) { ++ continue; ++ } ++ propertyMap.put(field.getName(), propertyClass.cast(field.get(null))); ++ } ++ return propertyMap; ++ } ++}