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

@@ -115,7 +115,7 @@ final class DecoratorCore {
PlatformBlockState bdx = data.get(x, height, z);
PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (!underwater && !canGoOn(bd, bdx)
if (!IrisSpeleothems.isSpike(bd) && !underwater && !canGoOn(bd, bdx)
&& !decorator.isForcePlace() && decorator.getForceBlock() == null) {
return;
}
@@ -137,6 +137,11 @@ final class DecoratorCore {
}
}
if (IrisSpeleothems.isSpike(bd)) {
placeSingleSpike(bd, data, x, z, height + 1, true, underwater && !caveSkipFluid);
return;
}
String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
int lowerY = height + 1;
@@ -165,8 +170,11 @@ final class DecoratorCore {
}
private static boolean matchesPalette(PlatformBlockState[] palette, PlatformBlockState surface) {
if (surface == null) {
return false;
}
for (int i = 0; i < palette.length; i++) {
if (palette[i].equals(surface)) {
if (palette[i] == surface || surface.matches(palette[i])) {
return true;
}
}
@@ -181,6 +189,12 @@ final class DecoratorCore {
if (bd == null) {
return;
}
if (IrisSpeleothems.isSpike(bd)) {
if (height + 1 < data.getHeight() && allowsSurface(decorator, data.get(x, height + 1, z), irisData)) {
placeSingleSpike(bd, data, x, z, height, false, false);
}
return;
}
if (applyFixFaces) {
bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle);
}
@@ -194,72 +208,48 @@ final class DecoratorCore {
return;
}
int effectiveMax = max;
if (opts.underwater && height < opts.fluidHeight) {
effectiveMax = opts.fluidHeight;
}
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
if (stack == 1) {
int targetY = height + 1;
if (targetY >= data.getHeight()) {
return;
}
PlatformBlockState existing = data.get(x, targetY, z);
if (!canReplaceStackTarget(existing, opts.underwater)
|| (opts.caveSkipFluid && B.isFluid(existing))) {
return;
}
PlatformBlockState block = decorator.pickBlockDataTop(rng, irisData, realX, realZ);
if (block == null || (!opts.underwater && !canGoOn(block, data.get(x, height, z)))) {
return;
}
block = stackedVineBlock(block, stack, 0);
data.set(x, targetY, z, block);
PlatformBlockState support = data.get(x, height, z);
if (!allowsSurface(decorator, support, irisData)) {
return;
}
PlatformBlockState bdx = data.get(x, height, z);
int effectiveMax = opts.underwater ? Math.min(max, opts.fluidHeight - height) : max;
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
int placed = 0;
boolean hasSpikes = false;
for (int i = 0; i < stack; i++) {
int h = height + i;
double threshold = ((double) i) / (stack - 1);
PlatformBlockState bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
int y = height + 1 + i;
if (y >= data.getHeight()) {
break;
}
if (i == 0 && !opts.underwater && !canGoOn(bd, bdx)) {
break;
}
if (opts.underwater && height + 1 + i > opts.fluidHeight) {
break;
}
if (height + 1 + i >= data.getHeight()) {
break;
}
PlatformBlockState existing = data.get(x, height + 1 + i, z);
PlatformBlockState existing = data.get(x, y, z);
if (!canReplaceStackTarget(existing, opts.underwater)
|| (opts.caveSkipFluid && B.isFluid(existing))) {
break;
}
if (IrisProceduralBlocks.materialKey(bd).equals("minecraft:pointed_dripstone")) {
bd = dripstoneBlock(stack, i, "up");
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (block == null) {
break;
}
bd = stackedVineBlock(bd, stack, i);
data.set(x, height + 1 + i, z, bd);
if (IrisSpeleothems.isSpike(block)) {
if (!IrisSpeleothems.canPlace(block, data, x, z, y, true, opts.underwater)) {
break;
}
block = IrisSpeleothems.orient(block, existing, true);
hasSpikes = true;
} else if (i == 0 && !opts.underwater && !canGoOn(block, support)) {
break;
}
data.set(x, y, z, stackedVineBlock(block, stack, i));
placed++;
}
if (placed > 0 && placed < stack) {
finishVineTip(data, x, z, height + placed);
}
if (hasSpikes) {
IrisSpeleothems.finishColumn(data, x, z, height + 1, placed, true);
}
}
@@ -269,46 +259,46 @@ final class DecoratorCore {
if (height < 0 || height >= data.getHeight()) {
return;
}
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
PlatformBlockState block = decorator.pickBlockDataTop(rng, irisData, realX, realZ);
if (block == null) {
return;
}
block = stackedVineBlock(block, stack, 0);
data.set(x, height, z, fixFacesForHunk(block, data, x, z, realX, height, realZ, mantle));
PlatformBlockState support = height + 1 < data.getHeight() ? data.get(x, height + 1, z) : null;
if (!allowsSurface(decorator, support, irisData)) {
return;
}
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
int placed = 0;
boolean hasSpikes = false;
for (int i = 0; i < stack; i++) {
int h = height - i;
if (h < 0 || h < minHeight) {
int y = height - i;
if (y < 0 || y < minHeight) {
break;
}
double threshold = ((double) i) / (double) (stack - 1);
PlatformBlockState bd = threshold >= decorator.getTopThreshold()
PlatformBlockState existing = data.get(x, y, z);
if (!canReplaceStackTarget(existing, opts.underwater)
|| (opts.caveSkipFluid && B.isFluid(existing))) {
break;
}
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
if (block == null) {
break;
}
if (IrisProceduralBlocks.materialKey(bd).equals("minecraft:pointed_dripstone")) {
bd = dripstoneBlock(stack, i, "down");
if (IrisSpeleothems.isSpike(block)) {
if (!IrisSpeleothems.canPlace(block, data, x, z, y, false, opts.underwater)) {
break;
}
block = IrisSpeleothems.orient(block, existing, false);
hasSpikes = true;
}
bd = stackedVineBlock(bd, stack, i);
if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) {
break;
}
data.set(x, h, z, fixFacesForHunk(bd, data, x, z, realX, h, realZ, mantle));
block = stackedVineBlock(block, stack, i);
data.set(x, y, z, fixFacesForHunk(block, data, x, z, realX, y, realZ, mantle));
placed++;
}
if (placed > 0 && placed < stack) {
finishVineTip(data, x, z, height - placed + 1);
}
if (hasSpikes) {
IrisSpeleothems.finishColumn(data, x, z, height, placed, false);
}
}
@@ -321,6 +311,13 @@ final class DecoratorCore {
return;
}
if (IrisSpeleothems.isSpike(bd)) {
if (max > 1 && allowsSurface(decorator, data.get(xf, height, zf), irisData)) {
placeSingleSpike(bd, data, xf, zf, height + 1, true, false);
}
return;
}
String half = IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
int lowerY = height + 1;
@@ -357,6 +354,7 @@ final class DecoratorCore {
}
int placed = 0;
boolean hasSpikes = false;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max || h >= data.getHeight()) {
@@ -369,10 +367,24 @@ final class DecoratorCore {
if (bd == null) {
break;
}
if (IrisSpeleothems.isSpike(bd)) {
if (!allowsSurface(decorator, data.get(xf, height, zf), irisData)
|| !IrisSpeleothems.canPlace(bd, data, xf, zf, h, true, false)) {
break;
}
bd = IrisSpeleothems.orient(bd, data.get(xf, h, zf), true);
hasSpikes = true;
}
bd = stackedVineBlock(bd, stack, i);
data.set(xf, h, zf, bd);
placed++;
}
if (placed > 0 && placed < stack) {
finishVineTip(data, xf, zf, height + placed);
}
if (hasSpikes) {
IrisSpeleothems.finishColumn(data, xf, zf, height + 1, placed, true);
}
return placed;
}
@@ -437,11 +449,7 @@ final class DecoratorCore {
if (!B.canPlaceOnto(decorator, surface)) {
return false;
}
DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness();
if (sturdiness != null) {
return sturdiness.canGoOn(surface);
}
return ((BlockData) surface.nativeHandle()).isFaceSturdy(BlockFace.UP, BlockSupport.FULL);
return IrisSpeleothems.isSturdy(surface, true);
}
static boolean isValidShorelineSupport(IrisDecorator decorator, PlatformBlockState decorant, PlatformBlockState surface) {
@@ -477,37 +485,27 @@ final class DecoratorCore {
return stack;
}
private static volatile PlatformBlockState[] dripstoneUp;
private static volatile PlatformBlockState[] dripstoneDown;
private static PlatformBlockState[] buildDripstoneArr(String direction) {
String[] order = {"tip", "frustum", "base"};
PlatformBlockState[] arr = new PlatformBlockState[3];
for (int k = 0; k < 3; k++) {
arr[k] = B.getState("minecraft:pointed_dripstone[thickness=" + order[k] + ",vertical_direction=" + direction + "]");
}
return arr;
private static boolean allowsSurface(IrisDecorator decorator, PlatformBlockState surface, IrisData data) {
return decorator.isForcePlace()
|| (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface))
&& (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(data), surface));
}
private static PlatformBlockState dripstoneBlock(int stack, int i, String direction) {
int thIdx;
if (i == stack - 1) {
thIdx = 0;
} else if (i == stack - 2) {
thIdx = 1;
} else {
thIdx = 2;
private static void placeSingleSpike(PlatformBlockState spike, Hunk<PlatformBlockState> data,
int x, int z, int y, boolean upward, boolean allowWater) {
if (!IrisSpeleothems.canPlace(spike, data, x, z, y, upward, allowWater)) {
return;
}
if (direction.equals("up")) {
if (dripstoneUp == null) {
dripstoneUp = buildDripstoneArr("up");
}
return dripstoneUp[thIdx];
data.set(x, y, z, IrisSpeleothems.orient(spike, data.get(x, y, z), upward));
IrisSpeleothems.finishColumn(data, x, z, y, 1, upward);
}
private static void finishVineTip(Hunk<PlatformBlockState> data, int x, int z, int y) {
PlatformBlockState state = data.get(x, y, z);
PlatformBlockState tip = stackedVineBlock(state, 1, 0);
if (tip != state) {
data.set(x, y, z, tip);
}
if (dripstoneDown == null) {
dripstoneDown = buildDripstoneArr("down");
}
return dripstoneDown[thIdx];
}
static String stackedVineKey(PlatformBlockState state, int stack, int index) {
@@ -34,7 +34,7 @@ public final class DecoratorPlatformHooks {
}
public interface SurfaceSturdiness {
boolean canGoOn(PlatformBlockState surface);
boolean canGoOn(PlatformBlockState surface, boolean upward);
}
public static synchronized Bindings bind(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) {
@@ -0,0 +1,153 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.hunk.Hunk;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.BlockData;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisSpeleothems {
private static final String[] SPIKE_THICKNESSES = {"tip", "frustum", "base", "middle", "tip_merge"};
private static final ConcurrentHashMap<PlatformBlockState, PlatformBlockState[][]> SPIKE_STATES = new ConcurrentHashMap<>();
private IrisSpeleothems() {
}
public static boolean isSpike(PlatformBlockState state) {
if (state == null || state.key() == null) {
return false;
}
String material = IrisProceduralBlocks.materialKey(state);
return material.equals("minecraft:pointed_dripstone") || material.equals("minecraft:sulfur_spike");
}
public static boolean isSupported(PlatformBlockState state, Hunk<PlatformBlockState> data, int x, int z, int y) {
return isSupported(state, data, x, z, y,
"up".equals(IrisProceduralBlocks.propertyValue(state, "vertical_direction")));
}
public static void finishAtTip(Hunk<PlatformBlockState> data, int x, int z, int y) {
PlatformBlockState state = data.get(x, y, z);
if (!isSpike(state)) {
return;
}
boolean upward = "up".equals(IrisProceduralBlocks.propertyValue(state, "vertical_direction"));
int frontY = y + (upward ? 1 : -1);
PlatformBlockState front = frontY >= 0 && frontY < data.getHeight() ? data.get(x, frontY, z) : null;
if (!sameSpike(state, front, upward)) {
finishColumn(data, x, z, y, 1, upward);
}
}
public static void finishColumn(Hunk<PlatformBlockState> data, int x, int z, int start, int placed, boolean upward) {
int step = upward ? 1 : -1;
for (int i = placed - 1; i >= 0; i--) {
int y = start + step * i;
PlatformBlockState state = data.get(x, y, z);
if (isSpike(state)) {
finishSpike(data, x, z, y, state, upward);
}
}
if (placed == 0) {
return;
}
PlatformBlockState state = data.get(x, start, z);
if (!isSpike(state)) {
return;
}
for (int y = start - step; y >= 0 && y < data.getHeight(); y -= step) {
PlatformBlockState behind = data.get(x, y, z);
if (!sameSpike(state, behind, upward)) {
break;
}
finishSpike(data, x, z, y, behind, upward);
}
}
static boolean canPlace(PlatformBlockState spike, Hunk<PlatformBlockState> data,
int x, int z, int y, boolean upward, boolean allowWater) {
int supportY = y + (upward ? -1 : 1);
if (y < 0 || y >= data.getHeight() || supportY < 0 || supportY >= data.getHeight()) {
return false;
}
PlatformBlockState existing = data.get(x, y, z);
if (!B.isAir(existing) && !(allowWater && existing != null && existing.isWater())) {
return false;
}
return isSupported(spike, data, x, z, y, upward);
}
static PlatformBlockState orient(PlatformBlockState state, PlatformBlockState existing, boolean upward) {
if (existing != null && existing.isWater() && !state.isWaterLogged()) {
state = state.withProperty("waterlogged", "true");
}
return spikeBlock(state, upward, 0);
}
static boolean isSturdy(PlatformBlockState surface, boolean upward) {
if (surface == null || B.isAir(surface) || B.isFluid(surface)) {
return false;
}
DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness();
if (sturdiness != null) {
return sturdiness.canGoOn(surface, upward);
}
return ((BlockData) surface.nativeHandle()).isFaceSturdy(upward ? BlockFace.UP : BlockFace.DOWN, BlockSupport.FULL);
}
private static boolean isSupported(PlatformBlockState state, Hunk<PlatformBlockState> data,
int x, int z, int y, boolean upward) {
int supportY = y + (upward ? -1 : 1);
if (supportY < 0 || supportY >= data.getHeight()) {
return false;
}
PlatformBlockState support = data.get(x, supportY, z);
return sameSpike(state, support, upward) || isSturdy(support, upward);
}
private static PlatformBlockState[][] buildSpikeStates(PlatformBlockState state) {
PlatformBlockState[][] states = new PlatformBlockState[2][SPIKE_THICKNESSES.length];
for (int direction = 0; direction < states.length; direction++) {
PlatformBlockState directed = state.withProperty("vertical_direction", direction == 0 ? "up" : "down");
for (int thickness = 0; thickness < SPIKE_THICKNESSES.length; thickness++) {
states[direction][thickness] = directed.withProperty("thickness", SPIKE_THICKNESSES[thickness]);
}
}
return states;
}
private static boolean sameSpike(PlatformBlockState state, PlatformBlockState other, boolean upward) {
return isSpike(other)
&& IrisProceduralBlocks.materialKey(state).equals(IrisProceduralBlocks.materialKey(other))
&& (upward ? "up" : "down").equals(IrisProceduralBlocks.propertyValue(other, "vertical_direction"));
}
private static PlatformBlockState spikeBlock(PlatformBlockState state, boolean upward, int thickness) {
return SPIKE_STATES.computeIfAbsent(state, IrisSpeleothems::buildSpikeStates)[upward ? 0 : 1][thickness];
}
private static void finishSpike(Hunk<PlatformBlockState> data, int x, int z, int y, PlatformBlockState state, boolean upward) {
int step = upward ? 1 : -1;
int frontY = y + step;
int backY = y - step;
PlatformBlockState front = frontY >= 0 && frontY < data.getHeight() ? data.get(x, frontY, z) : null;
PlatformBlockState back = backY >= 0 && backY < data.getHeight() ? data.get(x, backY, z) : null;
int thickness = 0;
if (sameSpike(state, front, !upward)) {
String frontThickness = IrisProceduralBlocks.propertyValue(front, "thickness");
if ("tip".equals(frontThickness) || "tip_merge".equals(frontThickness)) {
thickness = 4;
data.set(x, frontY, z, spikeBlock(front, !upward, 4));
}
} else if (sameSpike(state, front, upward)) {
String frontThickness = IrisProceduralBlocks.propertyValue(front, "thickness");
thickness = "tip".equals(frontThickness) || "tip_merge".equals(frontThickness)
? 1 : sameSpike(state, back, upward) ? 3 : 2;
}
data.set(x, y, z, spikeBlock(state, upward, thickness));
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.decorator.IrisSpeleothems;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
@@ -104,6 +105,9 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<PlatformBlock
for (int k = top; k >= 0; k--) {
PlatformBlockState b = output.get(finalI, k, j);
if (IrisSpeleothems.isSpike(b)) {
b = normalizeSpike(b, output, finalI, j, k, States.AIR, States.WATER);
}
boolean now = b != null && !(B.isAir(b) || B.isFluid(b));
if (now != inside) {
@@ -161,6 +165,40 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<PlatformBlock
getEngine().getMetrics().getPerfection().put(p.getMilliseconds());
}
static PlatformBlockState normalizeSpike(PlatformBlockState state, Hunk<PlatformBlockState> output,
int x, int z, int y, PlatformBlockState air, PlatformBlockState water) {
if (IrisSpeleothems.isSupported(state, output, x, z, y)) {
IrisSpeleothems.finishAtTip(output, x, z, y);
return output.get(x, y, z);
}
String material = IrisProceduralBlocks.materialKey(state);
String direction = IrisProceduralBlocks.propertyValue(state, "vertical_direction");
boolean upward = "up".equals(direction);
int step = upward ? 1 : -1;
int nextY = y;
while (nextY >= 0 && nextY < output.getHeight()) {
PlatformBlockState current = output.get(x, nextY, z);
if (!IrisSpeleothems.isSpike(current)
|| !material.equals(IrisProceduralBlocks.materialKey(current))
|| !direction.equals(IrisProceduralBlocks.propertyValue(current, "vertical_direction"))) {
break;
}
output.set(x, nextY, z, current.isWaterLogged() ? water : air);
nextY += step;
}
if (nextY >= 0 && nextY < output.getHeight()) {
PlatformBlockState retained = output.get(x, nextY, z);
if (IrisSpeleothems.isSpike(retained)
&& material.equals(IrisProceduralBlocks.materialKey(retained))
&& !direction.equals(IrisProceduralBlocks.propertyValue(retained, "vertical_direction"))) {
IrisSpeleothems.finishColumn(output, x, z, nextY, 1, !upward);
}
}
return output.get(x, y, z);
}
private void hideOres(Hunk<PlatformBlockState> output, boolean multicore) {
BurstExecutor burst = burst().burst(multicore);
int height = output.getHeight();