--- a/net/minecraft/world/level/block/state/StateHolder.java
+++ b/net/minecraft/world/level/block/state/StateHolder.java
@@ -15,10 +_,10 @@
 import javax.annotation.Nullable;
 import net.minecraft.world.level.block.state.properties.Property;
 
-public abstract class StateHolder<O, S> {
+public abstract class StateHolder<O, S> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder { // Paper - optimise blockstate property access
     public static final String NAME_TAG = "Name";
     public static final String PROPERTIES_TAG = "Properties";
-    private static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() {
+    public static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() {
         @Override
         public String apply(@Nullable Entry<Property<?>, Comparable<?>> propertyEntry) {
             if (propertyEntry == null) {
@@ -34,14 +_,28 @@
         }
     };
     protected final O owner;
-    private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values;
+    private Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; // Paper - optimise blockstate property access - remove final
     private Map<Property<?>, S[]> neighbours;
     protected final MapCodec<S> propertiesCodec;
 
+    // Paper start - optimise blockstate property access
+    protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> optimisedTable;
+    protected final long tableIndex;
+
+    @Override
+    public final long moonrise$getTableIndex() {
+        return this.tableIndex;
+    }
+    // Paper end - optimise blockstate property access
+
     protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> values, MapCodec<S> propertiesCodec) {
         this.owner = owner;
         this.values = values;
         this.propertiesCodec = propertiesCodec;
+        // Paper start - optimise blockstate property access
+        this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet());
+        this.tableIndex = this.optimisedTable.getIndex((StateHolder<O, S>)(Object)this);
+        // Paper end - optimise blockstate property access
     }
 
     public <T extends Comparable<T>> S cycle(Property<T> property) {
@@ -77,20 +_,21 @@
     }
 
     public Collection<Property<?>> getProperties() {
-        return Collections.unmodifiableCollection(this.values.keySet());
+        return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access
     }
 
     public boolean hasProperty(Property<?> property) {
-        return this.values.containsKey(property);
+        return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access
     }
 
     public <T extends Comparable<T>> T getValue(Property<T> property) {
-        Comparable<?> comparable = this.values.get(property);
-        if (comparable == null) {
-            throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner);
-        } else {
-            return property.getValueClass().cast(comparable);
+        // Paper start - optimise blockstate property access
+        final T ret = this.optimisedTable.get(this.tableIndex, property);
+        if (ret != null) {
+            return ret;
         }
+        throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner);
+        // Paper end - optimise blockstate property access
     }
 
     public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) {
@@ -103,22 +_,30 @@
 
     @Nullable
     private <T extends Comparable<T>> T getNullableValue(Property<T> property) {
-        Comparable<?> comparable = this.values.get(property);
-        return comparable == null ? null : property.getValueClass().cast(comparable);
+        return property == null ? null : this.optimisedTable.get(this.tableIndex, property); // Paper - optimise blockstate property access
     }
 
     public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) {
-        Comparable<?> comparable = this.values.get(property);
-        if (comparable == null) {
-            throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner);
-        } else {
-            return this.setValueInternal(property, value, comparable);
+        // Paper start - optimise blockstate property access
+        final S ret = this.optimisedTable.set(this.tableIndex, property, value);
+        if (ret != null) {
+            return ret;
         }
+        throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner);
+        // Paper end - optimise blockstate property access
     }
 
     public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) {
-        Comparable<?> comparable = this.values.get(property);
-        return (S)(comparable == null ? this : this.setValueInternal(property, value, comparable));
+        // Paper start - optimise blockstate property access
+        if (property == null) {
+            return (S)(StateHolder<O, S>)(Object)this;
+        }
+        final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder<O, S>)(Object)this);
+        if (ret != null) {
+            return ret;
+        }
+        throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner);
+        // Paper end - optimise blockstate property access
     }
 
     private <T extends Comparable<T>, V extends T> S setValueInternal(Property<T> property, V value, Comparable<?> comparable) {
@@ -135,21 +_,27 @@
     }
 
     public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> possibleStateMap) {
-        if (this.neighbours != null) {
-            throw new IllegalStateException();
-        } else {
-            Map<Property<?>, S[]> map = new Reference2ObjectArrayMap<>(this.values.size());
-
-            for (Entry<Property<?>, Comparable<?>> entry : this.values.entrySet()) {
-                Property<?> property = entry.getKey();
-                map.put(
-                    property,
-                    (S[]) property.getPossibleValues().stream().map(comparable -> possibleStateMap.get(this.makeNeighbourValues(property, comparable))).toArray()
-                );
-            }
-
-            this.neighbours = map;
-        }
+        // Paper start - optimise blockstate property access
+        final Map<Map<Property<?>, Comparable<?>>, S> map = possibleStateMap;
+        if (this.optimisedTable.isLoaded()) {
+            return;
+        }
+        this.optimisedTable.loadInTable(map);
+
+        // de-duplicate the tables
+        for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) {
+            final S value = entry.getValue();
+            ((StateHolder<O, S>)value).optimisedTable = this.optimisedTable;
+        }
+
+        // remove values arrays
+        for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) {
+            final S value = entry.getValue();
+            ((StateHolder<O, S>)value).values = null;
+        }
+
+        return;
+        // Paper end  optimise blockstate property access
     }
 
     private Map<Property<?>, Comparable<?>> makeNeighbourValues(Property<?> property, Comparable<?> value) {
@@ -159,7 +_,11 @@
     }
 
     public Map<Property<?>, Comparable<?>> getValues() {
-        return this.values;
+        // Paper start - optimise blockstate property access
+        ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> table = this.optimisedTable;
+        // We have to use this.values until the table is loaded
+        return table.isLoaded() ? table.getMapView(this.tableIndex) : this.values;
+        // Paper end - optimise blockstate property access
     }
 
     protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> ownerCodec, Function<O, S> defaultStateGetter) {
