diff --git a/patches/api/0466-BlockProperty-API.patch b/patches/api/0466-BlockProperty-API.patch new file mode 100644 index 000000000..740875338 --- /dev/null +++ b/patches/api/0466-BlockProperty-API.patch @@ -0,0 +1,1018 @@ +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 913acd58547d97cafc1466f6e2b3b4d22cf0b02d..6413ed69e7bce467ab1f575362728cc136e2e61c 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; +@@ -10,7 +11,7 @@ import org.jetbrains.annotations.Range; + * A representation of a fluid in a specific state of data. + * This type is not linked to a specific location and hence mostly resembles a {@link org.bukkit.block.data.BlockData}. + */ +-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/AbstractBlockProperty.java b/src/main/java/io/papermc/paper/block/property/AbstractBlockProperty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d750e2a1ab6002dc26c6f1a322771a162eb11029 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/AbstractBlockProperty.java +@@ -0,0 +1,49 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.base.MoreObjects; ++import java.util.Locale; ++import org.jetbrains.annotations.NotNull; ++ ++abstract sealed class AbstractBlockProperty> implements BlockProperty permits BooleanBlockProperty, IntegerBlockProperty, EnumBlockProperty, NoteBlockProperty { ++ ++ static final ExceptionCreator EXCEPTION_CREATOR = (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)); ++ ++ private final String name; ++ private final Class type; ++ ++ protected AbstractBlockProperty(final @NotNull String name, final @NotNull Class type) { ++ this.name = name; ++ this.type = type; ++ BlockProperties.PROPERTIES.put(name, this); ++ } ++ ++ @Override ++ public final @NotNull String name() { ++ return this.name; ++ } ++ ++ @Override ++ public final @NotNull Class type() { ++ return this.type; ++ } ++ ++ @Override ++ public String toString() { ++ return MoreObjects.toStringHelper(this) ++ .add("name", this.name) ++ .add("type", this.type) ++ .add("values", this.values()) ++ .toString(); ++ } ++ ++ @FunctionalInterface ++ interface ExceptionCreator { ++ ++ @NotNull IllegalArgumentException create(@NotNull Object value, @NotNull Type type, @NotNull BlockProperty property); ++ ++ enum Type { ++ NAME, ++ 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..25dd511098efea9ec683e6e018c5c6f21ea1466a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BlockProperties.java +@@ -0,0 +1,182 @@ ++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.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.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 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 = 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 = 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); ++ ++ private BlockProperties() { ++ } ++ ++ // ++ private static IntegerBlockProperty integer(final String name, final int min, final int max) { ++ return new IntegerBlockProperty(name, min, max); ++ } ++ ++ private static BooleanBlockProperty bool(final String name) { ++ return new BooleanBlockProperty(name); ++ } ++ ++ private static > EnumBlockProperty enumeration(final String name, final Class enumClass) { ++ return new EnumBlockProperty<>(name, enumClass, Set.of(enumClass.getEnumConstants())); ++ } ++ ++ @SuppressWarnings("SameParameterValue") ++ @SafeVarargs ++ private static > EnumBlockProperty enumeration(final String name, final Class enumClass, final E... values) { ++ return new EnumBlockProperty<>(name, enumClass, Set.of(values)); ++ } ++ ++ private static > EnumBlockProperty enumeration(final String name, final Class enumClass, final Predicate test) { ++ return new EnumBlockProperty<>(name, enumClass, Arrays.stream(enumClass.getEnumConstants()).filter(test).collect(Collectors.toSet())); ++ } ++ // ++} +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..ac300823fb9b4dae329d59692c2a8b217e5dc81f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BlockProperty.java +@@ -0,0 +1,136 @@ ++package io.papermc.paper.block.property; ++ ++import java.util.Optional; ++import java.util.Set; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public sealed interface BlockProperty> permits AbstractBlockProperty { ++ ++ /** ++ * Gets the name of this property. ++ * ++ * @return the name ++ */ ++ @NotNull String name(); ++ ++ /** ++ * Gets the value type of this property. ++ * ++ * @return the value type ++ */ ++ @NotNull 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) ++ */ ++ @NotNull String name(@NotNull 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(@NotNull 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) ++ */ ++ @NotNull T value(@NotNull String name); ++ ++ /** ++ * Checks if the object is a valid value for this property. ++ * ++ * @param object the object to check ++ * @return true if its a valid value ++ * @see #setValue(BlockPropertyHolder.Mutable, Object) ++ */ ++ boolean isValue(@NotNull Object object); ++ ++ /** ++ * Gets an immutable collection of possible values for this property. ++ * ++ * @return an immutable collection of values ++ */ ++ @Unmodifiable @NotNull Set<@NotNull T> 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 @NotNull 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 @NotNull T getValue(final @NotNull 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 @NotNull Optional getOptionalValue(final @NotNull 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 @NotNull BlockPropertyHolder.Mutable holder, final @NotNull 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 @NotNull BlockPropertyHolder.Mutable holder, final @NotNull Object value) { ++ if (!this.isValue(value)) { ++ throw AbstractBlockProperty.EXCEPTION_CREATOR.create(value, AbstractBlockProperty.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..988aa5d724226522424709486149020e0fe67f4b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BlockPropertyHolder.java +@@ -0,0 +1,94 @@ ++package io.papermc.paper.block.property; ++ ++import java.util.Collection; ++import java.util.Optional; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++/** ++ * Represents an object that holds {@link BlockProperty}s. ++ */ ++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(@NotNull 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(@NotNull 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) ++ */ ++ > @NotNull T getValue(@NotNull 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) ++ */ ++ > @NotNull Optional getOptionalValue(@NotNull BlockProperty property); ++ ++ /** ++ * Get all properties present on this. ++ * ++ * @return an unmodifiable collection of properties ++ */ ++ @NotNull @Unmodifiable Collection> getProperties(); ++ ++ /** ++ * Represents an object that holds {@link BlockProperty}s that can be changed. ++ */ ++ 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(@NotNull BlockProperty property, @NotNull 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 @NotNull BlockProperty property, final @NotNull 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..e5250229da138cf3aba8bfa493ef0bb71dc6c4e6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/BooleanBlockProperty.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.block.property; ++ ++import it.unimi.dsi.fastutil.booleans.BooleanSet; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public final class BooleanBlockProperty extends AbstractBlockProperty { ++ ++ private static final BooleanSet VALUES = BooleanSet.of(true, false); ++ ++ BooleanBlockProperty(final String name) { ++ super(name, Boolean.class); ++ } ++ ++ @Override ++ public @NotNull String name(final @NotNull Boolean value) { ++ return value.toString(); ++ } ++ ++ @Override ++ public boolean isValidName(final @NotNull String name) { ++ return "true".equals(name) || "false".equals(name); ++ } ++ ++ @Override ++ public @NotNull Boolean value(final @NotNull String name) { ++ return switch (name) { ++ case "true" -> true; ++ case "false" -> false; ++ default -> throw EXCEPTION_CREATOR.create(name, ExceptionCreator.Type.NAME, this); ++ }; ++ } ++ ++ @Override ++ public boolean isValue(final @NotNull Object object) { ++ return object instanceof Boolean; ++ } ++ ++ @Override ++ public @Unmodifiable @NotNull 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..2568ed7e3058a838804da49969e71123b9e6d3b2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/EnumBlockProperty.java +@@ -0,0 +1,114 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.collect.BiMap; ++import com.google.common.collect.HashBiMap; ++import com.google.common.collect.Sets; ++import java.util.Collection; ++import java.util.List; ++import java.util.Set; ++import java.util.function.ToIntFunction; ++import net.kyori.adventure.util.Index; ++import org.bukkit.Bukkit; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public sealed class EnumBlockProperty> extends AbstractBlockProperty permits EnumBlockProperty.EnumIntRepresented { ++ ++ private final Set values; ++ private @Nullable Index byName; // needs lazy init due to static circular dependencies ++ ++ EnumBlockProperty(final String name, final Class type, final Collection values) { ++ super(name, type); ++ this.values = Sets.immutableEnumSet(values); ++ } ++ ++ private @NotNull Index byNameIndex() { ++ if (this.byName == null) { ++ this.byName = this.createByNameIndex(); ++ } ++ return this.byName; ++ } ++ ++ @SuppressWarnings("deprecation") // valid unsafe use ++ protected @NotNull Index createByNameIndex() { ++ return Index.create(e -> Bukkit.getUnsafe().getPropertyEnumName(this, e), List.copyOf(this.values)); ++ } ++ ++ @Override ++ public @NotNull String name(final @NotNull E value) { ++ final String valueName = this.byNameIndex().key(value); ++ if (valueName == null) { ++ throw EXCEPTION_CREATOR.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return valueName; ++ } ++ ++ @Override ++ public boolean isValidName(final @NotNull String name) { ++ return this.byNameIndex().value(name) != null; ++ } ++ ++ @Override ++ public @NotNull E value(final @NotNull String name) { ++ final E value = this.byNameIndex().value(name); ++ if (value == null) { ++ throw EXCEPTION_CREATOR.create(name, ExceptionCreator.Type.NAME, this); ++ } ++ return value; ++ } ++ ++ @Override ++ public boolean isValue(final @NotNull Object object) { ++ return this.type().isInstance(object) && this.values.contains(object); ++ } ++ ++ @Override ++ public @Unmodifiable @NotNull Set values() { ++ return this.values; ++ } ++ ++ /** ++ * Represents a property whose value is stored as an int, ++ * but represented as an enum ++ * ++ * @param enum type ++ */ ++ static sealed class EnumIntRepresented> extends EnumBlockProperty implements IntegerBlockProperty.IntRepresented permits RotationBlockProperty { ++ ++ private final BiMap cache; ++ ++ EnumIntRepresented(final String name, final Class type, final Collection values) { ++ this(name, type, values, Enum::ordinal); ++ } ++ ++ EnumIntRepresented(final String name, final Class type, final Collection values, final ToIntFunction toIntFunction) { ++ super(name, type, values); ++ this.cache = HashBiMap.create(values.size()); ++ for (final E value : this.values()) { ++ this.cache.put(value, toIntFunction.applyAsInt(value)); ++ } ++ } ++ ++ @Override ++ protected @NotNull Index createByNameIndex() { ++ return Index.create(v -> Integer.toString(this.toIntValue(v)), List.copyOf(this.cache.keySet())); ++ } ++ ++ @Override ++ public int toIntValue(final @NotNull E value) { ++ if (!this.cache.containsKey(value)) { ++ throw EXCEPTION_CREATOR.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return this.cache.get(value); ++ } ++ ++ @Override ++ public @NotNull E fromIntValue(final int value) { ++ if (!this.cache.inverse().containsKey(value)) { ++ throw EXCEPTION_CREATOR.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return this.cache.inverse().get(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..368708a0e5e64547130045fa3b033d467b451121 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/IntegerBlockProperty.java +@@ -0,0 +1,104 @@ ++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; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public final class IntegerBlockProperty extends AbstractBlockProperty { ++ ++ private final IntSet values; ++ private final int min; ++ private final int max; ++ ++ IntegerBlockProperty(final String name, final int min, final int max) { ++ super(name, Integer.class); ++ this.min = min; ++ this.max = 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); ++ } ++ this.values = IntSets.unmodifiable(set); // use unmodifiable to preserve order (but in reality its immutable) ++ } ++ ++ /** ++ * Gets the min value for this property. ++ * ++ * @return the min value ++ */ ++ public int min() { ++ return this.min; ++ } ++ ++ /** ++ * Gets the max value for this property. ++ * ++ * @return the max value ++ */ ++ public int max() { ++ return this.max; ++ } ++ ++ @Override ++ public @NotNull String name(final @NotNull Integer value) { ++ if (value > this.max || value < this.min) { ++ throw EXCEPTION_CREATOR.create(value, ExceptionCreator.Type.VALUE, this); ++ } ++ return value.toString(); ++ } ++ ++ @Override ++ public boolean isValidName(final @NotNull String name) { ++ try { ++ final int value = Integer.parseInt(name); ++ if (this.values.contains(value)) { ++ return true; ++ } ++ } catch (final NumberFormatException ignored) { ++ } ++ return false; ++ } ++ ++ @Override ++ public @NotNull Integer value(final @NotNull String name) { ++ try { ++ final int value = Integer.parseInt(name); ++ if (this.values.contains(value)) { ++ return value; ++ } ++ throw EXCEPTION_CREATOR.create(name, ExceptionCreator.Type.NAME, this); ++ } catch (final NumberFormatException exception) { ++ throw EXCEPTION_CREATOR.create(name, ExceptionCreator.Type.NAME, this); ++ } ++ } ++ ++ @Override ++ public boolean isValue(final @NotNull Object object) { ++ return object instanceof final Integer num && num >= this.min && num <= this.max; ++ } ++ ++ @Override ++ public @Unmodifiable @NotNull IntSet values() { ++ return this.values; ++ } ++ ++ /** ++ * Represents properties that are represented in the API as non-integers ++ * but stored as integers ++ * ++ * @param type ++ */ ++ @ApiStatus.Internal ++ public sealed interface IntRepresented permits EnumBlockProperty.EnumIntRepresented, NoteBlockProperty { ++ ++ int toIntValue(@NotNull T value); ++ ++ @NotNull T fromIntValue(int value); ++ } ++} +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..ecd0aff95f1b10eceb1b283cb5f5cf92d204e8dd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/NoteBlockProperty.java +@@ -0,0 +1,72 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.collect.BiMap; ++import com.google.common.collect.HashBiMap; ++import java.util.Collections; ++import java.util.Set; ++import org.bukkit.Note; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Unmodifiable; ++ ++final class NoteBlockProperty extends AbstractBlockProperty implements IntegerBlockProperty.IntRepresented { ++ ++ private final BiMap cache; ++ ++ NoteBlockProperty(final @NotNull String name) { ++ super(name, Note.class); ++ this.cache = HashBiMap.create(25); ++ for (int i = 0; i <= 24; i++) { ++ this.cache.put(i, new Note(i)); ++ } ++ } ++ ++ @Override ++ public @NotNull String name(final @NotNull Note value) { ++ return String.valueOf(value.getId()); ++ } ++ ++ @Override ++ public boolean isValidName(final @NotNull String name) { ++ try { ++ final Integer value = Integer.valueOf(name); ++ if (this.cache.containsKey(value)) { ++ return true; ++ } ++ } catch (final NumberFormatException ignored) { ++ } ++ return false; ++ } ++ ++ @Override ++ public @NotNull Note value(final @NotNull String name) { ++ try { ++ final Integer value = Integer.valueOf(name); ++ if (this.cache.containsKey(value)) { ++ return this.cache.get(value); ++ } ++ throw EXCEPTION_CREATOR.create(name, ExceptionCreator.Type.NAME, this); ++ } catch (final NumberFormatException exception) { ++ throw EXCEPTION_CREATOR.create(name, ExceptionCreator.Type.NAME, this); ++ } ++ } ++ ++ @Override ++ public boolean isValue(final @NotNull Object object) { ++ return object instanceof final Note note && this.cache.inverse().containsKey(note); ++ } ++ ++ @Override ++ public @Unmodifiable @NotNull Set values() { ++ return Collections.unmodifiableSet(this.cache.values()); ++ } ++ ++ @Override ++ public int toIntValue(final @NotNull Note value) { ++ return this.cache.inverse().get(value); ++ } ++ ++ @Override ++ public @NotNull Note fromIntValue(final int value) { ++ return this.cache.get(value); ++ } ++} +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..8ee17bc3cfd3b579b39f490c349a49ad7c36597b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/RotationBlockProperty.java +@@ -0,0 +1,53 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.collect.Sets; ++import java.util.LinkedHashSet; ++import java.util.Set; ++import org.bukkit.block.BlockFace; ++ ++/** ++ * Special exception for the {@link BlockProperties#ROTATION_16} property because ++ * in the API it's represented as an enum, but stored as an integer. ++ */ ++final class RotationBlockProperty extends EnumBlockProperty.EnumIntRepresented { ++ ++ 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); ++ } ++ ++ RotationBlockProperty(final String name) { ++ super(name, BlockFace.class, VALUES, RotationBlockProperty::enumToInt); ++ } ++ ++ private static int enumToInt(final BlockFace rotation) { ++ return switch (rotation) { ++ case SOUTH -> 0x0; ++ case SOUTH_SOUTH_WEST -> 0x1; ++ case SOUTH_WEST -> 0x2; ++ case WEST_SOUTH_WEST -> 0x3; ++ case WEST -> 0x4; ++ case WEST_NORTH_WEST -> 0x5; ++ case NORTH_WEST -> 0x6; ++ case NORTH_NORTH_WEST -> 0x7; ++ case NORTH -> 0x8; ++ case NORTH_NORTH_EAST -> 0x9; ++ case NORTH_EAST -> 0xA; ++ case EAST_NORTH_EAST -> 0xB; ++ case EAST -> 0xC; ++ case EAST_SOUTH_EAST -> 0xD; ++ case SOUTH_EAST -> 0xE; ++ case SOUTH_SOUTH_EAST -> 0xF; ++ default -> throw new IllegalArgumentException("Illegal rotation " + rotation); ++ }; ++ } ++} +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 9a65c4f614a6c358d74491794d7b25172a00bc11..30b453d3341e8ff6b9be7ab8784c9e071720651e 100644 +--- a/src/main/java/org/bukkit/UnsafeValues.java ++++ b/src/main/java/org/bukkit/UnsafeValues.java +@@ -278,6 +278,18 @@ public interface UnsafeValues { + String getStatisticCriteriaKey(@NotNull org.bukkit.Statistic statistic); + // Paper end + ++ // 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 fb4c7cf0f67f3e4227d17f6702ae7b7bf1c110ab..6882ab2570581b197f115ca3f8ffdadc4c5ce57c 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/1054-BlockProperty-API.patch b/patches/server/1054-BlockProperty-API.patch new file mode 100644 index 000000000..0ff096f66 --- /dev/null +++ b/patches/server/1054-BlockProperty-API.patch @@ -0,0 +1,592 @@ +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..41b5e8c1ded52e039018de12c6fa7caea07f7290 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/PaperBlockProperties.java +@@ -0,0 +1,83 @@ ++package io.papermc.paper.block.property; ++ ++import io.papermc.paper.block.property.BlockProperties; ++import io.papermc.paper.block.property.BlockProperty; ++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(Property nmsProperty, BlockProperty paperProperty) { ++ CraftBlockData.DATA_PROPERTY_CACHE_MAP.put(nmsProperty, paperProperty); ++ } ++ ++ ++ public static BlockProperty convertToPaperProperty(Property nmsProperty) { ++ return CraftBlockData.DATA_PROPERTY_CACHE_MAP.computeIfAbsent(nmsProperty, prop -> { ++ java.util.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(BlockProperty paperProperty) { ++ return CraftBlockData.DATA_PROPERTY_CACHE_MAP.inverse().computeIfAbsent(paperProperty, prop -> { ++ java.util.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..3a0b62187ea6f213db979f89ea44edcf0897d629 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/property/PaperBlockPropertyHolder.java +@@ -0,0 +1,77 @@ ++package io.papermc.paper.block.property; ++ ++import com.google.common.base.Preconditions; ++import java.util.Collection; ++import java.util.Collections; ++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.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++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 @Nullable 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); ++ if (nmsProperty instanceof final IntegerProperty nmsIntProperty) { ++ final T value; ++ if (property instanceof final IntegerBlockProperty.IntRepresented intRepresented) { ++ value = ((IntegerBlockProperty.IntRepresented) intRepresented).fromIntValue(this.get(nmsIntProperty)); ++ } else { ++ value = this.get((Property) nmsIntProperty); ++ } ++ return value; ++ } else if (nmsProperty instanceof BooleanProperty) { ++ return this.get((Property) nmsProperty); ++ } else if (nmsProperty instanceof final EnumProperty enumProperty && property instanceof final EnumBlockProperty enumDataProperty) { ++ return (T) this.get(enumProperty, enumDataProperty.type()); ++ } else { ++ 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)); ++ } ++} +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 1d8c3f2e57aa9b0830cf7cfb6354fcf636e9c74b..05096544fef628edb1c81f57706ddc480f4685b1 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 +@@ -34,10 +34,12 @@ public abstract class Property> { + + public abstract int getIdFor(final T value); + // Paper end - optimise state lookup ++ 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; ++ 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 d6480b44f94f1a8d21eb5b5ded2956889883c560..6d56d63d0174386f6307a047b3c879e4fc64cc39 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/data/CraftBlockData.java +@@ -5,6 +5,7 @@ import com.google.common.base.Preconditions; + import com.google.common.collect.ImmutableSet; + import com.mojang.brigadier.StringReader; + import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import io.papermc.paper.block.property.BlockProperty; // Paper + import java.util.HashMap; + import java.util.Map; + import java.util.Set; +@@ -46,7 +47,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 { // Paper + + private net.minecraft.world.level.block.state.BlockState state; + private Map, Comparable> parsedStates; +@@ -76,7 +77,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); + } + +@@ -163,7 +164,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); + } +@@ -193,7 +194,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); + } +@@ -356,6 +357,7 @@ public class CraftBlockData implements BlockData { + + // + private static final Map, Function> MAP = new HashMap<>(); ++ public static final com.google.common.collect.BiMap, BlockProperty> DATA_PROPERTY_CACHE_MAP = com.google.common.collect.HashBiMap.create(); // Paper + + static { + // +@@ -531,8 +533,38 @@ 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(); ++ } ++ @SuppressWarnings("unchecked") ++ @Override ++ public > void setValue(final BlockProperty property, final T value) { ++ Preconditions.checkNotNull(value, "Cannot set a data property to null"); ++ final Property nmsProperty = io.papermc.paper.block.property.PaperBlockProperties.convertToNmsProperty(property); ++ Preconditions.checkArgument(this.state.hasProperty(nmsProperty), property.name() + " is not present on " + this); ++ if (nmsProperty instanceof final IntegerProperty nmsIntProperty) { ++ final int intValue; ++ if (property instanceof final io.papermc.paper.block.property.IntegerBlockProperty.IntRepresented intRepresented) { ++ intValue = ((io.papermc.paper.block.property.IntegerBlockProperty.IntRepresented) intRepresented).toIntValue(value); ++ } else { ++ intValue = (Integer) value; ++ } ++ this.set(nmsIntProperty, intValue); ++ } else if (nmsProperty instanceof final BooleanProperty nmsBoolProperty) { ++ this.set(nmsBoolProperty, (Boolean) value); ++ } else if (nmsProperty instanceof final EnumProperty enumProperty && value instanceof final Enum enumValue) { ++ this.set(enumProperty, enumValue); ++ } else { ++ throw new IllegalArgumentException("Did not recognize " + property + " with value " + value + " (" + value.getClass().getSimpleName() + ") for " + nmsProperty); ++ } ++ } ++ // 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 1324f05de8106032ce290e928cf106fb4f450517..c1b32b611b9a9d5100e4d2cac1898d81d1ff551a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -688,6 +688,18 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end - lifecycle event API + ++ // 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..fa43e14da73154feead4e94360e219b811e804a4 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/block/property/BlockPropertyTest.java +@@ -0,0 +1,218 @@ ++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.entity.trialspawner.TrialSpawnerState; ++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.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.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.Wall; ++import org.bukkit.support.AbstractTestingBase; ++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; ++ ++public class BlockPropertyTest extends AbstractTestingBase { ++ ++ 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) ++ .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())); ++ } ++ } ++ ++ @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) { ++ // special cases ++ if (property instanceof RotationBlockProperty && nmsProperty instanceof net.minecraft.world.level.block.state.properties.IntegerProperty && "rotation".equals(nmsProperty.getName())) { ++ return nmsProperty.getPossibleValues().size() == property.values().size(); ++ } else if (property instanceof NoteBlockProperty && nmsProperty instanceof net.minecraft.world.level.block.state.properties.IntegerProperty && "note".equals(nmsProperty.getName())) { ++ return nmsProperty.getPossibleValues().size() == property.values().size(); ++ } ++ // end special cases ++ if (nmsProperty instanceof net.minecraft.world.level.block.state.properties.BooleanProperty && property instanceof BooleanBlockProperty) { ++ return true; ++ } else if (nmsProperty instanceof net.minecraft.world.level.block.state.properties.IntegerProperty intProp && property instanceof IntegerBlockProperty prop) { ++ return intProp.min == prop.min() && intProp.max == prop.max() && intProp.getPossibleValues().size() == prop.values().size(); ++ } else if (nmsProperty instanceof net.minecraft.world.level.block.state.properties.EnumProperty enumProp && property instanceof EnumBlockProperty prop) { ++ return ENUM_MAPPING.get(enumProp.getValueClass()) == prop.type() && enumProp.getPossibleValues().size() == prop.values().size(); ++ } ++ return 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; ++ if (!ENUM_MAPPING.containsKey(enumProp.getValueClass())) { ++ throw new AssertionError("Missing enum mapping for " + enumProp.getValueClass()); // comment out to catch all missing types ++ } ++ 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; ++ } ++ throw new AssertionError(property + " is not a recognized property type"); ++ } ++ ++ 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; ++ } ++}