This commit is contained in:
Brian Neumann-Fopiano
2026-04-22 19:23:50 -04:00
parent 23fad24fb7
commit 3d128b70a7
17 changed files with 1102 additions and 488 deletions
+1 -1
View File
@@ -1 +1 @@
699705819 -1935789196
@@ -27,7 +27,9 @@ import art.arcane.volmlib.util.math.Spiraler;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
@Builder @Builder
@Data @Data
@@ -117,10 +119,50 @@ public class PregenTask {
})); }));
} }
@FunctionalInterface
public interface InterleavedChunkConsumer {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
}
public void iterateAllChunks(Spiraled s) { public void iterateAllChunks(Spiraled s) {
iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s))); iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s)));
} }
public void iterateAllChunksInterleaved(InterleavedChunkConsumer consumer) {
List<int[]> regions = new ArrayList<>();
iterateRegions((rX, rZ) -> regions.add(new int[]{rX, rZ}));
List<List<int[]>> regionChunks = new ArrayList<>();
for (int[] region : regions) {
List<int[]> chunks = new ArrayList<>();
iterateChunks(region[0], region[1], (cx, cz) -> chunks.add(new int[]{region[0], region[1], cx, cz}));
if (!chunks.isEmpty()) {
regionChunks.add(chunks);
}
}
int[] indices = new int[regionChunks.size()];
boolean anyRemaining = true;
while (anyRemaining) {
anyRemaining = false;
for (int r = 0; r < regionChunks.size(); r++) {
List<int[]> chunks = regionChunks.get(r);
int idx = indices[r];
if (idx >= chunks.size()) {
continue;
}
anyRemaining = true;
int[] entry = chunks.get(idx);
boolean first = idx == 0;
boolean last = idx == chunks.size() - 1;
indices[r]++;
if (!consumer.on(entry[0], entry[1], entry[2], entry[3], first, last)) {
return;
}
}
}
}
private class Bounds { private class Bounds {
private Bound chunk = null; private Bound chunk = null;
private Bound region = null; private Bound region = null;
@@ -355,13 +355,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
static int computePaperLikeRecommendedCap(int workerThreads) { static int computePaperLikeRecommendedCap(int workerThreads) {
int normalizedWorkers = Math.max(1, workerThreads); int normalizedWorkers = Math.max(1, workerThreads);
int recommendedCap = normalizedWorkers * 4; int recommendedCap = normalizedWorkers * 2;
if (recommendedCap < 8) { if (recommendedCap < 8) {
return 8; return 8;
} }
if (recommendedCap > 128) { if (recommendedCap > 96) {
return 128; return 96;
} }
return recommendedCap; return recommendedCap;
@@ -328,7 +328,14 @@ public final class WorldRuntimeControlService {
int[] scanOrder = new int[maxY - minY + 1]; int[] scanOrder = new int[maxY - minY + 1];
int index = 0; int index = 0;
for (int y = maxY; y >= minY; y--) { int runtimeSurface = world.getHighestBlockYAt((int) source.getX(), (int) source.getZ());
int startY = Math.min(maxY, runtimeSurface + 1);
for (int y = startY; y >= minY; y--) {
scanOrder[index++] = y;
}
for (int y = startY + 1; y <= maxY; y++) {
scanOrder[index++] = y; scanOrder[index++] = y;
} }
@@ -0,0 +1,430 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.decorator;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.block.data.type.PointedDripstone;
final class DecoratorCore {
private static final long SEED_OFFSET = 29356788L;
private static final long PART_FACTOR = 10439677L;
static final ThreadLocal<PlaceOpts> SCRATCH_OPTS = ThreadLocal.withInitial(PlaceOpts::new);
static final class PlaceOpts {
boolean caveSkipFluid;
boolean underwater;
int fluidHeight;
void reset() {
caveSkipFluid = false;
underwater = false;
fluidHeight = 0;
}
}
static long partSeed(long baseSeed, int partOrdinal) {
return baseSeed + SEED_OFFSET - (partOrdinal * PART_FACTOR);
}
static long partSeed(long baseSeed, IrisDecorationPart part) {
return partSeed(baseSeed, part.ordinal());
}
static IrisDecorator pickDecorator(IrisBiome biome, IrisDecorationPart part, RNG gRNG,
RNG colRng, IrisData data, double realX, double realZ) {
IrisDecorator[] bucket = biome.getDecoratorBucket(part);
if (bucket.length == 0) {
return null;
}
IrisDecorator picked = null;
int count = 0;
for (IrisDecorator d : bucket) {
try {
if (d.passesChanceGate(gRNG, realX, realZ, data)) {
count++;
if (count == 1 || colRng.nextInt(count) == 0) {
picked = d;
}
}
} catch (Throwable e) {
Iris.reportError(e);
}
}
return picked;
}
static void placeSingleUp(IrisDecorator decorator, int x, int z,
int realX, int height, int realZ, Hunk<BlockData> data,
RNG rng, IrisData irisData, boolean caveSkipFluid, EngineMantle mantle) {
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
return;
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
data.set(x, height + 2, z, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
static void placeSurfaceSingle(IrisDecorator decorator,
int x, int z, int realX, int height, int realZ,
Hunk<BlockData> data, RNG rng, IrisData irisData,
boolean underwater, boolean caveSkipFluid, EngineMantle mantle) {
BlockData bdx = data.get(x, height, z);
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (!underwater && !canGoOn(bd, bdx)
&& !decorator.isForcePlace() && decorator.getForceBlock() == null) {
return;
}
if (decorator.getForceBlock() != null) {
if (caveSkipFluid && B.isFluid(bdx)) {
return;
}
data.set(x, height, z, fixFacesForHunk(
decorator.getForceBlock().getBlockData(irisData), data, x, z, realX, height, realZ, mantle));
return;
}
if (!decorator.isForcePlace()) {
if (decorator.getWhitelist() != null
&& decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(irisData).equals(bdx))) {
return;
}
if (decorator.getBlacklist() != null
&& decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(irisData).equals(bdx))) {
return;
}
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
data.set(x, height + 2, z, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
static void placeSingleAt(IrisDecorator decorator, int x, int z,
int realX, int height, int realZ, Hunk<BlockData> data,
RNG rng, IrisData irisData, boolean applyFixFaces, EngineMantle mantle) {
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
return;
}
if (applyFixFaces) {
bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle);
}
data.set(x, height, z, bd);
}
static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int max, Hunk<BlockData> data,
RNG rng, IrisData irisData, PlaceOpts opts) {
int effectiveMax = max;
if (opts.underwater && height < opts.fluidHeight) {
effectiveMax = opts.fluidHeight;
}
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, decorator.pickBlockDataTop(rng, irisData, realX, realZ));
return;
}
BlockData bdx = data.get(x, height, z);
for (int i = 0; i < stack; i++) {
int h = height + i;
double threshold = ((double) i) / (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
break;
}
if (i == 0 && !opts.underwater && !canGoOn(bd, bdx)) {
break;
}
if (opts.underwater && height + 1 + i > opts.fluidHeight) {
break;
}
if (opts.caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
break;
}
if (bd instanceof PointedDripstone) {
bd = dripstoneBlock(stack, i, BlockFace.UP);
}
data.set(x, height + 1 + i, z, bd);
}
}
static void placeStackDown(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int minHeight, Hunk<BlockData> data,
RNG rng, IrisData irisData, int max, PlaceOpts opts, EngineMantle mantle) {
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, fixFacesForHunk(
decorator.pickBlockDataTop(rng, irisData, realX, realZ),
data, x, z, realX, height, realZ, mantle));
return;
}
for (int i = 0; i < stack; i++) {
int h = height - i;
if (h < minHeight) {
continue;
}
double threshold = ((double) i) / (double) (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd instanceof PointedDripstone) {
bd = dripstoneBlock(stack, i, BlockFace.DOWN);
}
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));
}
}
static void placeFloatingSimple(IrisDecorator decorator,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data,
RNG rng, IrisData irisData) {
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
return;
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (max > 2) {
data.set(xf, height + 2, zf, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (max > 1) {
data.set(xf, height + 1, zf, bd);
}
}
static int placeFloatingStacked(IrisDecorator decorator,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data,
RNG rng, IrisData irisData) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
int placed = 0;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max) {
break;
}
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
break;
}
data.set(xf, h, zf, bd);
placed++;
}
return placed;
}
static BlockData fixFacesForHunk(BlockData b, Hunk<BlockData> hunk, int rX, int rZ,
int x, int y, int z, EngineMantle mantle) {
if (!B.isVineBlock(b)) {
return b;
}
MultipleFacing data = (MultipleFacing) b.clone();
data.getFaces().forEach(f -> data.setFace(f, false));
boolean found = false;
for (BlockFace f : BlockFace.values()) {
if (!f.isCartesian()) {
continue;
}
int yy = y + f.getModY();
BlockData r = null;
if (mantle != null) {
r = mantle.getMantle().get(x + f.getModX(), yy, z + f.getModZ(), BlockData.class);
}
if (r == null) {
r = EngineMantle.AIR;
}
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
found = true;
data.setFace(f, true);
continue;
}
int xx = rX + f.getModX();
int zz = rZ + f.getModZ();
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) {
continue;
}
r = hunk.get(xx, yy, zz);
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
found = true;
data.setFace(f, true);
}
}
if (!found) {
data.setFace(BlockFace.DOWN, true);
}
return data;
}
static boolean canGoOn(BlockData decorator, BlockData surface) {
return surface.isFaceSturdy(BlockFace.UP, BlockSupport.FULL);
}
private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
IrisData irisData, int max) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
return stack;
}
// Lazily populated on first dripstone decoration — avoids Bukkit API at class-load time.
// Index: 0=TIP, 1=FRUSTUM, 2=BASE. Race on init is benign (only allocation cost, not correctness).
private static volatile BlockData[] dripstoneUp;
private static volatile BlockData[] dripstoneDown;
private static BlockData[] buildDripstoneArr(BlockFace direction) {
PointedDripstone.Thickness[] order = {
PointedDripstone.Thickness.TIP,
PointedDripstone.Thickness.FRUSTUM,
PointedDripstone.Thickness.BASE
};
BlockData[] arr = new BlockData[3];
for (int k = 0; k < 3; k++) {
BlockData bd = Material.POINTED_DRIPSTONE.createBlockData();
((PointedDripstone) bd).setThickness(order[k]);
((PointedDripstone) bd).setVerticalDirection(direction);
arr[k] = bd;
}
return arr;
}
private static BlockData dripstoneBlock(int stack, int i, BlockFace direction) {
int thIdx;
if (i == stack - 1) {
thIdx = 0;
} else if (i == stack - 2) {
thIdx = 1;
} else {
thIdx = 2;
}
if (direction == BlockFace.UP) {
if (dripstoneUp == null) {
dripstoneUp = buildDripstoneArr(BlockFace.UP);
}
return dripstoneUp[thIdx];
}
if (dripstoneDown == null) {
dripstoneDown = buildDripstoneArr(BlockFace.DOWN);
}
return dripstoneDown[thIdx];
}
}
@@ -18,117 +18,38 @@
package art.arcane.iris.engine.decorator; package art.arcane.iris.engine.decorator;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import java.util.concurrent.atomic.AtomicLong;
/* /*
* Floating island decoration path. Bypasses all canGoOn, slope, whitelist, and blacklist * Floating island decoration path. Bypasses all canGoOn, slope, whitelist, and blacklist
* gating from IrisSurfaceDecorator — the island top IS the biome's designated surface by * gating from IrisSurfaceDecorator — the island top IS the biome's designated surface by
* construction, so those material-compatibility checks are never meaningful here. * construction, so those material-compatibility checks are never meaningful here.
*/ */
public class FloatingDecorator { public class FloatingDecorator {
public static final AtomicLong decCandidatesNull = new AtomicLong();
private static final long SEED_SALT = 29356788L;
private static final long PART_SALT = 10439677L;
public static int decorateColumn(Engine engine, IrisBiome target, IrisDecorationPart part, public static int decorateColumn(Engine engine, IrisBiome target, IrisDecorationPart part,
int xf, int zf, int realX, int realZ, int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng) { int height, int max, Hunk<BlockData> data, RNG rng,
long gSeed = engine.getSeedManager().getDecorator() + SEED_SALT - (part.ordinal() * PART_SALT); Runnable candidatesNullCallback) {
RNG gRNG = new RNG(gSeed); RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part));
KList<IrisDecorator> candidates = new KList<>(); IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ);
for (IrisDecorator d : target.getDecorators()) { if (decorator == null) {
try { candidatesNullCallback.run();
if (d.getPartOf().equals(part) && d.getBlockData(target, gRNG, realX, realZ, engine.getData()) != null) {
candidates.add(d);
}
} catch (Throwable e) {
Iris.reportError(e);
}
}
if (candidates.isEmpty()) {
decCandidatesNull.incrementAndGet();
return 0; return 0;
} }
IrisDecorator decorator = candidates.get(rng.nextInt(candidates.size()));
if (!decorator.isStacking()) { if (!decorator.isStacking()) {
return placeSimple(decorator, target, xf, zf, realX, realZ, height, max, data, rng, engine); DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData());
} else { return max > 1 ? 1 : 0;
return placeStacked(decorator, target, xf, zf, realX, realZ, height, max, data, rng, engine);
}
} }
private static int placeSimple(IrisDecorator decorator, IrisBiome target, return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData());
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng, Engine engine) {
BlockData bd = decorator.getBlockData100(target, rng, realX, height, realZ, engine.getData());
if (bd == null) {
return 0;
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (max > 2) {
data.set(xf, height + 2, zf, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (max > 1) {
data.set(xf, height + 1, zf, bd);
return 1;
}
return 0;
}
private static int placeStacked(IrisDecorator decorator, IrisBiome target,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng, Engine engine) {
int stack = decorator.getHeight(rng, realX, realZ, engine.getData());
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
int placed = 0;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max) {
break;
}
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(target, rng, realX, height + i, realZ, engine.getData())
: decorator.getBlockData100(target, rng, realX, height + i, realZ, engine.getData());
if (bd == null) {
break;
}
data.set(xf, h, zf, bd);
placed++;
}
return placed;
} }
} }
@@ -24,89 +24,42 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.PointedDripstone;
public class IrisCeilingDecorator extends IrisEngineDecorator { public class IrisCeilingDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisCeilingDecorator(Engine engine) { public IrisCeilingDecorator(Engine engine) {
super(engine, "Ceiling", IrisDecorationPart.CEILING); super(engine, "Ceiling", IrisDecorationPart.CEILING);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.CEILING));
} }
@BlockCoordinates @BlockCoordinates
@Override @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) { public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
RNG rng = getRNG(realX, realZ); Hunk<BlockData> data, IrisBiome biome, int height, int max) {
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ);
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (decorator != null) {
if (!decorator.isStacking()) { if (!decorator.isStacking()) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return; return;
} }
data.set(x, height, z, fixFaces(decorator.getBlockData100(biome, rng, realX, height, realZ, getData()), data, x, z, realX, height, realZ)); DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle());
} else {
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
if (stack == 1) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return; return;
} }
for (int i = 0; i < stack; i++) { DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
int h = height - i; opts.reset();
if (h < getEngine().getMinHeight()) { opts.caveSkipFluid = caveSkipFluid;
continue; DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, getEngine().getMinHeight(), data, rng, getData(), max, opts, getEngine().getMantle());
}
double threshold = (((double) i) / (double) (stack - 1));
BlockData bd = threshold >= decorator.getTopThreshold() ?
decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) :
decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
if (bd instanceof PointedDripstone) {
PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE;
if (stack == 2) {
th = PointedDripstone.Thickness.FRUSTUM;
if (i == stack - 1) {
th = PointedDripstone.Thickness.TIP;
}
} else {
if (i == stack - 1) {
th = PointedDripstone.Thickness.TIP;
} else if (i == stack - 2) {
th = PointedDripstone.Thickness.FRUSTUM;
}
}
bd = Material.POINTED_DRIPSTONE.createBlockData();
((PointedDripstone) bd).setThickness(th);
((PointedDripstone) bd).setVerticalDirection(BlockFace.DOWN);
}
if (caveSkipFluid && B.isFluid(data.get(x, h, z))) {
break;
}
data.set(x, h, z, bd);
}
}
}
} }
} }
@@ -18,102 +18,28 @@
package art.arcane.iris.engine.decorator; package art.arcane.iris.engine.decorator;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent; import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineDecorator; import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import lombok.Getter; import lombok.Getter;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.MultipleFacing;
public abstract class IrisEngineDecorator extends EngineAssignedComponent implements EngineDecorator { public abstract class IrisEngineDecorator extends EngineAssignedComponent implements EngineDecorator {
@Getter @Getter
private final IrisDecorationPart part; private final IrisDecorationPart part;
private final long seed;
private final long modX, modZ;
public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) { public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) {
super(engine, name + " Decorator"); super(engine, name + " Decorator");
this.part = part; this.part = part;
this.seed = getSeed() + 29356788 - (part.ordinal() * 10439677L);
this.modX = 29356788 ^ (part.ordinal() + 6);
this.modZ = 10439677 ^ (part.ordinal() + 1);
} }
@BlockCoordinates @BlockCoordinates
protected RNG getRNG(int x, int z) { protected RNG getRNG(int x, int z) {
long seed = DecoratorCore.partSeed(getSeed(), part);
long modX = 29356788L ^ (part.ordinal() + 6);
long modZ = 10439677L ^ (part.ordinal() + 1);
return new RNG(x * modX + z * modZ + seed); return new RNG(x * modX + z * modZ + seed);
} }
protected IrisDecorator getDecorator(RNG rng, IrisBiome biome, double realX, double realZ) {
KList<IrisDecorator> v = new KList<>();
RNG gRNG = new RNG(seed);
for (IrisDecorator i : biome.getDecorators()) {
try {
if (i.getPartOf().equals(part) && i.getBlockData(biome, gRNG, realX, realZ, getData()) != null) {
v.add(i);
}
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("PART OF: " + biome.getLoadFile().getAbsolutePath() + " HAS AN INVALID DECORATOR near 'partOf'!!!");
}
}
if (v.isNotEmpty()) {
return v.get(rng.nextInt(v.size()));
}
return null;
}
protected BlockData fixFaces(BlockData b, Hunk<BlockData> hunk, int rX, int rZ, int x, int y, int z) {
if (B.isVineBlock(b)) {
MultipleFacing data = (MultipleFacing) b.clone();
data.getFaces().forEach(f -> data.setFace(f, false));
boolean found = false;
for (BlockFace f : BlockFace.values()) {
if (!f.isCartesian())
continue;
int yy = y + f.getModY();
BlockData r = getEngine().getMantle().getMantle().get(x + f.getModX(), yy, z + f.getModZ(), BlockData.class);
if (r == null) {
r = EngineMantle.AIR;
}
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
found = true;
data.setFace(f, true);
continue;
}
int xx = rX + f.getModX();
int zz = rZ + f.getModZ();
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight())
continue;
r = hunk.get(xx, yy, zz);
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
found = true;
data.setFace(f, true);
}
}
if (!found)
data.setFace(BlockFace.DOWN, true);
return data;
}
return b;
}
} }
@@ -22,23 +22,30 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
public class IrisSeaFloorDecorator extends IrisEngineDecorator { public class IrisSeaFloorDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisSeaFloorDecorator(Engine engine) { public IrisSeaFloorDecorator(Engine engine) {
super(engine, "Sea Floor", IrisDecorationPart.SEA_FLOOR); super(engine, "Sea Floor", IrisDecorationPart.SEA_FLOOR);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_FLOOR));
} }
@BlockCoordinates @BlockCoordinates
@Override @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) { public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
RNG rng = getRNG(realX, realZ); RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (decorator != null) {
if (!decorator.isStacking()) { if (!decorator.isStacking()) {
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
@@ -47,31 +54,31 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
if (height >= 0 || height < getEngine().getHeight()) { if (height >= 0 || height < getEngine().getHeight()) {
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
} }
} else { return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData()); int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) { if (decorator.isScaleStack()) {
int maxStack = max - height; stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); } else {
} else stack = Math.min(stack, max - height); stack = Math.min(stack, max - height);
}
if (stack == 1) { if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return; return;
} }
int engineHeight = getEngine().getHeight();
for (int i = 0; i < stack; i++) { for (int i = 0; i < stack; i++) {
int h = height + i; int h = height + i;
if (h > max || h > getEngine().getHeight()) { if (h > max || h > engineHeight) {
continue; continue;
} }
double threshold = ((double) i) / (stack - 1); double threshold = ((double) i) / (stack - 1);
data.set(x, h, z, threshold >= decorator.getTopThreshold() ? data.set(x, h, z, threshold >= decorator.getTopThreshold()
decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
} }
} }
} }
}
}
@@ -22,32 +22,40 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisSeaSurfaceDecorator(Engine engine) { public IrisSeaSurfaceDecorator(Engine engine) {
super(engine, "Sea Surface", IrisDecorationPart.SEA_SURFACE); super(engine, "Sea Surface", IrisDecorationPart.SEA_SURFACE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_SURFACE));
} }
@BlockCoordinates @BlockCoordinates
@Override @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) { public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
RNG rng = getRNG(realX, realZ); RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (decorator != null) {
if (!decorator.isStacking()) { if (!decorator.isStacking()) {
if (height >= 0 || height < getEngine().getHeight()) { if (height >= 0 || height < getEngine().getHeight()) {
data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
} }
} else { return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData()); int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) { if (decorator.isScaleStack()) {
int maxStack = max - height; stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
stack = (int) Math.ceil((double) maxStack * ((double) stack / 100));
} }
if (stack == 1) { if (stack == 1) {
@@ -55,18 +63,16 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
return; return;
} }
int engineHeight = getEngine().getHeight();
for (int i = 0; i < stack; i++) { for (int i = 0; i < stack; i++) {
int h = height + i; int h = height + i;
if (h >= max || h >= getEngine().getHeight()) { if (h >= max || h >= engineHeight) {
continue; continue;
} }
double threshold = ((double) i) / (stack - 1); double threshold = ((double) i) / (stack - 1);
data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() ? data.set(x, h + 1, z, threshold >= decorator.getTopThreshold()
decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
}
}
} }
} }
} }
@@ -22,30 +22,44 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
public class IrisShoreLineDecorator extends IrisEngineDecorator { public class IrisShoreLineDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisShoreLineDecorator(Engine engine) { public IrisShoreLineDecorator(Engine engine) {
super(engine, "Shore Line", IrisDecorationPart.SHORE_LINE); super(engine, "Shore Line", IrisDecorationPart.SHORE_LINE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SHORE_LINE));
} }
@BlockCoordinates @BlockCoordinates
@Override @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) { public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
if (height != getDimension().getFluidHeight()) {
return;
}
double complexFluidHeight = getComplex().getFluidHeight();
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
return;
}
if (height == getDimension().getFluidHeight()) {
if (Math.round(getComplex().getHeightStream().get(realX1, realZ)) < getComplex().getFluidHeight() ||
Math.round(getComplex().getHeightStream().get(realX_1, realZ)) < getComplex().getFluidHeight() ||
Math.round(getComplex().getHeightStream().get(realX, realZ1)) < getComplex().getFluidHeight() ||
Math.round(getComplex().getHeightStream().get(realX, realZ_1)) < getComplex().getFluidHeight()
) {
RNG rng = getRNG(realX, realZ); RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (decorator != null) {
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return; return;
@@ -53,12 +67,15 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
if (!decorator.isStacking()) { if (!decorator.isStacking()) {
data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
} else { return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData()); int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) { if (decorator.isScaleStack()) {
int maxStack = max - height; stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); } else {
} else stack = Math.min(max - height, stack); stack = Math.min(max - height, stack);
}
if (stack == 1) { if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
@@ -68,13 +85,9 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
for (int i = 0; i < stack; i++) { for (int i = 0; i < stack; i++) {
int h = height + i; int h = height + i;
double threshold = ((double) i) / (stack - 1); double threshold = ((double) i) / (stack - 1);
data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() ? data.set(x, h + 1, z, threshold >= decorator.getTopThreshold()
decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
}
}
}
}
} }
} }
} }
@@ -18,29 +18,27 @@
package art.arcane.iris.engine.decorator; package art.arcane.iris.engine.decorator;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.PointedDripstone;
public class IrisSurfaceDecorator extends IrisEngineDecorator { public class IrisSurfaceDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisSurfaceDecorator(Engine engine) { public IrisSurfaceDecorator(Engine engine) {
super(engine, "Surface", IrisDecorationPart.NONE); super(engine, "Surface", IrisDecorationPart.NONE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE));
} }
protected IrisSurfaceDecorator(Engine engine, String name) { protected IrisSurfaceDecorator(Engine engine, String name) {
super(engine, name, IrisDecorationPart.NONE); super(engine, name, IrisDecorationPart.NONE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE));
} }
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) { protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) {
@@ -52,133 +50,33 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
@BlockCoordinates @BlockCoordinates
@Override @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) { public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
if (biome.getInferredType().equals(InferredType.SHORE) && height < getDimension().getFluidHeight()) { Hunk<BlockData> data, IrisBiome biome, int height, int max) {
int fluidHeight = getDimension().getFluidHeight();
if (biome.getInferredType().equals(InferredType.SHORE) && height < fluidHeight) {
return; return;
} }
BlockData bd, bdx; boolean underwater = height < fluidHeight && biome.getInferredType() != InferredType.CAVE;
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ);
bdx = data.get(x, height, z);
boolean underwater = height < getDimension().getFluidHeight() && biome.getInferredType() != InferredType.CAVE;
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator != null) { if (decorator == null || !isSlopeValid(decorator, realX, realZ)) {
if (!isSlopeValid(decorator, realX, realZ)) {
return; return;
} }
if (!decorator.isStacking()) { if (decorator.isStacking()) {
bd = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
if (!underwater) { opts.underwater = underwater;
if (!canGoOn(bd, bdx) && (!decorator.isForcePlace() && decorator.getForceBlock() == null)) { opts.fluidHeight = fluidHeight;
return; opts.caveSkipFluid = caveSkipFluid;
} DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
}
if (decorator.getForceBlock() != null) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, fixFaces(decorator.getForceBlock().getBlockData(getData()), data, x, z, realX, height, realZ));
} else if (!decorator.isForcePlace()) {
if (decorator.getWhitelist() != null && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(getData()).equals(bdx))) {
return;
}
if (decorator.getBlacklist() != null && decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(getData()).equals(bdx))) {
return;
}
}
if (bd instanceof Bisected) {
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.TOP);
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
data.set(x, height + 2, z, bd);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFaces(bd, data, x, z, realX, height + 1, realZ));
}
} else {
if (height < getDimension().getFluidHeight()) {
max = getDimension().getFluidHeight();
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
if (stack == 1) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return; return;
} }
for (int i = 0; i < stack; i++) { DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
int h = height + i; data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
double threshold = ((double) i) / (stack - 1);
bd = threshold >= decorator.getTopThreshold() ?
decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) :
decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
if (bd == null) {
break;
}
if (i == 0 && !underwater && !canGoOn(bd, bdx)) {
break;
}
if (underwater && height + 1 + i > getDimension().getFluidHeight()) {
break;
}
if (caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
break;
}
if (bd instanceof PointedDripstone) {
PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE;
if (stack == 2) {
th = PointedDripstone.Thickness.FRUSTUM;
if (i == stack - 1) {
th = PointedDripstone.Thickness.TIP;
}
} else {
if (i == stack - 1) {
th = PointedDripstone.Thickness.TIP;
} else if (i == stack - 2) {
th = PointedDripstone.Thickness.FRUSTUM;
}
}
bd = Material.POINTED_DRIPSTONE.createBlockData();
((PointedDripstone) bd).setThickness(th);
((PointedDripstone) bd).setVerticalDirection(BlockFace.UP);
}
data.set(x, height + 1 + i, z, bd);
}
}
}
} }
} }
@@ -59,8 +59,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
private static final AtomicLong decorateNoChange = new AtomicLong(); private static final AtomicLong decorateNoChange = new AtomicLong();
private static final AtomicLong decorateFloorNull = new AtomicLong(); private static final AtomicLong decorateFloorNull = new AtomicLong();
private static final java.util.concurrent.ConcurrentHashMap<String, AtomicLong> floorMatHisto = new java.util.concurrent.ConcurrentHashMap<>(); private static final java.util.concurrent.ConcurrentHashMap<String, AtomicLong> floorMatHisto = new java.util.concurrent.ConcurrentHashMap<>();
private static final AtomicLong decCandidatesNull = new AtomicLong();
private static final AtomicLong lastReportMs = new AtomicLong(0L); private static final AtomicLong lastReportMs = new AtomicLong(0L);
private static final AtomicLong reportCycle = new AtomicLong(0L); private static final AtomicLong reportCycle = new AtomicLong(0L);
private static final Runnable INC_DEC_CANDIDATES_NULL = () -> decCandidatesNull.incrementAndGet();
private final RNG rng; private final RNG rng;
private final EngineDecorator seaSurfaceDecorator; private final EngineDecorator seaSurfaceDecorator;
@@ -84,7 +86,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
+ " decPlaced=" + decoratePlaced.get() + " decPlaced=" + decoratePlaced.get()
+ " decNoChange=" + decorateNoChange.get() + " decNoChange=" + decorateNoChange.get()
+ " decFloorNull=" + decorateFloorNull.get() + " decFloorNull=" + decorateFloorNull.get()
+ " decCandidatesNull=" + FloatingDecorator.decCandidatesNull.get() + " decCandidatesNull=" + decCandidatesNull.get()
+ " decSkipNonAir=" + decorateSkippedNotAir.get() + " decSkipNonAir=" + decorateSkippedNotAir.get()
+ " decSkipNoInherit=" + decorateSkippedNoInherit.get() + " decSkipNoInherit=" + decorateSkippedNoInherit.get()
+ " decPhaseCols=" + decoratePhaseColumns.get() + " decPhaseCols=" + decoratePhaseColumns.get()
@@ -126,7 +128,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
decorateNoChange.set(0); decorateNoChange.set(0);
decorateFloorNull.set(0); decorateFloorNull.set(0);
floorMatHisto.clear(); floorMatHisto.clear();
FloatingDecorator.decCandidatesNull.set(0); decCandidatesNull.set(0);
MantleFloatingObjectComponent.resetObjectCounters(); MantleFloatingObjectComponent.resetObjectCounters();
} }
@@ -285,7 +287,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
} }
try { try {
RNG colRng = rng.nextParallelRNG((int) FloatingIslandSample.columnSeed(baseSeed, wx, wz)); RNG colRng = rng.nextParallelRNG((int) FloatingIslandSample.columnSeed(baseSeed, wx, wz));
int placed = FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng); int placed = FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng, INC_DEC_CANDIDATES_NULL);
if (placed > 0) { if (placed > 0) {
decoratePlaced.addAndGet(placed); decoratePlaced.addAndGet(placed);
} else { } else {
@@ -48,6 +48,7 @@ import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import java.awt.*; import java.awt.*;
import java.util.EnumMap;
@Accessors(chain = true) @Accessors(chain = true)
@NoArgsConstructor @NoArgsConstructor
@@ -77,6 +78,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private final transient AtomicCache<KList<CNG>> layerSeaHeightGenerators = new AtomicCache<>(); private final transient AtomicCache<KList<CNG>> layerSeaHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>(); private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<EnumMap<IrisDecorationPart, IrisDecorator[]>> decoratorBuckets = new AtomicCache<>();
private static final IrisDecorator[] EMPTY_BUCKET = new IrisDecorator[0];
@MinNumber(2) @MinNumber(2)
@Required @Required
@Desc("This is the human readable name for this biome. This can and should be different than the file name. This is not used for loading biomes in other objects.") @Desc("This is the human readable name for this biome. This can and should be different than the file name. This is not used for loading biomes in other objects.")
@@ -791,7 +794,23 @@ public class IrisBiome extends IrisRegistrant implements IRare {
return "biomes"; return "biomes";
} }
@Override public IrisDecorator[] getDecoratorBucket(IrisDecorationPart part) {
return decoratorBuckets.aquire(this::buildDecoratorBuckets).getOrDefault(part, EMPTY_BUCKET);
}
private EnumMap<IrisDecorationPart, IrisDecorator[]> buildDecoratorBuckets() {
EnumMap<IrisDecorationPart, KList<IrisDecorator>> staging = new EnumMap<>(IrisDecorationPart.class);
for (IrisDecorator d : decorators) {
staging.computeIfAbsent(d.getPartOf(), k -> new KList<>()).add(d);
}
EnumMap<IrisDecorationPart, IrisDecorator[]> result = new EnumMap<>(IrisDecorationPart.class);
for (IrisDecorationPart part : IrisDecorationPart.values()) {
KList<IrisDecorator> list = staging.get(part);
result.put(part, list == null ? EMPTY_BUCKET : list.toArray(EMPTY_BUCKET));
}
return result;
}
public String getTypeName() { public String getTypeName() {
return "Biome"; return "Biome";
} }
@@ -43,6 +43,8 @@ public class IrisDecorator {
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>(); private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
private final transient AtomicCache<KList<BlockData>> blockData = new AtomicCache<>(); private final transient AtomicCache<KList<BlockData>> blockData = new AtomicCache<>();
private final transient AtomicCache<KList<BlockData>> blockDataTops = new AtomicCache<>(); private final transient AtomicCache<KList<BlockData>> blockDataTops = new AtomicCache<>();
private final transient AtomicCache<BlockData[]> blockDataArray = new AtomicCache<>();
private final transient AtomicCache<BlockData[]> blockDataTopsArray = new AtomicCache<>();
@Desc("The varience dispersion is used when multiple blocks are put in the palette. Scatter scrambles them, Wispy shows streak-looking varience") @Desc("The varience dispersion is used when multiple blocks are put in the palette. Scatter scrambles them, Wispy shows streak-looking varience")
private IrisGeneratorStyle variance = NoiseStyle.STATIC.style(); private IrisGeneratorStyle variance = NoiseStyle.STATIC.style();
@Desc("Forcefully place this decorant anywhere it is supposed to go even if it should not go on a specific surface block. For example, you could force tallgrass to place on top of stone by using this.") @Desc("Forcefully place this decorant anywhere it is supposed to go even if it should not go on a specific surface block. For example, you could force tallgrass to place on top of stone by using this.")
@@ -134,24 +136,23 @@ public class IrisDecorator {
return palette; return palette;
} }
public BlockData getBlockData(IrisBiome b, RNG rng, double x, double z, IrisData data) { public boolean passesChanceGate(RNG rng, double x, double z, IrisData data) {
if (getBlockData(data).isEmpty()) { if (getBlockData(data).isEmpty()) {
Iris.warn("Empty Block Data for " + b.getName()); return false;
return null;
} }
double xx = x / style.getZoom(); double xx = x / style.getZoom();
double zz = z / style.getZoom(); double zz = z / style.getZoom();
return getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance;
}
if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { public BlockData getBlockData(IrisBiome b, RNG rng, double x, double z, IrisData data) {
if (!passesChanceGate(rng, x, z, data)) {
return null;
}
if (getBlockData(data).size() == 1) { if (getBlockData(data).size() == 1) {
return getBlockData(data).get(0); return getBlockData(data).get(0);
} }
return getVarianceGenerator(rng, data).fit(getBlockData(data), z, x);
return getVarianceGenerator(rng, data).fit(getBlockData(data), z, x); //X and Z must be switched
}
return null;
} }
public BlockData getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) { public BlockData getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) {
@@ -230,6 +231,42 @@ public class IrisDecorator {
}); });
} }
public BlockData[] getBlockDataArray(IrisData data) {
return blockDataArray.aquire(() -> {
KList<BlockData> list = getBlockData(data);
return list.toArray(new BlockData[0]);
});
}
public BlockData[] getBlockDataTopsArray(IrisData data) {
return blockDataTopsArray.aquire(() -> {
KList<BlockData> list = getBlockDataTops(data);
return list.toArray(new BlockData[0]);
});
}
public BlockData pickBlockData(RNG rng, IrisData data, double x, double z) {
BlockData[] arr = getBlockDataArray(data);
if (arr.length == 0) {
return null;
}
if (arr.length == 1) {
return arr[0];
}
return arr[Math.abs((int) getVarianceGenerator(rng, data).fit(0, arr.length - 1, z, x))];
}
public BlockData pickBlockDataTop(RNG rng, IrisData data, double x, double z) {
BlockData[] arr = getBlockDataTopsArray(data);
if (arr.length == 0) {
return pickBlockData(rng, data, x, z);
}
if (arr.length == 1) {
return arr[0];
}
return arr[Math.abs((int) getVarianceGenerator(rng, data).fit(0, arr.length - 1, z, x))];
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted") @SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isStacking() { public boolean isStacking() {
return getStackMax() > 1; return getStackMax() > 1;
@@ -0,0 +1,129 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class DecoratorCoreTest {
@Test
public void partSeed_differsByPartOrdinal() {
long base = 123456789L;
long s0 = DecoratorCore.partSeed(base, 0);
long s1 = DecoratorCore.partSeed(base, 1);
long s2 = DecoratorCore.partSeed(base, 2);
assertNotEquals(s0, s1);
assertNotEquals(s1, s2);
}
@Test
public void partSeed_isDeterministic() {
long base = 987654321L;
assertEquals(DecoratorCore.partSeed(base, 0), DecoratorCore.partSeed(base, 0));
assertEquals(DecoratorCore.partSeed(base, 3), DecoratorCore.partSeed(base, 3));
}
@Test
public void placeOpts_resetClearsAllFields() {
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.caveSkipFluid = true;
opts.underwater = true;
opts.fluidHeight = 99;
opts.reset();
assertFalse(opts.caveSkipFluid);
assertFalse(opts.underwater);
assertEquals(0, opts.fluidHeight);
}
@Test
public void scratchOpts_sameInstanceReturnedWithinThread() {
DecoratorCore.PlaceOpts a = DecoratorCore.SCRATCH_OPTS.get();
DecoratorCore.PlaceOpts b = DecoratorCore.SCRATCH_OPTS.get();
assertSame(a, b);
}
@Test
public void pickDecorator_emptyBucket_returnsNull() {
IrisBiome biome = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[0]);
IrisDecorator result = DecoratorCore.pickDecorator(
biome, IrisDecorationPart.NONE, new RNG(1L), new RNG(2L), data, 0.0, 0.0);
assertNull(result);
}
@Test
public void pickDecorator_nonePassChanceGate_returnsNull() {
IrisBiome biome = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
IrisDecorator d = mock(IrisDecorator.class);
when(d.passesChanceGate(any(RNG.class), anyDouble(), anyDouble(), any(IrisData.class))).thenReturn(false);
when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[]{d});
IrisDecorator result = DecoratorCore.pickDecorator(
biome, IrisDecorationPart.NONE, new RNG(1L), new RNG(2L), data, 0.0, 0.0);
assertNull(result);
}
@Test
public void pickDecorator_singleCandidate_alwaysReturnsThat() {
IrisBiome biome = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
IrisDecorator d = mock(IrisDecorator.class);
when(d.passesChanceGate(any(RNG.class), anyDouble(), anyDouble(), any(IrisData.class))).thenReturn(true);
when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[]{d});
RNG gRNG = new RNG(42L);
for (int t = 0; t < 50; t++) {
IrisDecorator result = DecoratorCore.pickDecorator(
biome, IrisDecorationPart.NONE, gRNG, new RNG(t * 13L + 7), data, 0.0, 0.0);
assertSame(d, result);
}
}
@Test
public void pickDecorator_multiplePassingCandidates_selectsUniformly() {
IrisBiome biome = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
int n = 4;
IrisDecorator[] decorators = new IrisDecorator[n];
for (int i = 0; i < n; i++) {
IrisDecorator d = mock(IrisDecorator.class);
when(d.passesChanceGate(any(RNG.class), anyDouble(), anyDouble(), any(IrisData.class))).thenReturn(true);
decorators[i] = d;
}
when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(decorators);
RNG gRNG = new RNG(99L);
int[] counts = new int[n];
int trials = 2000;
for (int t = 0; t < trials; t++) {
IrisDecorator picked = DecoratorCore.pickDecorator(
biome, IrisDecorationPart.NONE, gRNG, new RNG(t * 31L + 3), data, 0.0, 0.0);
assertNotNull(picked);
for (int i = 0; i < n; i++) {
if (picked == decorators[i]) {
counts[i]++;
break;
}
}
}
double expected = trials / (double) n;
for (int i = 0; i < n; i++) {
double deviation = Math.abs(counts[i] - expected) / expected;
assertTrue("Decorator " + i + " selected " + counts[i] + " times; expected ~" + (int) expected
+ " (deviation " + String.format("%.0f%%", deviation * 100) + ")", deviation < 0.20);
}
}
}
@@ -0,0 +1,224 @@
# Floating Island Shard Domain Warp Implementation Plan
Created: 2026-04-22
Status: VERIFIED
Approved: Yes
Iterations: 0
Worktree: No
Type: Feature
## Summary
**Goal:** In the Iris overworld pack (primary testbed: `roughplains.json`), make floating island shards read as less "mathy / sharply vertical" by enabling the already-wired per-layer domain warp on the two `floatingChildBiomes` entries.
**Architecture:** `IrisFloatingChildBiomes` already defines `wallWarpStyle` + `wallWarpAmplitude` fields and exposes them through `getWallWarpCng(...)`. `FloatingIslandSample.sample(...)` already consumes them at `Iris/core/.../FloatingIslandSample.java:220-254` — for each Y layer of an island column, it offsets the footprint XZ sample by a signed 3D noise value before re-testing against `footprintThreshold`. This gives organic bulge/recession on the side walls without touching the tops (driven by `topShapeMode=BIOME`) or the bottoms (driven by `bottomStyle`). The current pack doesn't set `wallWarpStyle`, so `useWarp == false` and walls are a straight extrusion of the 2D footprint.
**Tech Stack:** Iris pack JSON (no Java changes).
---
## Scope
### In Scope
- Edit `[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json`.
- Add `wallWarpStyle` and `wallWarpAmplitude` to **both** entries in `floatingChildBiomes` (`mushroom/forest` and `tropical/wilds`).
- Chosen values (answers to Q1/Q2/Q3): `wallWarpStyle = {"style":"SIMPLEX","zoom":0.25}`, `wallWarpAmplitude = 10.0`. Identical on both entries.
### Out of Scope
- Java source changes inside `Iris/core` — the warp is already wired. No `FloatingIslandSample.java` or `IrisFloatingChildBiomes.java` edits.
- Changing the schema default for `wallWarpStyle` from `null` to a non-null value — would silently change behavior for every existing pack; user explicitly chose "pack only".
- Audit of other biomes — Grep confirms `floatingChildBiomes` only appears in `roughplains.json` across the overworld pack.
- Any change to `topShapeMode`, `bottomStyle`, `carveStyle`, or `footprintStyle` (tops and bottoms already look correct).
- Code quality improvements to the warp algorithm (independent X/Z seeds, multi-octave / iq-style warp-the-warp, vertical frequency multiplier) — user declined the "code-side improvement too" option.
- `TotalFixes.md` entry — repo policy (AGENTS.md) says append to existing sections; will be handled during implementation as part of the DoD if the repo conventions require it.
---
## Approach
**Chosen:** Pack-only enablement of the existing wall-warp feature.
**Why:** All the plumbing already exists; the missing link is pack configuration. Zero code changes means zero risk of regressing any other floating-island behavior. The user owns this test pack, so they can tune values in follow-up without a code round-trip.
**Alternatives considered:**
- **Flip schema default of `wallWarpStyle` to a gentle SIMPLEX.** Rejected — silently changes behavior for every existing pack/user, and the point of having the feature opt-in is that visual style is a pack-authored choice.
- **Rewrite the warp in `FloatingIslandSample` for higher fidelity** (iq-style warp-the-warp, independent X/Z noise hashes, per-axis frequency scaling). Rejected — the current single-pass 3D warp already produces perturbed walls; the author of this ticket observed that the walls look "very sharply vertical" precisely because the feature is off in this pack, not because the existing implementation is inadequate. Revisit only if enabling the feature fails to soften the walls to the user's liking.
- **Per-entry tuning (mushroom gentler, tropical heavier).** Rejected — user selected "same warp for both". Revisit in a follow-up if the tropical shards (thicker at 112) still read as too vertical compared to mushroom (32).
---
## Context for Implementer
- **Target file (ONLY file to edit):** `/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json`
- **Do not touch:** any file under `Iris/core/src/...`. This is pack authoring, not code.
- **Schema reference:** `Iris/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java:170-176` documents `wallWarpStyle` and `wallWarpAmplitude` with accepted values, including the exact example the chosen config is based on (`{"style":"SIMPLEX","zoom":0.25}` for "gentle undulation"). Amplitude 10 falls between the labeled bands (`@Desc` line 175: "4..8 = gentle naturalization. 16+ = heavily meandering"), placing it in the lower-medium range — consistent with the user's stated Medium (~8-12 blocks) preference.
- **Field type:** `wallWarpAmplitude` is declared `double` (default `6.0`) at `IrisFloatingChildBiomes.java:176`. In JSON, write `10.0` (not `10`) to match the field's declared type and the existing default's literal form — Gson coerces either, but `10.0` is clearer for future pack authors comparing to the default.
- **Consumer reference:** `FloatingIslandSample.java:220-254` — confirms that when both `wallWarpStyle != null` and `wallWarpAmplitude > 0`, the per-layer XZ warp activates. No other branch needs to be exercised.
- **Field placement convention:** Existing entries order properties as `footprint* → picker* → altitude* → height → topShape → bottom* → maxThickness → carve* → inheritX → objectShrinkFactor`. Insert the two wall-warp fields **immediately after `maxThickness`** and **before `carveStyle`** — this keeps wall-shaping modifiers grouped and matches the @Desc-style progression from coarse (footprint) → fine (carve) detail. Preserve JSON formatting (4-space indent, trailing commas only where already present).
- **Gotchas:**
- JSON (not JSONC) — no comments, no trailing commas after the last key of an object.
- Iris CNG styles are case-sensitive — use `"SIMPLEX"` exactly, not `"simplex"`.
- The file currently has two entries in `floatingChildBiomes` — both must be updated. The whole point of the change is symmetric behavior on both.
### Domain context
- **What the warp does at runtime (to guide visual verification):** For each Y layer of a candidate island column, the generator computes `(sx, sz) = (wx, wz) + 10 × signed_noise3d(wx, wy, wz)` (X and Z offsets use decorrelated input coordinates), then re-samples the 2D footprint at `(sx, sz)` and re-tests against `footprintThreshold`. This means the horizontal silhouette of a shard can expand or contract by up to ~10 blocks per Y slice — the wall is no longer a vertical extrusion of the footprint. Zoom 0.25 makes the feature size of the warp ~25 blocks, which varies across the ~30-block CELLULAR shards enough to push each shard's silhouette into asymmetric bulges without high-frequency jitter.
- **Why tops/bottoms stay clean:** Tops are shaped by `topShapeMode = BIOME` (the target biome's own generators); bottoms by `bottomStyle` (CELLULAR zoom 0.3 / NOWHERE). The wall warp only perturbs the footprint test inside `sample()`, not the computed `topY` or `botY` — so the nicely-noised caps are untouched.
---
## Runtime Environment
This is a Minecraft server plugin — there is no UI to script and no HTTP surface. Verification requires running the Minecraft server with the edited pack and inspecting a generated chunk that contains a floating island.
- **Pack hot-reload:** Iris supports `/iris studio close` + `/iris studio open` or `/iris reload <pack>` on a live server; the user's workflow memory (`project_iris_floating_debug.md`) confirms this is the active testbed.
- **Deploy path:** The pack is already under `[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/` — editing the file **in place is the deploy step** (no build artifact to copy; per `AGENTS.md` never copy JARs into server plugin folders, but pack JSON edits are safe).
- **Health check / restart procedure:** Run `/iris studio close` in-game to close any open studio world, then `/iris create <test-world> overworld` (or re-open the existing studio world) so the floating child entries re-initialize. Teleport to a floating island column and visually inspect walls.
---
## Assumptions
- Both `floatingChildBiomes` entries are intended to benefit from wall warping — supported by the user's phrasing "the shards" (plural) and Q3 choice "Same warp for both". Task 1 depends on this.
- `wallWarpStyle = SIMPLEX, zoom 0.25` + `wallWarpAmplitude = 10.0` lands in the lower-medium range between the `@Desc` labels "4..8 = gentle" and "16+ = heavily meandering" on `IrisFloatingChildBiomes.java:175`. Task 1 depends on this.
- Iris correctly serializes `wallWarpStyle` into an `IrisGeneratorStyle` on pack load. Supported by `IrisFloatingChildBiomes.getWallWarpCng(...)` reading it via `getWallWarpStyle()` and `FloatingIslandSample:221` consuming the CNG (no reflection path or gated loader). Task 1 depends on this.
- The generator does not cache island solidity in a way that would persist pre-change results — `FloatingIslandSample` uses `CHUNK_MEMO` ThreadLocal which is cleared on each chunk build. Task 1 (verification step) depends on this; if stale chunks appear post-edit, regenerate or use a fresh world.
- No other pack files reference `roughplains` via snippet-inheritance that would also need edits. Supported by Grep of `floatingChildBiomes` returning only `roughplains.json`. Task 1 depends on this.
---
## Risks and Mitigations
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|-----------|
| JSON syntax error (missing comma, wrong quote) breaks pack load | Low | Medium — pack won't load until fixed | Validate with `python3 -m json.tool <file>` after edit; check Iris console for load errors on server restart |
| Amplitude 10 on the taller tropical/wilds shards (112 thick) still reads as too vertical | Medium | Low — purely aesthetic, follow-up tune | Document in "Deferred Ideas" the fallback of bumping tropical amplitude to ~14 or adding scale-aware per-entry tuning |
| Warp displaces thin edge columns entirely out of the footprint, shrinking island footprint noticeably | Medium | Low — this is the desired effect but could feel over-aggressive at the edges | Amplitude 10 is moderate; if islands feel too shrunken, reduce to 6-8 on follow-up |
| Enabling warp adds 2 extra 3D noise samples per Y layer inside islands — perf hit on pregen | Low | Low — bounded by island coverage × max thickness; CNG samples are cheap and already-cached | No action needed; if pregen CPS regresses (see `project_iris_pregen_perf.md`), revisit by disabling warp on one entry |
| The two entries producing identical warp patterns at the same world XZ (because the warp seed mask `0xA117BA17E0FL` is constant across entries) makes them look correlated | Low | Very low — entries don't spatially overlap (picker noise chooses one per column) | Mention as a known-limitation in "Deferred Ideas"; if user ever wants decorrelated warp, add a per-entry seed salt. Not in scope. |
---
## Goal Verification
### Truths
1. `wallWarpStyle` and `wallWarpAmplitude` are set on **both** entries of `floatingChildBiomes` in `roughplains.json`.
2. Both entries use **identical** warp config: `{"style":"SIMPLEX","zoom":0.25}` and amplitude `10.0`.
3. `roughplains.json` remains valid JSON (parses without error).
4. The placement of the two new keys is inside each floating entry object, adjacent to other wall/interior shaping fields (between `maxThickness` and `carveStyle`).
5. No files under `Iris/core/` are modified (enforced by `git diff --stat` showing only the pack JSON changed, if the user is tracking the pack in git).
6. At runtime, a generated floating island in a roughplains region shows horizontal wall perturbation (bulge/recede) rather than a straight XZ extrusion of the 2D footprint. (Manual verification — see scenario MV-001 below.)
### Artifacts
- `[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json` — the edit
- `Iris/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java:170-176` — schema source (read-only reference, not modified)
- `Iris/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java:220-254` — consumer source (read-only reference, not modified)
---
## Manual Verification Scenarios
No browser automation path exists for a Minecraft server plugin. Scenarios are documented for the user to run in-game.
### MV-001: Wall warp visually active on roughplains floating islands
**Priority:** Critical
**Preconditions:** Test world generated (or fresh chunks loaded) using the overworld pack with the edited `roughplains.json`. Fly mode + `/iris studio` access.
**Mapped Tasks:** Task 1
| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | Restart the server (or `/iris reload overworld`) so the pack JSON is re-parsed | Iris console logs pack load with no errors mentioning `wallWarpStyle` or `IrisFloatingChildBiomes` |
| 2 | Generate or teleport to a roughplains region (Y ≥ ~180 where floating islands live) | At least one floating island column visible |
| 3 | Fly around the side of a floating island shard | Side walls show horizontal bulges/recesses as Y changes — silhouette is NOT a perfect vertical extrusion of the top outline |
| 4 | Compare top surface with earlier screenshots/observations | Top biome surface (mushroom/forest or tropical/wilds) looks the same as before — tops unchanged |
| 5 | Compare bottom tail with earlier observations | Bottom tail (dripping/roots) looks the same as before — bottoms unchanged |
### MV-002: Pack loads as valid JSON
**Priority:** Critical
**Preconditions:** Edit applied.
**Mapped Tasks:** Task 1
| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | Run `python3 -m json.tool <pack-path>` from shell | Exits 0, no parse error |
| 2 | Grep the file for `wallWarpStyle` | Returns exactly 2 matches (one per floating entry) |
| 3 | Grep the file for `wallWarpAmplitude` | Returns exactly 2 matches |
---
## Progress Tracking
- [x] Task 1: Enable wall warp on both `floatingChildBiomes` entries in `roughplains.json`
**Total Tasks:** 1 | **Completed:** 1 | **Remaining:** 0
---
## Implementation Tasks
### Task 1: Enable wall warp on both floating child biomes in roughplains.json
**Objective:** Add `wallWarpStyle` and `wallWarpAmplitude` to both entries of `floatingChildBiomes` in `roughplains.json`, identical values on both.
**Dependencies:** None
**Mapped Scenarios:** MV-001, MV-002
**Files:**
- Modify: `/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json`
**Key Decisions / Notes:**
- Insert two keys in each entry, immediately after `"maxThickness": N,` and before `"carveStyle": { ... },`.
- Exact JSON to insert (indentation matches surrounding 4-space style — one leading space for the key position based on the file's existing alignment):
```json
"wallWarpStyle": {
"style": "SIMPLEX",
"zoom": 0.25
},
"wallWarpAmplitude": 10.0,
```
- Do this on the `mushroom/forest` entry (currently the block starting at line 255) AND on the `tropical/wilds` entry (starting at line 277).
- Do NOT reorder any existing fields.
- Do NOT add `wallWarpStyle` to the parent biome fields — it is per-entry on `IrisFloatingChildBiomes`, not on `IrisBiome`.
- Preserve the existing (minor) indentation quirks in the file as-is; this is a targeted additive edit.
- No performance concern — the warp adds two noise samples per Y layer inside island columns only; CNG results are cached per-entry via `AtomicCache` in `IrisFloatingChildBiomes`.
**Definition of Done:**
- [ ] Both `floatingChildBiomes` entries contain `wallWarpStyle: {"style":"SIMPLEX","zoom":0.25}` and `wallWarpAmplitude: 10.0`.
- [ ] Pre-edit sanity: `grep -rl 'floatingChildBiomes' /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/` returns only `roughplains.json` (no other pack file needs the same edit).
- [ ] `python3 -m json.tool <pack-path>` exits 0 (valid JSON).
- [ ] `grep -c 'wallWarpStyle' <pack-path>` returns `2`.
- [ ] `grep -c 'wallWarpAmplitude' <pack-path>` returns `2`.
- [ ] MV-001 passes end-to-end on a live test world: side walls of roughplains floating shards exhibit horizontal bulge/recession, while tops and bottoms look unchanged from prior observation.
- [ ] No files under `Iris/core/` or any other plugin source tree are modified.
**Verify:**
```bash
# JSON validity
python3 -m json.tool /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json > /dev/null
# Field counts
grep -c 'wallWarpStyle' /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json
grep -c 'wallWarpAmplitude' /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json
```
Then MV-001 via manual in-game verification on the user's running test server.
---
## Deferred Ideas
- **Per-entry scale-aware tuning:** If the tropical/wilds shards (112 thick) still read as too vertical compared to mushroom/forest (32 thick), bump tropical's `wallWarpAmplitude` to ~14 while leaving mushroom at 10.
- **Per-entry warp decorrelation:** Both entries currently sample `wallWarp` noise from the same RNG mask (`0xA117BA17E0FL` in `IrisFloatingChildBiomes:81`). Entries never spatially overlap (the picker routes each column to exactly one), but if visually noticeable correlation ever appears where biomes are adjacent, add a per-entry seed salt.
- **Higher-fidelity warp (iq-style):** Chain a second noise sample: `P' = P + A·n1(P); P'' = P' + B·n2(P');` then sample footprint at `P''`. Only pursue if the single-pass warp is not expressive enough after Task 1 is tuned.
- **Vertical frequency scaling:** Expose a `wallWarpVerticalScale` so the warp varies faster with Y than with XZ — would make adjacent Y slices more different, reducing any residual vertical banding without changing horizontal feature size.
- **Schema default flip:** Changing `IrisFloatingChildBiomes.wallWarpStyle` default from `null` to a gentle SIMPLEX. Out of scope this plan; would silently change behavior for any existing pack.