Grow speleothems from decorators and keep them supported

Dripstone and sulfur spikes are placed as oriented, thickness-graded
columns by the surface and ceiling decorators through a shared
IrisSpeleothems helper, replacing the ad hoc dripstone fixups, and the
stacking path is unified for both directions. The perfection modifier
renormalizes a spike whose support a pool replaced, dropping the
unsupported run and re-tipping what remains. Surface sturdiness is asked
per face now, so a downward spike tests the ceiling it hangs from.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 05:03:14 -04:00
1 parent fd66b41bad
commit 5053a644c7
9 files changed
+839 -127

No files matched your search

@@ -5,12 +5,20 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -21,8 +29,11 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class DecoratorCoreTest {
@@ -429,6 +440,211 @@ public class DecoratorCoreTest {
assertEquals("minecraft:twisting_vines", DecoratorCore.stackedVineKey(vine, 3, 2));
}
@Test
public void sulfurStalagmiteKeepsSulfurMaterialAndTapersToOneTip() {
Hunk<PlatformBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, true, false);
assertSpikeColumn(output, "minecraft:sulfur_spike", true, false,
"base", "middle", "middle", "frustum", "tip");
}
@Test
public void sulfurStalactiteKeepsWaterloggingAndPointsDown() {
Hunk<PlatformBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, false, true);
assertSpikeColumn(output, "minecraft:sulfur_spike", false, true,
"base", "middle", "middle", "frustum", "tip");
}
@Test
public void shortSulfurSpikesHaveCorrectTipsInBothDirections() {
for (boolean upward : new boolean[]{true, false}) {
Hunk<PlatformBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false);
Hunk<PlatformBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false);
assertSpikeColumn(single, "minecraft:sulfur_spike", upward, false, "tip");
assertSpikeColumn(pair, "minecraft:sulfur_spike", upward, false, "frustum", "tip");
}
}
@Test
public void pointedDripstoneRetainsWaterloggingWhenStacked() {
Hunk<PlatformBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, true, true);
assertSpikeColumn(output, "minecraft:pointed_dripstone", true, true,
"base", "middle", "frustum", "tip");
}
@Test
public void clippedSpikesRebuildTheirFreeEndWithoutOverwritingObstructions() {
for (boolean upward : new boolean[]{true, false}) {
SpikeFixture fixture = new SpikeFixture(5, upward);
PlatformBlockState obstruction = sturdyState();
fixture.output.set(0, upward ? 4 : 2, 0, obstruction);
fixture.place();
assertSame(obstruction, fixture.output.get(0, upward ? 4 : 2, 0));
assertEquals("tip", fixture.property(3, "thickness"));
assertEquals("frustum", fixture.property(upward ? 2 : 4, "thickness"));
assertEquals("base", fixture.property(upward ? 1 : 5, "thickness"));
}
}
@Test
public void ceilingSpikesRequireTheUndersideOfTheirSupport() {
SpikeFixture fixture = new SpikeFixture(3, false);
BlockData support = (BlockData) fixture.output.get(0, 4, 0).nativeHandle();
when(support.isFaceSturdy(BlockFace.DOWN, BlockSupport.FULL)).thenReturn(false);
fixture.place();
assertTrue(fixture.output.get(0, 3, 0).isAir());
}
@Test
public void spikeStacksHonorSurfacePalettesInBothDirections() {
for (boolean upward : new boolean[]{true, false}) {
SpikeFixture fixture = new SpikeFixture(3, upward);
PlatformBlockState allowed = sturdyState();
when(fixture.decorator.getWhitelist()).thenReturn(new KList<>());
when(fixture.decorator.getWhitelistArray(fixture.data)).thenReturn(new PlatformBlockState[]{allowed});
fixture.place();
assertTrue(fixture.output.get(0, upward ? 1 : 3, 0).isAir());
PlatformBlockState support = fixture.output.get(0, upward ? 0 : 4, 0);
when(support.matches(allowed)).thenReturn(true);
fixture.place();
assertEquals("tip", fixture.property(upward ? 3 : 1, "thickness"));
when(fixture.decorator.getBlacklist()).thenReturn(new KList<>());
when(fixture.decorator.getBlacklistArray(fixture.data)).thenReturn(new PlatformBlockState[]{allowed});
fixture.output.set(0, upward ? 1 : 3, 0, airState());
fixture.place();
assertTrue(fixture.output.get(0, upward ? 1 : 3, 0).isAir());
}
}
@Test
public void submergedSpikesWaterlogAndStopAtLava() {
for (boolean upward : new boolean[]{true, false}) {
SpikeFixture fixture = new SpikeFixture(4, upward);
PlatformBlockState water = mock(PlatformBlockState.class);
PlatformBlockState lava = mock(PlatformBlockState.class);
when(water.isFluid()).thenReturn(true);
when(water.isWater()).thenReturn(true);
when(lava.isFluid()).thenReturn(true);
int first = upward ? 1 : 4;
int second = upward ? 2 : 3;
fixture.output.set(0, first, 0, water);
fixture.output.set(0, second, 0, lava);
fixture.opts.underwater = true;
fixture.opts.fluidHeight = 4;
fixture.place();
assertEquals("true", fixture.property(first, "waterlogged"));
assertEquals("tip", fixture.property(first, "thickness"));
assertSame(lava, fixture.output.get(0, second, 0));
}
}
@Test
public void oppositeSulfurTipsMergeWithoutReplacingEitherColumn() {
for (boolean upward : new boolean[]{true, false}) {
SpikeFixture fixture = new SpikeFixture(5, upward);
int oppositeY = 3;
PlatformBlockState opposite = spikeState("minecraft:sulfur_spike", Map.of(
"thickness", "tip", "vertical_direction", upward ? "down" : "up", "waterlogged", "false"));
fixture.output.set(0, oppositeY, 0, opposite);
fixture.place();
assertEquals("tip_merge", fixture.property(oppositeY, "thickness"));
assertEquals(upward ? "down" : "up", fixture.property(oppositeY, "vertical_direction"));
assertEquals("tip_merge", fixture.property(upward ? 2 : 4, "thickness"));
assertEquals("frustum", fixture.property(upward ? 1 : 5, "thickness"));
}
}
@Test
public void sulfurTipsDoNotMergeWithPointedDripstone() {
SpikeFixture fixture = new SpikeFixture(4, true);
PlatformBlockState dripstone = spikeState("minecraft:pointed_dripstone", Map.of(
"thickness", "tip", "vertical_direction", "down", "waterlogged", "false"));
fixture.output.set(0, 3, 0, dripstone);
fixture.place();
assertEquals("tip", fixture.property(2, "thickness"));
assertSame(dripstone, fixture.output.get(0, 3, 0));
}
@Test
public void singleDecoratorsNormalizeNativeSpikeDirectionAndThickness() {
for (boolean upward : new boolean[]{true, false}) {
SpikeFixture fixture = new SpikeFixture(1, upward);
if (upward) {
DecoratorCore.placeSurfaceSingle(fixture.decorator, 0, 0, 0, 0, 0,
fixture.output, new RNG(1L), fixture.data, false, false, null);
} else {
DecoratorCore.placeSingleAt(fixture.decorator, 0, 0, 0, 1, 0,
fixture.output, new RNG(1L), fixture.data, true, null);
}
assertEquals("tip", fixture.property(1, "thickness"));
assertEquals(upward ? "up" : "down", fixture.property(1, "vertical_direction"));
}
}
@Test
public void extendingSpikeRepairsTheExistingTipBehindIt() {
SpikeFixture fixture = new SpikeFixture(3, true);
PlatformBlockState spike = spikeState("minecraft:sulfur_spike", Map.of(
"thickness", "tip", "vertical_direction", "up", "waterlogged", "false"));
fixture.output.set(0, 0, 0, spike);
fixture.place();
assertEquals("base", fixture.property(0, "thickness"));
assertEquals("middle", fixture.property(1, "thickness"));
assertEquals("frustum", fixture.property(2, "thickness"));
assertEquals("tip", fixture.property(3, "thickness"));
}
@Test
public void singleSpikeChecksPaletteBeforeReplacingItsSupport() {
SpikeFixture fixture = new SpikeFixture(1, true);
PlatformBlockState original = fixture.output.get(0, 0, 0);
PlatformBlockState replacement = sturdyState();
IrisBlockData forced = mock(IrisBlockData.class);
when(forced.getBlockData(fixture.data)).thenReturn(replacement);
when(fixture.decorator.getForceBlock()).thenReturn(forced);
when(fixture.decorator.getWhitelist()).thenReturn(new KList<>());
when(fixture.decorator.getWhitelistArray(fixture.data)).thenReturn(new PlatformBlockState[]{original});
DecoratorCore.placeSurfaceSingle(fixture.decorator, 0, 0, 0, 0, 0,
fixture.output, new RNG(1L), fixture.data, false, false, null);
assertSame(replacement, fixture.output.get(0, 0, 0));
assertEquals("tip", fixture.property(1, "thickness"));
}
@Test
public void clippedCeilingVinesRetainTheirGrowingTip() {
SpikeFixture fixture = new SpikeFixture(5, false);
PlatformBlockState plant = mock(PlatformBlockState.class);
PlatformBlockState tip = mock(PlatformBlockState.class);
PlatformBlockState obstruction = sturdyState();
when(plant.key()).thenReturn("minecraft:weeping_vines_plant");
when(tip.key()).thenReturn("minecraft:weeping_vines");
when(fixture.decorator.pickBlockData(any(RNG.class), eq(fixture.data), anyDouble(), anyDouble())).thenReturn(tip);
when(fixture.decorator.pickBlockDataTop(any(RNG.class), eq(fixture.data), anyDouble(), anyDouble())).thenReturn(tip);
fixture.output.set(0, 3, 0, obstruction);
try (MockedStatic<B> blocks = mockStatic(B.class, CALLS_REAL_METHODS)) {
blocks.when(() -> B.getState("minecraft:weeping_vines_plant")).thenReturn(plant);
blocks.when(() -> B.getState("minecraft:weeping_vines")).thenReturn(tip);
fixture.place();
}
assertSame(plant, fixture.output.get(0, 5, 0));
assertSame(tip, fixture.output.get(0, 4, 0));
assertSame(obstruction, fixture.output.get(0, 3, 0));
}
@Test
public void boundDecoratorHooksWinWhenBukkitClassesArePresent() {
PlatformBlockState vine = mock(PlatformBlockState.class);
@@ -441,7 +657,7 @@ public class DecoratorCoreTest {
when(vine.isVineBlock()).thenReturn(true);
when(faceFixer.fixFaces(vine, output, 0, 0, 0, 0, 0, null)).thenReturn(fixed);
when(decorator.canPlaceOnto(surface)).thenReturn(true);
when(sturdiness.canGoOn(surface)).thenReturn(true);
when(sturdiness.canGoOn(surface, true)).thenReturn(true);
DecoratorPlatformHooks.Bindings previous = DecoratorPlatformHooks.bind(faceFixer, sturdiness);
try {
assertSame(fixed, DecoratorCore.fixFacesForHunk(vine, output, 0, 0, 0, 0, 0, null));
@@ -457,6 +673,58 @@ public class DecoratorCoreTest {
return air;
}
private Hunk<PlatformBlockState> placeSpikeColumn(String material, int height, boolean upward, boolean waterlogged) {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState spike = spikeState(material, Map.of(
"thickness", "tip_merge", "vertical_direction", upward ? "down" : "up",
"waterlogged", Boolean.toString(waterlogged)));
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(height);
when(decorator.getTopThreshold()).thenReturn(0.75);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, height + 2, 1);
for (int y = 1; y <= height; y++) {
output.set(0, y, 0, airState());
}
PlatformBlockState support = sturdyState();
output.set(0, upward ? 0 : height + 1, 0, support);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
if (upward) {
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 0, height, output, new RNG(1L), data, opts);
} else {
DecoratorCore.placeStackDown(decorator, 0, 0, 0, 0, height, 1, output, new RNG(1L), data, height, opts, null);
}
assertSame(support, output.get(0, upward ? 0 : height + 1, 0));
return output;
}
private void assertSpikeColumn(Hunk<PlatformBlockState> output, String material,
boolean upward, boolean waterlogged, String... thicknesses) {
for (int index = 0; index < thicknesses.length; index++) {
int y = upward ? index + 1 : thicknesses.length - index;
assertEquals(material + "[thickness=" + thicknesses[index]
+ ",vertical_direction=" + (upward ? "up" : "down")
+ ",waterlogged=" + waterlogged + "]",
output.get(0, y, 0).key());
}
}
private PlatformBlockState spikeState(String material, Map<String, String> properties) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(material + "[thickness=" + properties.get("thickness")
+ ",vertical_direction=" + properties.get("vertical_direction")
+ ",waterlogged=" + properties.get("waterlogged") + "]");
when(state.canPlaceOnto(any(PlatformBlockState.class))).thenReturn(true);
when(state.withProperty(anyString(), anyString())).thenAnswer(invocation -> {
Map<String, String> updated = new LinkedHashMap<>(properties);
updated.put(invocation.getArgument(0), invocation.getArgument(1));
return spikeState(material, updated);
});
return state;
}
private PlatformBlockState tallPlantState() {
return tallPlantState(mock(PlatformBlockState.class), mock(PlatformBlockState.class));
}
@@ -476,4 +744,41 @@ public class DecoratorCoreTest {
when(blockData.isFaceSturdy(any(), eq(BlockSupport.FULL))).thenReturn(true);
return support;
}
private class SpikeFixture {
private final IrisDecorator decorator = mock(IrisDecorator.class);
private final IrisData data = mock(IrisData.class);
private final DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
private final Hunk<PlatformBlockState> output;
private final int height;
private final boolean upward;
private SpikeFixture(int height, boolean upward) {
this.height = height;
this.upward = upward;
output = Hunk.newArrayHunk(1, height + 2, 1);
for (int y = 0; y < output.getHeight(); y++) {
output.set(0, y, 0, airState());
}
output.set(0, upward ? 0 : height + 1, 0, sturdyState());
PlatformBlockState spike = spikeState("minecraft:sulfur_spike", Map.of(
"thickness", "tip_merge", "vertical_direction", upward ? "down" : "up", "waterlogged", "false"));
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(height);
when(decorator.getTopThreshold()).thenReturn(0.75);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike);
}
private void place() {
if (upward) {
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 0, height, output, new RNG(1L), data, opts);
} else {
DecoratorCore.placeStackDown(decorator, 0, 0, 0, 0, height, 1, output, new RNG(1L), data, height, opts, null);
}
}
private String property(int y, String name) {
return IrisProceduralBlocks.propertyValue(output.get(0, y, 0), name);
}
}
}
@@ -0,0 +1,175 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.decorator.IrisSpeleothems;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisPerfectionModifierSpeleothemTest {
@Test
public void poolReplacementRemovesPreviouslySupportedSpikesInBothDirections() {
for (String material : new String[]{"minecraft:sulfur_spike", "minecraft:pointed_dripstone"}) {
for (boolean upward : new boolean[]{true, false}) {
for (String fluid : new String[]{"minecraft:water", "minecraft:lava"}) {
Fixture fixture = new Fixture();
fixture.column(material, upward, false);
fixture.normalize();
for (int y = 2; y <= 4; y++) {
assertTrue(IrisSpeleothems.isSpike(fixture.output.get(0, y, 0)));
}
int supportY = upward ? 1 : 5;
PlatformBlockState pool = fluid.equals("minecraft:water") ? fixture.water : fluid(fluid);
fixture.output.set(0, supportY, 0, pool);
fixture.normalize();
assertSame(pool, fixture.output.get(0, supportY, 0));
for (int y = 2; y <= 4; y++) {
assertSame(fixture.air, fixture.output.get(0, y, 0));
}
}
}
}
}
@Test
public void removedWaterloggedSpikesRestoreWater() {
for (boolean upward : new boolean[]{true, false}) {
Fixture fixture = new Fixture();
fixture.column("minecraft:sulfur_spike", upward, true);
fixture.output.set(0, upward ? 1 : 5, 0, fixture.water);
fixture.normalize();
for (int y = 2; y <= 4; y++) {
assertSame(fixture.water, fixture.output.get(0, y, 0));
}
}
}
@Test
public void removingOneMergedColumnRepairsTheSupportedOppositeTip() {
for (boolean removeUpward : new boolean[]{true, false}) {
Fixture fixture = new Fixture();
fixture.output.set(0, 0, 0, sturdyState());
fixture.output.set(0, 7, 0, sturdyState());
fixture.output.set(0, 1, 0, spike("minecraft:sulfur_spike", true, "base", false));
fixture.output.set(0, 2, 0, spike("minecraft:sulfur_spike", true, "frustum", false));
fixture.output.set(0, 3, 0, spike("minecraft:sulfur_spike", true, "tip_merge", false));
fixture.output.set(0, 4, 0, spike("minecraft:sulfur_spike", false, "tip_merge", false));
fixture.output.set(0, 5, 0, spike("minecraft:sulfur_spike", false, "frustum", false));
fixture.output.set(0, 6, 0, spike("minecraft:sulfur_spike", false, "base", false));
fixture.output.set(0, removeUpward ? 0 : 7, 0, fixture.water);
fixture.normalize();
for (int y = removeUpward ? 1 : 4; y <= (removeUpward ? 3 : 6); y++) {
assertSame(fixture.air, fixture.output.get(0, y, 0));
}
assertEquals("tip", fixture.property(removeUpward ? 4 : 3, "thickness"));
assertEquals("frustum", fixture.property(removeUpward ? 5 : 2, "thickness"));
assertEquals("base", fixture.property(removeUpward ? 6 : 1, "thickness"));
assertEquals(removeUpward ? "down" : "up",
fixture.property(removeUpward ? 4 : 3, "vertical_direction"));
}
}
@Test
public void objectRemovingTheFreeEndRetapersTheSupportedRemainder() {
for (boolean upward : new boolean[]{true, false}) {
Fixture fixture = new Fixture();
fixture.column("minecraft:sulfur_spike", upward, false);
fixture.output.set(0, upward ? 4 : 2, 0, fixture.air);
fixture.normalize();
assertEquals("tip", fixture.property(3, "thickness"));
assertEquals("frustum", fixture.property(upward ? 2 : 4, "thickness"));
assertSame(fixture.air, fixture.output.get(0, upward ? 4 : 2, 0));
}
}
private static PlatformBlockState spike(String material, boolean upward, String thickness, boolean waterlogged) {
return spike(material, Map.of(
"vertical_direction", upward ? "up" : "down",
"thickness", thickness,
"waterlogged", Boolean.toString(waterlogged)));
}
private static PlatformBlockState spike(String material, Map<String, String> properties) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(material + "[thickness=" + properties.get("thickness")
+ ",vertical_direction=" + properties.get("vertical_direction")
+ ",waterlogged=" + properties.get("waterlogged") + "]");
when(state.isWaterLogged()).thenReturn(Boolean.parseBoolean(properties.get("waterlogged")));
when(state.withProperty(anyString(), anyString())).thenAnswer(invocation -> {
Map<String, String> updated = new LinkedHashMap<>(properties);
updated.put(invocation.getArgument(0), invocation.getArgument(1));
return spike(material, updated);
});
return state;
}
private static PlatformBlockState sturdyState() {
PlatformBlockState state = mock(PlatformBlockState.class);
BlockData nativeState = mock(BlockData.class);
when(state.key()).thenReturn("minecraft:sulfur");
when(state.nativeHandle()).thenReturn(nativeState);
when(nativeState.isFaceSturdy(any(), eq(BlockSupport.FULL))).thenReturn(true);
return state;
}
private static PlatformBlockState fluid(String material) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(material);
when(state.isFluid()).thenReturn(true);
when(state.isWater()).thenReturn(material.equals("minecraft:water"));
return state;
}
private static class Fixture {
private final PlatformBlockState air = mock(PlatformBlockState.class);
private final PlatformBlockState water = fluid("minecraft:water");
private final Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 9, 1);
private Fixture() {
when(air.key()).thenReturn("minecraft:air");
when(air.isAir()).thenReturn(true);
for (int y = 0; y < output.getHeight(); y++) {
output.set(0, y, 0, air);
}
}
private void column(String material, boolean upward, boolean waterlogged) {
output.set(0, upward ? 1 : 5, 0, sturdyState());
output.set(0, upward ? 2 : 4, 0, spike(material, upward, "base", waterlogged));
output.set(0, 3, 0, spike(material, upward, "frustum", waterlogged));
output.set(0, upward ? 4 : 2, 0, spike(material, upward, "tip", waterlogged));
}
private void normalize() {
for (int y = output.getHeight() - 1; y >= 0; y--) {
PlatformBlockState state = output.get(0, y, 0);
if (IrisSpeleothems.isSpike(state)) {
IrisPerfectionModifier.normalizeSpike(state, output, 0, 0, y, air, water);
}
}
}
private String property(int y, String property) {
return IrisProceduralBlocks.propertyValue(output.get(0, y, 0), property);
}
}
}