This commit is contained in:
Brian Neumann-Fopiano
2026-04-23 22:44:52 -04:00
parent 1968ef2f2c
commit 821cc027db
15 changed files with 1123 additions and 279 deletions
@@ -42,11 +42,6 @@ public class IslandObjectPlacer implements IObjectPlacer {
private final int chunkMinIslandBottomY;
private final int anchorY;
private final AnchorFace face;
private int writesAttempted;
private int writesDroppedBelow;
private int writesDroppedOverhang;
private int writesDroppedAboveBottom;
private int writesDroppedBottomOverhang;
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorTopY) {
this(wrapped, samples, minX, minZ, anchorTopY, AnchorFace.TOP);
@@ -64,9 +59,13 @@ public class IslandObjectPlacer implements IObjectPlacer {
for (FloatingIslandSample s : samples) {
if (s != null) {
int ty = s.topY();
if (ty > maxTopY) maxTopY = ty;
if (ty > maxTopY) {
maxTopY = ty;
}
int by = s.bottomY();
if (by >= 0 && by < minBottomY) minBottomY = by;
if (by >= 0 && by < minBottomY) {
minBottomY = by;
}
}
}
this.chunkMaxIslandTopY = maxTopY;
@@ -105,34 +104,7 @@ public class IslandObjectPlacer implements IObjectPlacer {
return mask;
}
public int getWritesAttempted() {
return writesAttempted;
}
public int getWritesDroppedBelow() {
return writesDroppedBelow;
}
public int getWritesDroppedOverhang() {
return writesDroppedOverhang;
}
public int getWritesDroppedAboveBottom() {
return writesDroppedAboveBottom;
}
public int getWritesDroppedBottomOverhang() {
return writesDroppedBottomOverhang;
}
private boolean shouldSkipAirColumn(int x, int y, int z) {
return shouldSkipAirColumn(x, y, z, true);
}
private boolean shouldSkipAirColumn(int x, int y, int z, boolean countWrite) {
if (countWrite) {
writesAttempted++;
}
int xf = x - minX;
int zf = z - minZ;
if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) {
@@ -142,37 +114,22 @@ public class IslandObjectPlacer implements IObjectPlacer {
return false;
}
if (y >= anchorY) {
if (countWrite) {
writesDroppedAboveBottom++;
}
return true;
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
if (countWrite) {
writesDroppedBelow++;
}
return true;
}
if (!overhangAllowed[idx]) {
if (countWrite) {
writesDroppedOverhang++;
}
return true;
}
} else {
if (y >= anchorY) {
if (countWrite) {
writesDroppedBottomOverhang++;
}
return true;
}
if (!overhangAllowed[idx]) {
if (countWrite) {
writesDroppedBottomOverhang++;
}
return true;
}
}
@@ -180,27 +137,18 @@ public class IslandObjectPlacer implements IObjectPlacer {
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
if (countWrite) {
writesDroppedBelow++;
}
return true;
}
} else {
if (y >= anchorY) {
if (countWrite) {
writesDroppedBottomOverhang++;
}
return true;
}
}
if (countWrite) {
writesDroppedOverhang++;
}
return true;
}
public boolean canWriteObjectBlock(int x, int y, int z) {
return !shouldSkipAirColumn(x, y, z, false);
return !shouldSkipAirColumn(x, y, z);
}
private @Nullable FloatingIslandSample sampleAt(int x, int z) {
@@ -216,7 +164,9 @@ public class IslandObjectPlacer implements IObjectPlacer {
public int getHighest(int x, int z, IrisData data) {
FloatingIslandSample s = sampleAt(x, z);
if (face == AnchorFace.TOP) {
if (s != null) return s.topY();
if (s != null) {
return s.topY();
}
return chunkMaxIslandTopY;
}
if (s != null) {
@@ -53,74 +53,17 @@ import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@ComponentFlag(ReservedFlag.FLOATING_OBJECT)
public class MantleFloatingObjectComponent extends IrisMantleComponent {
public static final AtomicLong objectsAttempted = new AtomicLong();
public static final AtomicLong objectsPlaced = new AtomicLong();
public static final AtomicLong objectsSkippedNoFlat = new AtomicLong();
public static final AtomicLong objectsSkippedNoInterior = new AtomicLong();
public static final AtomicLong objectsRelaxed = new AtomicLong();
public static final AtomicLong objectsSkippedShrink = new AtomicLong();
public static final AtomicLong objectsSkippedNullObj = new AtomicLong();
public static final AtomicLong writesAttemptedTotal = new AtomicLong();
public static final AtomicLong writesDroppedBelowTotal = new AtomicLong();
public static final AtomicLong writesDroppedOverhangTotal = new AtomicLong();
public static final AtomicLong objectsInvertedAttempted = new AtomicLong();
public static final AtomicLong objectsInvertedPlaced = new AtomicLong();
public static final AtomicLong objectsInvertedSkippedNoFlat = new AtomicLong();
public static final AtomicLong objectsInvertedFallbackNoInterior = new AtomicLong();
public static final AtomicLong objectsInvertedSkippedShrink = new AtomicLong();
public static final AtomicLong objectsInvertedSkippedNullObj = new AtomicLong();
public static final AtomicLong writesDroppedAboveBottomTotal = new AtomicLong();
public static final AtomicLong writesDroppedBottomOverhangTotal = new AtomicLong();
private static final int MIN_FOOTPRINT_CELLS_CHECKED = 3;
private static final int INVERTED_PICK_ATTEMPTS = 8;
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
public static final ConcurrentHashMap<String, AtomicLong> anchorYHisto = new ConcurrentHashMap<>();
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
}
public static void resetObjectCounters() {
objectsAttempted.set(0);
objectsPlaced.set(0);
objectsSkippedNoFlat.set(0);
objectsSkippedNoInterior.set(0);
objectsRelaxed.set(0);
objectsSkippedShrink.set(0);
objectsSkippedNullObj.set(0);
writesAttemptedTotal.set(0);
writesDroppedBelowTotal.set(0);
writesDroppedOverhangTotal.set(0);
anchorYHisto.clear();
objectsInvertedAttempted.set(0);
objectsInvertedPlaced.set(0);
objectsInvertedSkippedNoFlat.set(0);
objectsInvertedFallbackNoInterior.set(0);
objectsInvertedSkippedShrink.set(0);
objectsInvertedSkippedNullObj.set(0);
writesDroppedAboveBottomTotal.set(0);
writesDroppedBottomOverhangTotal.set(0);
}
private static void recordWriteStats(IslandObjectPlacer islandPlacer) {
int attempted = islandPlacer.getWritesAttempted();
int below = islandPlacer.getWritesDroppedBelow();
int overhang = islandPlacer.getWritesDroppedOverhang();
writesAttemptedTotal.addAndGet(attempted);
writesDroppedBelowTotal.addAndGet(below);
writesDroppedOverhangTotal.addAndGet(overhang);
}
private static void recordInvertedWriteStats(IslandObjectPlacer islandPlacer) {
writesDroppedAboveBottomTotal.addAndGet(islandPlacer.getWritesDroppedAboveBottom());
writesDroppedBottomOverhangTotal.addAndGet(islandPlacer.getWritesDroppedBottomOverhang());
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
@@ -150,12 +93,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> entryColumns = new IdentityHashMap<>();
IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> bottomEntryColumns = new IdentityHashMap<>();
for (int i = 0; i < 256; i++) {
FloatingIslandSample s = samples[i];
if (s == null || s.entry == null) {
continue;
}
entryColumns.computeIfAbsent(s.entry, e -> new KList<>()).add(i);
IrisFloatingChildBiomes bottomEntry = s.bottomEntry();
if (bottomEntry != null) {
bottomEntryColumns.computeIfAbsent(bottomEntry, e -> new KList<>()).add(i);
}
}
for (Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : entryColumns.entrySet()) {
@@ -193,11 +141,20 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
}
}
}
for (Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : bottomEntryColumns.entrySet()) {
IrisFloatingChildBiomes entry = ec.getKey();
KList<Integer> columns = ec.getValue();
if (columns.isEmpty()) {
continue;
}
IrisBiome parent = complex.getTrueBiomeStream().get(minX + (columns.get(0) & 15), minZ + (columns.get(0) >> 4));
IrisBiome target = entry.getRealBiome(parent, data);
KList<IrisObjectPlacement> bottom = target != null ? entry.resolveBottomObjects(target) : null;
if (bottom != null && !bottom.isEmpty()) {
if (interior == null) {
interior = interiorColumns(samples, columns);
}
KList<Integer> interior = interiorColumns(samples, columns);
for (IrisObjectPlacement placement : bottom) {
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
}
@@ -213,24 +170,20 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
objectsAttempted.incrementAndGet();
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
objectsSkippedNullObj.incrementAndGet();
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
}
@@ -249,7 +202,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsPlaced.incrementAndGet();
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -265,25 +217,21 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
objectsAttempted.incrementAndGet();
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
objectsSkippedNullObj.incrementAndGet();
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
}
@@ -292,26 +240,20 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
if (interior.isEmpty()) {
objectsSkippedNoInterior.incrementAndGet();
}
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int pickedXf = pickedKey & 15;
int pickedZf = pickedKey >> 4;
FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf];
if (pickedSample == null) {
objectsSkippedNoFlat.incrementAndGet();
continue;
}
int pickTopY = pickedSample.topY();
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 2)) {
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 4)) {
objectsSkippedNoFlat.incrementAndGet();
continue;
}
objectsRelaxed.incrementAndGet();
}
int wx = minX + pickedXf - fp.getTallestKx();
@@ -338,9 +280,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsPlaced.incrementAndGet();
recordAnchorYHisto(pickTopY);
recordWriteStats(islandPlacer);
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -356,25 +295,21 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
objectsInvertedAttempted.incrementAndGet();
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
objectsInvertedSkippedNullObj.incrementAndGet();
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
objectsInvertedSkippedShrink.incrementAndGet();
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
objectsInvertedSkippedShrink.incrementAndGet();
continue;
}
}
@@ -385,9 +320,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
IrisObjectRotation invertedRotation = IrisObjectRotation.xFlip180WithY(invertedYRotation);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
if (interior.isEmpty()) {
objectsInvertedFallbackNoInterior.incrementAndGet();
}
int pickedXf = -1;
int pickedZf = -1;
@@ -416,7 +348,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
break;
}
if (!foundBottomAnchor) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
@@ -444,8 +375,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsInvertedPlaced.incrementAndGet();
recordInvertedWriteStats(islandPlacer);
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -554,20 +483,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return touchedChunkEdge;
}
private static void recordAnchorYHisto(int topY) {
String bucket = String.valueOf(topY >> 3);
if (anchorYHisto.size() < 32) {
anchorYHisto.computeIfAbsent(bucket, k -> new AtomicLong()).incrementAndGet();
} else {
AtomicLong existing = anchorYHisto.get(bucket);
if (existing != null) {
existing.incrementAndGet();
} else {
anchorYHisto.computeIfAbsent("other", k -> new AtomicLong()).incrementAndGet();
}
}
}
private static KList<Integer> interiorColumns(FloatingIslandSample[] samples, KList<Integer> columns) {
KList<Integer> interior = new KList<>();
for (int key : columns) {
@@ -49,6 +49,8 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.util.IdentityHashMap;
import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<BlockData> {
@@ -132,7 +134,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
bottomDepths[i] = -1;
}
int depth = 0;
IdentityHashMap<IrisFloatingChildBiomes, Integer> depthByEntry = new IdentityHashMap<>();
int max = Math.min(sample.topIdx, sample.solidMask.length - 1);
for (int k = 0; k <= max; k++) {
if (!sample.solidMask[k]) {
@@ -142,7 +144,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (y < 0 || y >= chunkHeight) {
continue;
}
bottomDepths[k] = depth++;
IrisFloatingChildBiomes entry = sample.entryAt(k);
int depth = depthByEntry.getOrDefault(entry, 0);
bottomDepths[k] = depth;
depthByEntry.put(entry, depth + 1);
}
return bottomDepths;
@@ -170,6 +175,30 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
return block == null ? fallbackSolid : block;
}
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, long colSeed, int paletteDepth, IrisData data, IrisComplex complex) {
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
int entrySeed = entry == null || entry.getBiome() == null ? 0 : entry.getBiome().hashCode();
RNG layerRng = rng.nextParallelRNG((int) (colSeed ^ 0x7A4E ^ entrySeed));
KList<BlockData> topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
if (topBlocks == null || topBlocks.isEmpty()) {
topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
}
KList<BlockData> bottomBlocks = generateBottomPaletteLayers(entry, dimension, wx, wz, layerRng, paletteDepth, data, complex);
return new PaletteContext(topBlocks, bottomBlocks, B.get("minecraft:stone"));
}
private static final class PaletteContext {
private final KList<BlockData> topBlocks;
private final KList<BlockData> bottomBlocks;
private final BlockData fallbackSolid;
private PaletteContext(KList<BlockData> topBlocks, KList<BlockData> bottomBlocks, BlockData fallbackSolid) {
this.topBlocks = topBlocks;
this.bottomBlocks = bottomBlocks;
this.fallbackSolid = fallbackSolid;
}
}
public IrisFloatingChildBiomeModifier(Engine engine) {
super(engine, "FloatingChildBiomes");
rng = new RNG(engine.getSeedManager().getTerrain() ^ 0x7EB0A73F1DCE514DL);
@@ -198,20 +227,11 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
continue;
}
IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data);
long colSeed = FloatingIslandSample.columnSeed(baseSeed, wx, wz);
RNG layerRng = rng.nextParallelRNG((int) (colSeed ^ 0x7A4E));
int paletteDepth = Math.max(4, sample.solidCount + 4);
KList<BlockData> blocks = target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
if (blocks == null || blocks.isEmpty()) {
blocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
}
KList<BlockData> bottomBlocks = generateBottomPaletteLayers(entry, dimension, wx, wz, layerRng, paletteDepth, data, complex);
BlockData fallbackSolid = B.get("minecraft:stone");
int[] bottomDepths = usesBottomPalette(entry) ? bottomDepths(sample, chunkHeight) : null;
int depth = 0;
IdentityHashMap<IrisFloatingChildBiomes, PaletteContext> paletteContexts = new IdentityHashMap<>();
IdentityHashMap<IrisFloatingChildBiomes, Integer> topDepthByEntry = new IdentityHashMap<>();
int[] bottomDepths = bottomDepths(sample, chunkHeight);
for (int k = sample.topIdx; k >= 0; k--) {
if (!sample.solidMask[k]) {
continue;
@@ -220,14 +240,22 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (y < 0 || y >= chunkHeight) {
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k);
PaletteContext paletteContext = paletteContexts.get(entry);
if (paletteContext == null) {
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, colSeed, paletteDepth, data, complex);
paletteContexts.put(entry, paletteContext);
}
int depth = topDepthByEntry.getOrDefault(entry, 0);
int bottomDepth = bottomDepths == null || bottomDepths[k] < 0 ? depth : bottomDepths[k];
BlockData block = selectPaletteBlock(entry, blocks, bottomBlocks, depth, bottomDepth, fallbackSolid);
BlockData block = selectPaletteBlock(entry, paletteContext.topBlocks, paletteContext.bottomBlocks, depth, bottomDepth, paletteContext.fallbackSolid);
if (block != null) {
output.set(xf, y, zf, block);
}
depth++;
topDepthByEntry.put(entry, depth + 1);
}
IrisFloatingChildBiomes entry = sample.entry;
Integer localFluidHeight = entry.getLocalFluidHeight();
if (localFluidHeight != null && localFluidHeight > 0) {
BlockData fluid = B.get(entry.getFluidBlock());
@@ -256,9 +284,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
}
}
if (target != null) {
writeIslandSkyBiome(target, wx, wz, sample, chunkHeight);
}
writeIslandSkyBiomes(parent, wx, wz, sample, chunkHeight, data);
}
}
@@ -339,23 +365,41 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
}
}
private void writeIslandSkyBiome(IrisBiome target, int wx, int wz, FloatingIslandSample sample, int chunkHeight) {
private void writeIslandSkyBiomes(IrisBiome parent, int wx, int wz, FloatingIslandSample sample, int chunkHeight, IrisData data) {
try {
MatterBiomeInject matter;
if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, wx, 0, wz);
matter = BiomeInjectMatter.get(INMS.get().getBiomeBaseIdForKey(getDimension().getLoadKey() + ":" + custom.getId()));
} else {
Biome v = target.getSkyBiome(rng, wx, 0, wz);
matter = BiomeInjectMatter.get(v);
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
for (int k = 0; k <= sample.topIdx; k++) {
if (!sample.solidMask[k]) {
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k);
MatterBiomeInject matter = matterByEntry.get(entry);
if (matter == null) {
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
if (target == null) {
continue;
}
matter = createSkyBiomeMatter(target, wx, wz);
matterByEntry.put(entry, matter);
}
int yFrom = Math.max(0, sample.islandBaseY);
int yTo = Math.min(chunkHeight - 1, sample.islandBaseY + sample.topIdx);
for (int y = yFrom; y <= yTo; y += 4) {
getEngine().getMantle().getMantle().set(wx, y, wz, matter);
}
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
}
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, wx, 0, wz);
return BiomeInjectMatter.get(INMS.get().getBiomeBaseIdForKey(getDimension().getLoadKey() + ":" + custom.getId()));
}
Biome v = target.getSkyBiome(rng, wx, 0, wz);
return BiomeInjectMatter.get(v);
}
}
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -39,6 +40,13 @@ public final class FloatingIslandSample {
public static final int REJECT_CLUSTER = REJECT_NO_SEED;
private static final double EDGE_ROUNDING_BAND = 0.28;
private static final double FOOTPRINT_PINHOLE_REPAIR_MARGIN = 0.16;
private static final int PINHOLE_CARDINAL_FILL = 4;
private static final int PINHOLE_TOTAL_FILL = 7;
private static final int CARVE_CARDINAL_SUPPORT = 2;
private static final int CARVE_TOTAL_SUPPORT = 4;
private static final double CARVE_SHELL_FRACTION = 0.14D;
private static final double CARVE_MAX_VERTICAL_RUN_FRACTION = 0.18D;
private static final ThreadLocal<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]);
private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
@@ -93,21 +101,31 @@ public final class FloatingIslandSample {
public final int topIdx;
public final int solidCount;
public final boolean[] solidMask;
private final IrisFloatingChildBiomes[] entryMask;
private transient int cachedBottomIdx = -2;
private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) {
this(entry, islandBaseY, thickness, topIdx, solidCount, solidMask, null);
}
private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask, IrisFloatingChildBiomes[] entryMask) {
this.entry = entry;
this.islandBaseY = islandBaseY;
this.thickness = thickness;
this.topIdx = topIdx;
this.solidCount = solidCount;
this.solidMask = solidMask;
this.entryMask = entryMask;
}
static FloatingIslandSample constructForTest(int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) {
return new FloatingIslandSample(null, islandBaseY, thickness, topIdx, solidCount, solidMask);
}
static FloatingIslandSample constructForTest(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask, IrisFloatingChildBiomes[] entryMask) {
return new FloatingIslandSample(entry, islandBaseY, thickness, topIdx, solidCount, solidMask, entryMask);
}
public int topY() {
return islandBaseY + topIdx;
}
@@ -125,6 +143,27 @@ public final class FloatingIslandSample {
return cachedBottomIdx == -1 ? -1 : islandBaseY + cachedBottomIdx;
}
public IrisFloatingChildBiomes entryAt(int maskIndex) {
if (entryMask != null && maskIndex >= 0 && maskIndex < entryMask.length && entryMask[maskIndex] != null) {
return entryMask[maskIndex];
}
return entry;
}
public IrisFloatingChildBiomes bottomEntry() {
int bottomY = bottomY();
if (bottomY < 0) {
return entry;
}
return entryAt(bottomY - islandBaseY);
}
public boolean hasMergedEntries() {
return entryMask != null;
}
public static long columnSeed(long baseSeed, int wx, int wz) {
return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L);
}
@@ -135,6 +174,10 @@ public final class FloatingIslandSample {
return reject(REJECT_NO_ENTRIES);
}
if (parent.isMergeFloatingChildBiomes()) {
return sampleMerged(parent, entries, wx, wz, chunkHeight, baseSeed, data, engine);
}
IrisFloatingChildBiomes entry;
if (entries.size() == 1) {
entry = entries.getFirst();
@@ -153,6 +196,77 @@ public final class FloatingIslandSample {
}
}
return sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine);
}
private static FloatingIslandSample sampleMerged(IrisBiome parent, KList<IrisFloatingChildBiomes> entries, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
KList<FloatingIslandSample> samples = new KList<>();
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (IrisFloatingChildBiomes entry : entries) {
FloatingIslandSample sample = sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine);
if (sample == null) {
continue;
}
int bottomY = sample.bottomY();
if (bottomY < 0) {
continue;
}
samples.add(sample);
if (bottomY < minY) {
minY = bottomY;
}
int topY = sample.topY();
if (topY > maxY) {
maxY = topY;
}
}
if (samples.isEmpty() || minY > maxY) {
return reject(REJECT_NO_SEED);
}
int thickness = maxY - minY + 1;
boolean[] solidMask = new boolean[thickness];
IrisFloatingChildBiomes[] entryMask = new IrisFloatingChildBiomes[thickness];
int[] entryTopY = new int[thickness];
Arrays.fill(entryTopY, Integer.MIN_VALUE);
int solidCount = 0;
for (FloatingIslandSample sample : samples) {
int sampleTopY = sample.topY();
for (int i = 0; i < sample.solidMask.length; i++) {
if (!sample.solidMask[i]) {
continue;
}
int y = sample.islandBaseY + i;
int mergedIndex = y - minY;
if (!solidMask[mergedIndex]) {
solidMask[mergedIndex] = true;
solidCount++;
}
if (sampleTopY >= entryTopY[mergedIndex]) {
entryTopY[mergedIndex] = sampleTopY;
entryMask[mergedIndex] = sample.entryAt(i);
}
}
}
int topIdx = highestSolidIndex(solidMask);
if (solidCount == 0 || topIdx < 0) {
return reject(REJECT_NO_SOLID);
}
IrisFloatingChildBiomes topEntry = entryMask[topIdx] == null ? samples.getFirst().entry : entryMask[topIdx];
LAST_REJECT.get()[0] = REJECT_NONE;
return new FloatingIslandSample(topEntry, minY, thickness, topIdx, solidCount, solidMask, entryMask);
}
private static FloatingIslandSample sampleEntry(IrisBiome parent, IrisFloatingChildBiomes entry, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
CNG footprintCng = entry.getFootprintCng(baseSeed, data);
if (footprintCng == null) {
warnNullCng("footprintStyle", parent);
@@ -167,12 +281,16 @@ public final class FloatingIslandSample {
diag[0] = signed;
diag[1] = signedCut;
if (signed <= signedCut) {
NeighborSupport footprintSupport = footprintNeighborSupport(footprintCng, wx, wz, signedCut);
boolean footprintSolid = signed > signedCut;
boolean repairedFootprint = !footprintSolid && isFootprintPinholeRepairable(signed, signedCut, footprintSupport);
if (!footprintSolid && !repairedFootprint) {
return reject(REJECT_NO_SEED);
}
if (!hasFootprintNeighborSupport(footprintCng, wx, wz, signedCut)) {
if (footprintSolid && !footprintSupport.hasSolidSupport()) {
return reject(REJECT_NO_SEED);
}
double shapeSigned = repairedFootprint ? signedCut + FOOTPRINT_PINHOLE_REPAIR_MARGIN : signed;
CNG altitudeCng = entry.getAltitudeCng(baseSeed, data);
if (altitudeCng == null) {
@@ -186,7 +304,7 @@ public final class FloatingIslandSample {
int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface() - worldMin);
int baseY = minAlt + (int) Math.round(altClamped * (maxAlt - minAlt));
double edgeFade = edgeFade(signed, signedCut);
double edgeFade = edgeFade(shapeSigned, signedCut);
IrisBiome target = entry.getRealBiome(parent, data);
int topH = roundedEdgeHeight(computeTopHeight(entry, target, engine, baseSeed, wx, wz, data), edgeFade);
int topY = baseY + topH;
@@ -239,33 +357,30 @@ public final class FloatingIslandSample {
boolean[] solidMask = new boolean[thickness];
CNG wallWarp = entry.getWallWarpCng(baseSeed, data);
double warpAmp = Math.max(0, entry.getWallWarpAmplitude());
CNG carve = entry.getCarveCng(baseSeed, data);
IrisCaveProfileSampler carvingProfileSampler = entry.getCarvingProfileSampler(engine, data);
CNG carve = carvingProfileSampler == null && !entry.hasCarvingReference() ? entry.getCarveCng(baseSeed, data) : null;
double carveThreshold = entry.getCarveThreshold();
boolean useWarp = wallWarp != null && warpAmp > 0;
boolean useCarve = carve != null && carveThreshold < 1.0;
boolean useProfileCarve = carvingProfileSampler != null;
boolean useCarve = directCarveEnabled(entry, carvingProfileSampler, carve, carveThreshold);
for (int k = 0; k < thickness; k++) {
int wy = botY + k;
double sx = wx;
double sz = wz;
if (useWarp) {
double wnX = wallWarp.noise(wx, wy, wz);
double signedWarpX = (Math.max(0, Math.min(1, wnX)) * 2.0) - 1.0;
sx = wx + signedWarpX * warpAmp;
double wnZ = wallWarp.noise(wx + 1987.3, wy, wz + 2341.1);
double signedWarpZ = (Math.max(0, Math.min(1, wnZ)) * 2.0) - 1.0;
sz = wz + signedWarpZ * warpAmp;
boolean layerSolid = layerFootprintSolid(footprintCng, wallWarp, useWarp, warpAmp, wx, wy, wz, signedCut);
NeighborSupport layerSupport = layerNeighborSupport(footprintCng, wallWarp, useWarp, warpAmp, wx, wy, wz, signedCut);
if (layerSolid && !layerSupport.hasSolidSupport()) {
continue;
}
double layerFoot = footprintCng.noise(sx, sz);
double layerSigned = (Math.max(0, Math.min(1, layerFoot)) * 2.0) - 1.0;
if (layerSigned <= signedCut) {
if (!layerSolid && !layerSupport.canFillPinhole()) {
continue;
}
solidMask[k] = true;
}
int solidCount = solidifyUncarvedInterior(solidMask);
if (useCarve) {
if (useProfileCarve) {
solidCount = carveSolidInterior(solidMask, botY, wx, wz, carvingProfileSampler, carveThreshold);
} else if (useCarve) {
solidCount = carveSolidInterior(solidMask, botY, wx, wz, carve, carveThreshold);
}
int highestSolidIdx = highestSolidIndex(solidMask);
@@ -299,7 +414,23 @@ public final class FloatingIslandSample {
return (int) Math.round(fullDepth * fade);
}
static boolean directCarveEnabled(IrisFloatingChildBiomes entry, IrisCaveProfileSampler carvingProfileSampler, CNG carve, double carveThreshold) {
return carvingProfileSampler == null && (entry == null || !entry.hasCarvingReference()) && carve != null && carveThreshold < 1.0;
}
static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, CNG carve, double carveThreshold) {
return carveSolidInterior(solidMask, botY, wx, wz, (x, y, z) -> {
double carveNoise = carve.noise(x, y, z);
double carveClamped = Math.max(0, Math.min(1, carveNoise));
return carveClamped > carveThreshold;
});
}
static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, IrisCaveProfileSampler carve, double carveThreshold) {
return carveSolidInterior(solidMask, botY, wx, wz, (x, y, z) -> carve.shouldCarve(x, y, z, carveThreshold));
}
private static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, CarveSampler carve) {
int firstSolid = -1;
int lastSolid = -1;
for (int i = 0; i < solidMask.length; i++) {
@@ -314,24 +445,108 @@ public final class FloatingIslandSample {
if (firstSolid < 0) {
return 0;
}
int count = 0;
for (int i = firstSolid; i <= lastSolid; i++) {
if (i != firstSolid && i != lastSolid) {
double carveNoise = carve.noise(wx, botY + i, wz);
double carveClamped = Math.max(0, Math.min(1, carveNoise));
if (carveClamped > carveThreshold) {
solidMask[i] = false;
int span = lastSolid - firstSolid + 1;
int shell = carveShellThickness(span);
int carveStart = firstSolid + shell;
int carveEnd = lastSolid - shell;
boolean[] carveMask = new boolean[solidMask.length];
if (carveStart <= carveEnd) {
for (int i = carveStart; i <= carveEnd; i++) {
if (!solidMask[i]) {
continue;
}
int y = botY + i;
if (carve.shouldCarve(wx, y, wz) && hasCarveClusterSupport(carve, wx, y, wz)) {
carveMask[i] = true;
}
if (solidMask[i]) {
}
clampVerticalCarveRuns(carveMask, carveStart, carveEnd, maxVerticalCarveRun(span));
for (int i = carveStart; i <= carveEnd; i++) {
if (carveMask[i]) {
solidMask[i] = false;
}
}
}
int count = 0;
for (boolean solid : solidMask) {
if (solid) {
count++;
}
}
return count;
}
static int carveShellThickness(int solidSpan) {
if (solidSpan <= 4) {
return 1;
}
int proportional = (int) Math.ceil(solidSpan * CARVE_SHELL_FRACTION);
return Math.max(2, Math.min(5, proportional));
}
static int maxVerticalCarveRun(int solidSpan) {
int proportional = (int) Math.round(solidSpan * CARVE_MAX_VERTICAL_RUN_FRACTION);
return Math.max(2, Math.min(6, proportional));
}
static void clampVerticalCarveRuns(boolean[] carveMask, int start, int end, int maxRun) {
int i = start;
while (i <= end) {
if (!carveMask[i]) {
i++;
continue;
}
int runStart = i;
while (i <= end && carveMask[i]) {
i++;
}
int runEnd = i - 1;
int runLength = runEnd - runStart + 1;
if (runLength <= maxRun) {
continue;
}
int keepStart = runStart + ((runLength - maxRun) / 2);
int keepEnd = keepStart + maxRun - 1;
for (int j = runStart; j <= runEnd; j++) {
carveMask[j] = j >= keepStart && j <= keepEnd;
}
}
}
private static boolean hasCarveClusterSupport(CarveSampler carve, int wx, int wy, int wz) {
int cardinal = 0;
int diagonal = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dz == 0) {
continue;
}
if (!carve.shouldCarve(wx + dx, wy, wz + dz)) {
continue;
}
if (Math.abs(dx) + Math.abs(dz) == 1) {
cardinal++;
} else {
diagonal++;
}
}
}
return cardinal >= CARVE_CARDINAL_SUPPORT && cardinal + diagonal >= CARVE_TOTAL_SUPPORT;
}
static boolean hasFootprintNeighborSupport(CNG footprintCng, int wx, int wz, double signedCut) {
return footprintNeighborSupport(footprintCng, wx, wz, signedCut).hasSolidSupport();
}
static boolean isFootprintPinholeRepairable(double signed, double signedCut, NeighborSupport support) {
return signed >= signedCut - FOOTPRINT_PINHOLE_REPAIR_MARGIN && support.canFillPinhole();
}
static NeighborSupport footprintNeighborSupport(CNG footprintCng, int wx, int wz, double signedCut) {
int cardinal = 0;
int diagonal = 0;
for (int dx = -1; dx <= 1; dx++) {
@@ -351,7 +566,47 @@ public final class FloatingIslandSample {
}
}
}
return cardinal > 0 || diagonal >= 2;
return new NeighborSupport(cardinal, diagonal);
}
static NeighborSupport layerNeighborSupport(CNG footprintCng, CNG wallWarp, boolean useWarp, double warpAmp, int wx, int wy, int wz, double signedCut) {
int cardinal = 0;
int diagonal = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dz == 0) {
continue;
}
if (!layerFootprintSolid(footprintCng, wallWarp, useWarp, warpAmp, wx + dx, wy, wz + dz, signedCut)) {
continue;
}
if (Math.abs(dx) + Math.abs(dz) == 1) {
cardinal++;
} else {
diagonal++;
}
}
}
return new NeighborSupport(cardinal, diagonal);
}
static boolean layerFootprintSolid(CNG footprintCng, CNG wallWarp, boolean useWarp, double warpAmp, int wx, int wy, int wz, double signedCut) {
double sx = wx;
double sz = wz;
if (useWarp) {
double wnX = wallWarp.noise(wx, wy, wz);
double signedWarpX = signedFromUnit(wnX);
sx = wx + signedWarpX * warpAmp;
double wnZ = wallWarp.noise(wx + 1987.3, wy, wz + 2341.1);
double signedWarpZ = signedFromUnit(wnZ);
sz = wz + signedWarpZ * warpAmp;
}
double layerFoot = footprintCng.noise(sx, sz);
return signedFromUnit(layerFoot) > signedCut;
}
private static double signedFromUnit(double value) {
return (Math.max(0, Math.min(1, value)) * 2.0) - 1.0;
}
static int solidifyUncarvedInterior(boolean[] solidMask) {
@@ -384,6 +639,41 @@ public final class FloatingIslandSample {
return -1;
}
@FunctionalInterface
private interface CarveSampler {
boolean shouldCarve(int x, int y, int z);
}
static final class NeighborSupport {
private final int cardinal;
private final int diagonal;
private NeighborSupport(int cardinal, int diagonal) {
this.cardinal = cardinal;
this.diagonal = diagonal;
}
int cardinal() {
return cardinal;
}
int diagonal() {
return diagonal;
}
int total() {
return cardinal + diagonal;
}
boolean hasSolidSupport() {
return cardinal > 0 || diagonal >= 2;
}
boolean canFillPinhole() {
return cardinal >= PINHOLE_CARDINAL_FILL && total() >= PINHOLE_TOTAL_FILL;
}
}
private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) {
int maxTopHeight = Math.max(0, entry.getMaxTopHeight());
if (maxTopHeight == 0) {
@@ -167,8 +167,10 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@Desc("Objects define what schematics (iob files) iris will place in this biome")
private KList<IrisObjectPlacement> objects = new KList<>();
@ArrayType(min = 1, type = IrisFloatingChildBiomes.class)
@Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column.")
@Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column unless mergeFloatingChildBiomes is enabled.")
private KList<IrisFloatingChildBiomes> floatingChildBiomes = new KList<>();
@Desc("When true, every floating child entry is sampled independently and their solid masks are unioned, so multiple floating islands can stack, overlap, and collide instead of the picker choosing only one child per column.")
private boolean mergeFloatingChildBiomes = false;
@Required
@ArrayType(min = 1, type = IrisBiomeGeneratorLink.class)
@Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.")
@@ -0,0 +1,165 @@
/*
* 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.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.List;
public final class IrisCaveProfileSampler {
private static final double FLOATING_THRESHOLD_BIAS_SCALE = 0.2D;
private final IrisData data;
private final IrisCaveProfile profile;
private final CNG baseDensity;
private final CNG detailDensity;
private final CNG warpDensity;
private final RNG thresholdRng;
private final ModuleState[] modules;
private final double inverseNormalization;
private final double baseWeight;
private final double detailWeight;
private final double warpStrength;
private final boolean enabled;
private final boolean hasWarp;
private final boolean hasModules;
public IrisCaveProfileSampler(Engine engine, IrisCaveProfile profile) {
this.data = engine.getData();
this.profile = profile;
List<ModuleState> moduleStates = new ArrayList<>();
RNG baseRng = new RNG(engine.getSeedManager().getCarve());
this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data);
this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data);
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
this.thresholdRng = baseRng.nextParallelRNG(489_112);
this.baseWeight = profile.getBaseWeight();
this.detailWeight = profile.getDetailWeight();
this.warpStrength = profile.getWarpStrength();
this.hasWarp = warpStrength > 0D;
double weight = Math.abs(baseWeight) + Math.abs(detailWeight);
int index = 0;
for (IrisCaveFieldModule module : profile.getModules()) {
CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data);
ModuleState state = new ModuleState(module, moduleDensity);
moduleStates.add(state);
weight += Math.abs(state.weight);
index++;
}
this.modules = moduleStates.toArray(new ModuleState[0]);
double normalization = weight <= 0 ? 1D : weight;
this.inverseNormalization = 1D / normalization;
this.hasModules = modules.length > 0;
this.enabled = profile.isEnabled() && baseDensity != null && detailDensity != null && (!hasWarp || warpDensity != null);
}
public boolean shouldCarve(int x, int y, int z, double floatingCarveThreshold) {
if (!enabled) {
return false;
}
double threshold = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias();
threshold += floatingThresholdBias(floatingCarveThreshold);
return sampleDensity(x, y, z) <= threshold;
}
double sampleDensity(int x, int y, int z) {
if (hasWarp) {
return sampleDensityWarped(x, y, z);
}
return sampleDensityUnwarped(x, y, z);
}
private double sampleDensityUnwarped(int x, int y, int z) {
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
if (hasModules) {
for (ModuleState module : modules) {
if (!module.isActive(y)) {
continue;
}
density += module.sample(x, y, z);
}
}
return density * inverseNormalization;
}
private double sampleDensityWarped(int x, int y, int z) {
double warpA = warpDensity.noiseFastSigned3D(x, y, z);
double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D);
double warpedX = x + (warpA * warpStrength);
double warpedY = y + (warpB * warpStrength);
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
if (hasModules) {
for (ModuleState module : modules) {
if (!module.isActive(y)) {
continue;
}
density += module.sample(warpedX, warpedY, warpedZ);
}
}
return density * inverseNormalization;
}
private double floatingThresholdBias(double floatingCarveThreshold) {
double clamped = Math.max(0D, Math.min(1D, floatingCarveThreshold));
return (1D - clamped) * FLOATING_THRESHOLD_BIAS_SCALE;
}
private static final class ModuleState {
private final CNG density;
private final int minY;
private final int maxY;
private final double weight;
private final double threshold;
private final boolean invert;
private ModuleState(IrisCaveFieldModule module, CNG density) {
IrisRange range = module.getVerticalRange();
this.density = density;
this.minY = (int) Math.floor(range.getMin());
this.maxY = (int) Math.ceil(range.getMax());
this.weight = module.getWeight();
this.threshold = module.getThreshold();
this.invert = module.isInvert();
}
private boolean isActive(int y) {
return density != null && y >= minY && y <= maxY;
}
private double sample(double x, double y, double z) {
double sampled = density.noiseFastSigned3D(x, y, z);
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
@@ -51,6 +52,7 @@ public class IrisFloatingChildBiomes implements IRare {
private final transient AtomicCache<CNG> bottomCache = new AtomicCache<>();
private final transient AtomicCache<CNG> wallWarpCache = new AtomicCache<>();
private final transient AtomicCache<CNG> carveCache = new AtomicCache<>();
private final transient AtomicCache<IrisCaveProfileSampler> carvingProfileSamplerCache = new AtomicCache<>(true);
private final transient AtomicCache<IrisObjectScale> shrinkScaleCache = new AtomicCache<>();
public CNG getFootprintCng(long baseSeed, IrisData data) {
@@ -89,6 +91,45 @@ public class IrisFloatingChildBiomes implements IRare {
return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data));
}
public IrisCaveProfileSampler getCarvingProfileSampler(Engine engine, IrisData data) {
if (!hasCarvingReference()) {
return null;
}
return carvingProfileSamplerCache.aquire(() -> {
IrisBiome resolved = resolveCarvingBiome(carving, engine, data);
if (resolved == null || resolved.getCaveProfile() == null || !resolved.getCaveProfile().isEnabled()) {
return null;
}
return new IrisCaveProfileSampler(engine, resolved.getCaveProfile());
});
}
public boolean hasCarvingReference() {
return carving != null && !carving.isBlank();
}
static IrisBiome resolveCarvingBiome(String carving, Engine engine, IrisData data) {
if (carving == null || carving.isBlank() || engine == null || data == null) {
return null;
}
IrisDimension dimension = engine.getDimension();
if (dimension != null && dimension.getCarvingEntryIndex() != null) {
IrisDimensionCarvingEntry entry = dimension.getCarvingEntryIndex().get(carving);
if (entry != null) {
return IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry);
}
}
if (data.getBiomeLoader() == null) {
return null;
}
return data.getBiomeLoader().load(carving);
}
@RegistryListResource(IrisBiome.class)
@Desc("The target biome whose visual design (layers, palette, decorators, surface objects, derivative, and — when topShapeMode=BIOME — generator profile) drives the floating island. Leave empty to reuse the parent biome (self).")
private String biome = "";
@@ -185,9 +226,12 @@ public class IrisFloatingChildBiomes implements IRare {
@Desc("Optional 3D noise that swiss-cheeses the island interior by marking individual blocks as air when the noise exceeds carveThreshold. Leave null to keep the island solid. Good defaults: {\"style\":\"CELLULAR\",\"zoom\":0.3} for bubble pockets, {\"style\":\"VASCULAR\",\"zoom\":0.25} for wormy tunnels.")
private IrisGeneratorStyle carveStyle = null;
@Desc("Optional carving biome key or dimension carving entry id whose caveProfile drives floating-island internal air pockets. Dimension carving ids resolve first; biome keys such as carving/mushroom resolve second. When set, this overrides carveStyle while carveThreshold remains a final tuning bias.")
private String carving = "";
@MinNumber(0)
@MaxNumber(1)
@Desc("Threshold (0..1) above which carveStyle noise carves air pockets. 1.0 = no carving. 0.75 = sparse pockets. 0.55 = heavy swiss-cheese. 0.4 = shredded lattice. Ignored when carveStyle is null.")
@Desc("Threshold (0..1) above which carveStyle noise carves air pockets. 1.0 = no direct-noise carving. 0.75 = sparse pockets. 0.55 = heavy swiss-cheese. 0.4 = shredded lattice. When carving is set, lower values add a final positive threshold bias to the referenced caveProfile.")
private double carveThreshold = 1.0;
@Desc("Optional water surface height above the island base, in blocks. null = no internal water. Positive = water fills any dip in the top profile up to baseY + localFluidHeight (forms lakes/ponds in concavities of the biome-top heightmap).")
@@ -162,24 +162,38 @@ public class IrisObjectPlacement {
p.setMode(mode);
p.setEdit(edit);
p.setTranslate(translate);
p.setScale(scale);
p.setWarp(warp);
p.setBore(bore);
p.setMeld(meld);
p.setWaterloggable(waterloggable);
p.setOnwater(onwater);
p.setFromBottom(fromBottom);
p.setBottom(bottom);
p.setSmartBore(smartBore);
p.setCarvingSupport(carvingSupport);
p.setCaveAnchorMode(caveAnchorMode);
p.setUnderwater(underwater);
p.setHeightmap(heightmap);
p.setBoreExtendMaxY(boreExtendMaxY);
p.setBoreExtendMinY(boreExtendMinY);
p.setStiltSettings(stiltSettings);
p.setDensity(density);
p.setDensityStyle(densityStyle);
p.setChance(chance);
p.setSnow(snow);
p.setClamp(clamp);
p.setRotation(rotation);
p.setSlopeCondition(slopeCondition);
p.setRotateTowardsSlope(rotateTowardsSlope);
p.setDolphinTarget(isDolphinTarget);
p.setMarkers(markers);
p.setLoot(loot);
p.setVanillaLoot(vanillaLoot);
p.setOverrideGlobalLoot(overrideGlobalLoot);
p.setTrees(trees);
p.setAllowedCollisions(allowedCollisions);
p.setForbiddenCollisions(forbiddenCollisions);
p.setForcePlace(forcePlace);
return p;
}
@@ -526,7 +526,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
});
mantleChunk.iterate(BlockData.class, (x, y, z, blockData) -> {
if (blockData == null) {
if (!shouldApplyMantleOverlayBlock(blockData)) {
return;
}
int worldY = y + minWorldY;
@@ -542,6 +542,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return new OverlayMetrics(appliedBlocks.get(), objectKeys.get());
}
static boolean shouldApplyMantleOverlayBlock(BlockData blockData) {
if (blockData == null) {
return false;
}
Material material = blockData.getMaterial();
return material != null && material != Material.AIR && material != Material.CAVE_AIR && material != Material.VOID_AIR;
}
private record OverlayMetrics(int appliedBlocks, int objectKeys) {
}
@@ -8,7 +8,7 @@ import java.lang.reflect.Constructor;
import static org.junit.Assert.assertEquals;
public class MantleFloatingObjectComponentInvertedCountersTest {
public class MantleFloatingObjectComponentInvertedPlacementTest {
private FloatingObjectFootprint footprint(int lowestSolidKeyY, int highestSolidKeyY, int tallestKx, int tallestKz) throws Exception {
Constructor<FloatingObjectFootprint> constructor = FloatingObjectFootprint.class.getDeclaredConstructor(
@@ -38,40 +38,6 @@ public class MantleFloatingObjectComponentInvertedCountersTest {
);
}
@Test
public void resetObjectCounters_resetsAllInvertedCountersToZero() {
MantleFloatingObjectComponent.objectsInvertedAttempted.set(5);
MantleFloatingObjectComponent.objectsInvertedPlaced.set(3);
MantleFloatingObjectComponent.objectsInvertedSkippedNoFlat.set(2);
MantleFloatingObjectComponent.objectsInvertedFallbackNoInterior.set(1);
MantleFloatingObjectComponent.objectsInvertedSkippedShrink.set(4);
MantleFloatingObjectComponent.objectsInvertedSkippedNullObj.set(7);
MantleFloatingObjectComponent.writesDroppedAboveBottomTotal.set(11);
MantleFloatingObjectComponent.writesDroppedBottomOverhangTotal.set(9);
MantleFloatingObjectComponent.resetObjectCounters();
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedAttempted.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedPlaced.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedSkippedNoFlat.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedFallbackNoInterior.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedSkippedShrink.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedSkippedNullObj.get());
assertEquals(0, MantleFloatingObjectComponent.writesDroppedAboveBottomTotal.get());
assertEquals(0, MantleFloatingObjectComponent.writesDroppedBottomOverhangTotal.get());
}
@Test
public void resetObjectCounters_alsoResetsExistingCounters_noRegression() {
MantleFloatingObjectComponent.objectsAttempted.set(99);
MantleFloatingObjectComponent.objectsPlaced.set(88);
MantleFloatingObjectComponent.resetObjectCounters();
assertEquals(0, MantleFloatingObjectComponent.objectsAttempted.get());
assertEquals(0, MantleFloatingObjectComponent.objectsPlaced.get());
}
@Test
public void invertedBaseY_anchorsOriginalLowestSolidBelowBottomFace() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
@@ -6,6 +6,9 @@ import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class FloatingIslandSampleBottomYTest {
private FloatingIslandSample buildSample(int islandBaseY, boolean[] solidMask) {
@@ -55,6 +58,20 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(first, second);
}
@Test
public void mergedEntryMask_preservesTopAndBottomOwners() {
IrisFloatingChildBiomes bottom = new IrisFloatingChildBiomes();
IrisFloatingChildBiomes top = new IrisFloatingChildBiomes();
boolean[] mask = {true, true, false, true};
IrisFloatingChildBiomes[] entryMask = {bottom, bottom, null, top};
FloatingIslandSample sample = FloatingIslandSample.constructForTest(top, 80, mask.length, 3, 3, mask, entryMask);
assertSame(top, sample.entry);
assertSame(bottom, sample.bottomEntry());
assertSame(bottom, sample.entryAt(1));
assertSame(top, sample.entryAt(3));
}
@Test
public void solidifyUncarvedInterior_fillsGapsBetweenSolids() {
boolean[] mask = {false, true, false, false, true, false};
@@ -124,6 +141,173 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(false, mask[5]);
}
@Test
public void footprintPinholeFill_acceptsSingleColumnHoleWithSolidRing() {
CNG footprint = new CNG(new RNG(4)) {
@Override
public double noise(double x, double z) {
return x == 0 && z == 0 ? 0.46 : 1.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandSample.NeighborSupport support = FloatingIslandSample.footprintNeighborSupport(footprint, 0, 0, 0.0);
assertTrue(FloatingIslandSample.isFootprintPinholeRepairable(-0.08, 0.0, support));
}
@Test
public void footprintPinholeFill_keepsBroadHoleOpen() {
CNG footprint = new CNG(new RNG(5)) {
@Override
public double noise(double x, double z) {
return Math.abs(x) <= 1 && Math.abs(z) <= 1 ? 0.0 : 1.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandSample.NeighborSupport support = FloatingIslandSample.footprintNeighborSupport(footprint, 0, 0, 0.0);
assertFalse(FloatingIslandSample.isFootprintPinholeRepairable(-1.0, 0.0, support));
}
@Test
public void layerSupport_rejectsIsolatedSolidProtrusion() {
CNG footprint = new CNG(new RNG(6)) {
@Override
public double noise(double x, double z) {
return x == 0 && z == 0 ? 1.0 : 0.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertTrue(FloatingIslandSample.layerFootprintSolid(footprint, null, false, 0.0, 0, 64, 0, 0.0));
assertFalse(FloatingIslandSample.layerNeighborSupport(footprint, null, false, 0.0, 0, 64, 0, 0.0).hasSolidSupport());
}
@Test
public void layerSupport_fillsSingleColumnAirGap() {
CNG footprint = new CNG(new RNG(7)) {
@Override
public double noise(double x, double z) {
return x == 0 && z == 0 ? 0.0 : 1.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertFalse(FloatingIslandSample.layerFootprintSolid(footprint, null, false, 0.0, 0, 64, 0, 0.0));
assertTrue(FloatingIslandSample.layerNeighborSupport(footprint, null, false, 0.0, 0, 64, 0, 0.0).canFillPinhole());
}
@Test
public void carveSolidInterior_rejectsIsolatedCarveAir() {
boolean[] mask = {true, true, true, true, true};
CNG carve = new CNG(new RNG(8), new NoiseGenerator() {
@Override
public double noise(double x) {
return 0.0;
}
@Override
public double noise(double x, double z) {
return x == 0 && z == 0 ? 1.0 : 0.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
}, 1.0, 1);
int count = FloatingIslandSample.carveSolidInterior(mask, 100, 0, 0, carve, 0.5);
assertEquals(5, count);
assertEquals(true, mask[1]);
assertEquals(true, mask[2]);
assertEquals(true, mask[3]);
}
@Test
public void carveSolidInterior_capsBroadVerticalCarveSheet() {
boolean[] mask = new boolean[30];
for (int i = 0; i < mask.length; i++) {
mask[i] = true;
}
CNG carve = new CNG(new RNG(10), new NoiseGenerator() {
@Override
public double noise(double x) {
return 1.0;
}
@Override
public double noise(double x, double z) {
return 1.0;
}
@Override
public double noise(double x, double y, double z) {
return 1.0;
}
}, 1.0, 1);
int count = FloatingIslandSample.carveSolidInterior(mask, 100, 0, 0, carve, 0.5);
assertEquals(25, count);
for (int i = 0; i < FloatingIslandSample.carveShellThickness(mask.length); i++) {
assertTrue(mask[i]);
assertTrue(mask[mask.length - 1 - i]);
}
int longestAirRun = 0;
int currentAirRun = 0;
for (boolean solid : mask) {
if (solid) {
currentAirRun = 0;
continue;
}
currentAirRun++;
longestAirRun = Math.max(longestAirRun, currentAirRun);
}
assertEquals(FloatingIslandSample.maxVerticalCarveRun(mask.length), longestAirRun);
}
@Test
public void clampVerticalCarveRuns_keepsMiddleOfOversizedRunOnly() {
boolean[] carveMask = new boolean[12];
for (int i = 1; i <= 10; i++) {
carveMask[i] = true;
}
FloatingIslandSample.clampVerticalCarveRuns(carveMask, 1, 10, 4);
assertEquals(false, carveMask[2]);
assertEquals(true, carveMask[4]);
assertEquals(true, carveMask[7]);
assertEquals(false, carveMask[9]);
}
@Test
public void directCarveEnabled_respectsCarvingReferenceOverride() {
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setCarving("carving/mushroom");
CNG carve = new CNG(new RNG(9));
assertFalse(FloatingIslandSample.directCarveEnabled(entry, null, carve, 0.5));
}
@Test
public void hasFootprintNeighborSupport_rejectsSingleIsolatedColumn() {
CNG footprint = new CNG(new RNG(2)) {
@@ -0,0 +1,104 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.block.data.BlockData;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisFloatingChildBiomesCarvingResolutionTest {
@BeforeClass
public static void setupBukkit() {
if (Bukkit.getServer() != null) {
return;
}
Server server = mock(Server.class);
BlockData emptyBlockData = mock(BlockData.class);
doReturn(Logger.getLogger("IrisTest")).when(server).getLogger();
doReturn("IrisTestServer").when(server).getName();
doReturn("1.0").when(server).getVersion();
doReturn("1.0").when(server).getBukkitVersion();
doReturn(emptyBlockData).when(server).createBlockData(any(Material.class));
doReturn(emptyBlockData).when(server).createBlockData(anyString());
Bukkit.setServer(server);
}
@Test
public void resolveCarvingBiome_loadsBiomeKeyWhenEntryIdMissing() {
IrisBiome biome = mock(IrisBiome.class);
Fixture fixture = createFixture(Map.of(), Map.of("carving/mushroom", biome));
IrisBiome resolved = IrisFloatingChildBiomes.resolveCarvingBiome("carving/mushroom", fixture.engine, fixture.data);
assertSame(biome, resolved);
}
@Test
public void resolveCarvingBiome_prefersDimensionCarvingEntryId() {
IrisBiome entryBiome = mock(IrisBiome.class);
IrisBiome sameKeyBiome = mock(IrisBiome.class);
IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry();
entry.setId("global-deepdark-band");
entry.setBiome("carving/standard-deepdark");
Map<String, IrisDimensionCarvingEntry> entries = new HashMap<>();
entries.put("global-deepdark-band", entry);
Map<String, IrisBiome> biomes = new HashMap<>();
biomes.put("carving/standard-deepdark", entryBiome);
biomes.put("global-deepdark-band", sameKeyBiome);
Fixture fixture = createFixture(entries, biomes);
IrisBiome resolved = IrisFloatingChildBiomes.resolveCarvingBiome("global-deepdark-band", fixture.engine, fixture.data);
assertSame(entryBiome, resolved);
}
private Fixture createFixture(Map<String, IrisDimensionCarvingEntry> entries, Map<String, IrisBiome> biomes) {
@SuppressWarnings("unchecked")
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
for (Map.Entry<String, IrisBiome> biome : biomes.entrySet()) {
doReturn(biome.getValue()).when(biomeLoader).load(biome.getKey());
}
IrisData data = mock(IrisData.class);
doReturn(biomeLoader).when(data).getBiomeLoader();
IrisDimension dimension = mock(IrisDimension.class);
doReturn(entries).when(dimension).getCarvingEntryIndex();
doReturn(new KList<>(entries.values())).when(dimension).getCarving();
Engine engine = mock(Engine.class);
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
return new Fixture(engine, data);
}
private static final class Fixture {
private final Engine engine;
private final IrisData data;
private Fixture(Engine engine, IrisData data) {
this.engine = engine;
this.data = data;
}
}
}
@@ -0,0 +1,121 @@
package art.arcane.iris.engine.object;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class IrisObjectPlacementToPlacementTest {
@Test
public void toPlacement_preservesInheritedPlacementMetadata() {
IrisObjectPlacement source = new IrisObjectPlacement();
IrisObjectRotation rotation = new IrisObjectRotation();
IrisObjectLimit clamp = new IrisObjectLimit();
IrisStyledRange densityStyle = new IrisStyledRange();
densityStyle.setMin(7D);
densityStyle.setMax(7D);
IrisStiltSettings stiltSettings = new IrisStiltSettings();
KList<IrisObjectMarker> markers = new KList<>();
IrisNoiseGenerator heightmap = new IrisNoiseGenerator(false);
IrisGeneratorStyle warp = NoiseStyle.SIMPLEX.style();
KList<IrisObjectReplace> edit = new KList<>();
IrisObjectTranslate translate = new IrisObjectTranslate();
IrisObjectScale scale = new IrisObjectScale();
KList<IrisObjectLoot> loot = new KList<>();
KList<IrisObjectVanillaLoot> vanillaLoot = new KList<>();
KList<IrisTree> trees = new KList<>();
KList<String> allowedCollisions = new KList<>();
KList<String> forbiddenCollisions = new KList<>();
IrisSlopeClip slopeCondition = new IrisSlopeClip();
markers.add(new IrisObjectMarker());
edit.add(new IrisObjectReplace());
loot.add(new IrisObjectLoot());
vanillaLoot.add(new IrisObjectVanillaLoot());
trees.add(new IrisTree());
allowedCollisions.add("objects/allowed");
forbiddenCollisions.add("objects/forbidden");
source.setRotation(rotation);
source.setClamp(clamp);
source.setSnow(0.4D);
source.setDolphinTarget(true);
source.setSlopeCondition(slopeCondition);
source.setRotateTowardsSlope(true);
source.setChance(0.25D);
source.setDensity(7);
source.setDensityStyle(densityStyle);
source.setStiltSettings(stiltSettings);
source.setBoreExtendMaxY(8);
source.setMarkers(markers);
source.setBoreExtendMinY(3);
source.setUnderwater(true);
source.setCarvingSupport(CarvingMode.ANYWHERE);
source.setCaveAnchorMode(IrisCaveAnchorMode.CEILING);
source.setHeightmap(heightmap);
source.setSmartBore(true);
source.setWaterloggable(true);
source.setOnwater(true);
source.setMeld(true);
source.setFromBottom(true);
source.setBottom(true);
source.setBore(true);
source.setWarp(warp);
source.setTranslateCenter(true);
source.setMode(ObjectPlaceMode.FAST_MIN_STILT);
source.setEdit(edit);
source.setTranslate(translate);
source.setScale(scale);
source.setLoot(loot);
source.setVanillaLoot(vanillaLoot);
source.setOverrideGlobalLoot(true);
source.setTrees(trees);
source.setAllowedCollisions(allowedCollisions);
source.setForbiddenCollisions(forbiddenCollisions);
source.setForcePlace(true);
IrisObjectPlacement copy = source.toPlacement("objects/replaced");
assertEquals(1, copy.getPlace().size());
assertEquals("objects/replaced", copy.getPlace().get(0));
assertSame(rotation, copy.getRotation());
assertSame(clamp, copy.getClamp());
assertEquals(0.4D, copy.getSnow(), 0.0D);
assertTrue(copy.isDolphinTarget());
assertSame(slopeCondition, copy.getSlopeCondition());
assertTrue(copy.isRotateTowardsSlope());
assertEquals(0.25D, copy.getChance(), 0.0D);
assertEquals(7, copy.getDensity());
assertSame(densityStyle, copy.getDensityStyle());
assertSame(stiltSettings, copy.getStiltSettings());
assertEquals(8, copy.getBoreExtendMaxY());
assertSame(markers, copy.getMarkers());
assertEquals(3, copy.getBoreExtendMinY());
assertTrue(copy.isUnderwater());
assertEquals(CarvingMode.ANYWHERE, copy.getCarvingSupport());
assertEquals(IrisCaveAnchorMode.CEILING, copy.getCaveAnchorMode());
assertSame(heightmap, copy.getHeightmap());
assertTrue(copy.isSmartBore());
assertTrue(copy.isWaterloggable());
assertTrue(copy.isOnwater());
assertTrue(copy.isMeld());
assertTrue(copy.isFromBottom());
assertTrue(copy.isBottom());
assertTrue(copy.isBore());
assertSame(warp, copy.getWarp());
assertTrue(copy.isTranslateCenter());
assertEquals(ObjectPlaceMode.FAST_MIN_STILT, copy.getMode());
assertSame(edit, copy.getEdit());
assertSame(translate, copy.getTranslate());
assertSame(scale, copy.getScale());
assertSame(loot, copy.getLoot());
assertSame(vanillaLoot, copy.getVanillaLoot());
assertTrue(copy.isOverrideGlobalLoot());
assertSame(trees, copy.getTrees());
assertSame(allowedCollisions, copy.getAllowedCollisions());
assertSame(forbiddenCollisions, copy.getForbiddenCollisions());
assertTrue(copy.isForcePlace());
}
}
@@ -17,7 +17,7 @@ public class IslandObjectPlacerAnchorFaceTest {
@Test
public void bottomFace_getHighest_inFootprint_returnsSampleBottomY() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0); // bottomY = 100
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
@@ -28,43 +28,37 @@ public class IslandObjectPlacerAnchorFaceTest {
@Test
public void bottomFace_getHighest_offFootprint_returnsChunkMinBottomY() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0); // bottomY = 100, only sample
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
// No sample at (15, 15) → falls back to chunkMinIslandBottomY = 100
int result = placer.getHighest(15, 15, null);
assertEquals(100, result);
}
@Test
public void bottomFace_set_aboveAnchor_dropsWrite_andIncrementsDroppedAboveBottom() {
public void bottomFace_set_aboveAnchor_dropsWrite() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0); // bottomY = 100
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
// y=101 >= anchorBottomY=100 → in-footprint but above/at anchor → dropped
assertEquals(false, placer.canWriteObjectBlock(0, 101, 0));
placer.set(0, 101, 0, null);
assertEquals(1, placer.getWritesAttempted());
assertEquals(1, placer.getWritesDroppedAboveBottom());
}
@Test
public void bottomFace_canWriteObjectBlock_allowsBelowAnchorWithoutCounting() {
public void bottomFace_canWriteObjectBlock_allowsBelowAnchor() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
assertEquals(true, placer.canWriteObjectBlock(0, 99, 0));
assertEquals(0, placer.getWritesAttempted());
assertEquals(0, placer.getWritesDroppedAboveBottom());
}
@Test
public void bottomFace_canWriteObjectBlock_blocksAnchorAndAboveWithoutCounting() {
public void bottomFace_canWriteObjectBlock_blocksAnchorAndAbove() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
@@ -72,23 +66,16 @@ public class IslandObjectPlacerAnchorFaceTest {
assertEquals(false, placer.canWriteObjectBlock(0, 100, 0));
assertEquals(false, placer.canWriteObjectBlock(0, 101, 0));
assertEquals(0, placer.getWritesAttempted());
assertEquals(0, placer.getWritesDroppedAboveBottom());
}
@Test
public void topFace_existingConstructor_dropsBelowAnchor_noRegression() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
// No sample at x=1, z=0 (idx=1)
// Existing single-face constructor defaults to TOP
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 105);
// Off-footprint column, y=104 <= anchorTopY=105 → dropped below
assertEquals(false, placer.canWriteObjectBlock(1, 104, 0));
placer.set(1, 104, 0, null);
assertEquals(1, placer.getWritesAttempted());
assertEquals(1, placer.getWritesDroppedBelow());
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.engine.platform;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class BukkitChunkGeneratorOverlayPolicyTest {
@Test
public void skipsAirOverlayBlocks() {
BlockData air = mock(BlockData.class);
doReturn(Material.AIR).when(air).getMaterial();
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air));
}
@Test
public void skipsCaveAirOverlayBlocks() {
BlockData air = mock(BlockData.class);
doReturn(Material.CAVE_AIR).when(air).getMaterial();
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air));
}
@Test
public void skipsVoidAirOverlayBlocks() {
BlockData air = mock(BlockData.class);
doReturn(Material.VOID_AIR).when(air).getMaterial();
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air));
}
@Test
public void appliesSolidOverlayBlocks() {
BlockData stone = mock(BlockData.class);
doReturn(Material.STONE).when(stone).getMaterial();
assertTrue(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(stone));
}
@Test
public void skipsNullOverlayBlocks() {
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(null));
}
}