Wip floaters

This commit is contained in:
Brian Neumann-Fopiano
2026-04-23 21:29:23 -04:00
parent ce29b70618
commit 1968ef2f2c
13 changed files with 639 additions and 342 deletions
@@ -126,7 +126,13 @@ public class IslandObjectPlacer implements IObjectPlacer {
}
private boolean shouldSkipAirColumn(int x, int y, int z) {
writesAttempted++;
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) {
@@ -136,27 +142,37 @@ public class IslandObjectPlacer implements IObjectPlacer {
return false;
}
if (y >= anchorY) {
writesDroppedAboveBottom++;
if (countWrite) {
writesDroppedAboveBottom++;
}
return true;
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
writesDroppedBelow++;
if (countWrite) {
writesDroppedBelow++;
}
return true;
}
if (!overhangAllowed[idx]) {
writesDroppedOverhang++;
if (countWrite) {
writesDroppedOverhang++;
}
return true;
}
} else {
if (y >= anchorY) {
writesDroppedBottomOverhang++;
if (countWrite) {
writesDroppedBottomOverhang++;
}
return true;
}
if (!overhangAllowed[idx]) {
writesDroppedBottomOverhang++;
if (countWrite) {
writesDroppedBottomOverhang++;
}
return true;
}
}
@@ -164,19 +180,29 @@ public class IslandObjectPlacer implements IObjectPlacer {
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
writesDroppedBelow++;
if (countWrite) {
writesDroppedBelow++;
}
return true;
}
} else {
if (y >= anchorY) {
writesDroppedBottomOverhang++;
if (countWrite) {
writesDroppedBottomOverhang++;
}
return true;
}
}
writesDroppedOverhang++;
if (countWrite) {
writesDroppedOverhang++;
}
return true;
}
public boolean canWriteObjectBlock(int x, int y, int z) {
return !shouldSkipAirColumn(x, y, z, false);
}
private @Nullable FloatingIslandSample sampleAt(int x, int z) {
int xf = x - minX;
int zf = z - minZ;
@@ -29,6 +29,7 @@ import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.FloatingObjectFootprint;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObject;
@@ -36,12 +37,23 @@ import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector;
import java.io.File;
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)
@@ -53,7 +65,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
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 terrainMismatchWarnings = new AtomicLong();
public static final AtomicLong writesAttemptedTotal = new AtomicLong();
public static final AtomicLong writesDroppedBelowTotal = new AtomicLong();
public static final AtomicLong writesDroppedOverhangTotal = new AtomicLong();
@@ -65,13 +76,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
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 TERRAIN_MISMATCH_WARNING_CAP = 200;
private static final AtomicLong heavyClipWarnings = new AtomicLong();
private static final int HEAVY_CLIP_WARNING_CAP = 30;
private static final double HEAVY_CLIP_RATIO = 0.5;
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 java.util.concurrent.ConcurrentHashMap<String, AtomicLong> anchorYHisto = new java.util.concurrent.ConcurrentHashMap<>();
public static final ConcurrentHashMap<String, AtomicLong> anchorYHisto = new ConcurrentHashMap<>();
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
@@ -85,11 +93,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
objectsRelaxed.set(0);
objectsSkippedShrink.set(0);
objectsSkippedNullObj.set(0);
terrainMismatchWarnings.set(0);
writesAttemptedTotal.set(0);
writesDroppedBelowTotal.set(0);
writesDroppedOverhangTotal.set(0);
heavyClipWarnings.set(0);
anchorYHisto.clear();
objectsInvertedAttempted.set(0);
objectsInvertedPlaced.set(0);
@@ -101,26 +107,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writesDroppedBottomOverhangTotal.set(0);
}
private static void recordWriteStats(IrisObject obj, int wx, int wz, int pickTopY, IslandObjectPlacer islandPlacer) {
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);
int dropped = below + overhang;
if (attempted >= 32 && dropped >= attempted * HEAVY_CLIP_RATIO) {
long warned = heavyClipWarnings.get();
if (warned < HEAVY_CLIP_WARNING_CAP && heavyClipWarnings.incrementAndGet() <= HEAVY_CLIP_WARNING_CAP) {
String objKey = obj == null ? "<null>" : obj.getLoadKey();
Iris.warn("[FloatingWriteClip] object=" + objKey
+ " at=(" + wx + "," + (pickTopY + 1) + "," + wz + ")"
+ " attempted=" + attempted
+ " droppedBelow=" + below
+ " droppedOverhang=" + overhang
+ " written=" + (attempted - dropped));
}
}
}
private static void recordInvertedWriteStats(IslandObjectPlacer islandPlacer) {
@@ -128,34 +121,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writesDroppedBottomOverhangTotal.addAndGet(islandPlacer.getWritesDroppedBottomOverhang());
}
private static void verifyTerrainBelowObject(IrisObject obj, int wx, int wz, int pickTopY, FloatingIslandSample sample) {
if (terrainMismatchWarnings.get() >= TERRAIN_MISMATCH_WARNING_CAP) {
return;
}
if (sample != null
&& sample.solidMask != null
&& sample.topIdx >= 0
&& sample.topIdx < sample.solidMask.length
&& sample.solidMask[sample.topIdx]) {
return;
}
if (terrainMismatchWarnings.incrementAndGet() > TERRAIN_MISMATCH_WARNING_CAP) {
return;
}
String objKey = obj == null ? "<null>" : obj.getLoadKey();
String sampleTop = sample == null ? "null" : String.valueOf(sample.topY());
String sampleBase = sample == null ? "null" : String.valueOf(sample.islandBaseY);
String sampleTopIdx = sample == null ? "null" : String.valueOf(sample.topIdx);
String sampleMaskLen = sample == null || sample.solidMask == null ? "null" : String.valueOf(sample.solidMask.length);
Iris.warn("[FloatingTerrainCheck] object=" + objKey
+ " at=(" + wx + "," + (pickTopY + 1) + "," + wz + ")"
+ " sample reports non-solid trunk column"
+ " sampleTopY=" + sampleTop
+ " sampleBaseY=" + sampleBase
+ " sampleTopIdx=" + sampleTopIdx
+ " sampleMaskLen=" + sampleMaskLen);
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
@@ -184,7 +149,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
}
java.util.IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> entryColumns = new java.util.IdentityHashMap<>();
IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> entryColumns = new IdentityHashMap<>();
for (int i = 0; i < 256; i++) {
FloatingIslandSample s = samples[i];
if (s == null || s.entry == null) {
@@ -193,7 +158,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
entryColumns.computeIfAbsent(s.entry, e -> new KList<>()).add(i);
}
for (java.util.Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : entryColumns.entrySet()) {
for (Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : entryColumns.entrySet()) {
IrisFloatingChildBiomes entry = ec.getKey();
KList<Integer> columns = ec.getValue();
if (columns.isEmpty()) {
@@ -280,7 +245,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
try {
obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
if (marker != null && shouldWritePlacementMarker(writer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
@@ -367,16 +332,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
try {
obj.place(wx, yv, wz, islandPlacer, anchored, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
if (marker != null
&& islandPlacer.canWriteObjectBlock(b.getX(), b.getY(), b.getZ())
&& shouldWritePlacementMarker(islandPlacer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsPlaced.incrementAndGet();
recordAnchorYHisto(pickTopY);
int trunkWx = minX + pickedXf;
int trunkWz = minZ + pickedZf;
verifyTerrainBelowObject(obj, trunkWx, trunkWz, pickTopY, pickedSample);
recordWriteStats(obj, trunkWx, trunkWz, pickTopY, islandPlacer);
recordWriteStats(islandPlacer);
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -417,44 +381,56 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
final IrisObject obj = obj0;
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
int invertedYRotation = rng.i(0, 3) * 90;
IrisObjectRotation invertedRotation = IrisObjectRotation.xFlip180WithY(invertedYRotation);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
if (interior.isEmpty()) {
objectsInvertedFallbackNoInterior.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) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
int pickBottomY = pickedSample.bottomY();
if (pickBottomY < 0) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
if (!isFootprintFlatBottom(fp, pickedXf, pickedZf, pickBottomY, samples, 2)) {
if (!isFootprintFlatBottom(fp, pickedXf, pickedZf, pickBottomY, samples, 4)) {
objectsInvertedSkippedNoFlat.incrementAndGet();
int pickedXf = -1;
int pickedZf = -1;
int pickBottomY = -1;
boolean foundBottomAnchor = false;
for (int attempt = 0; attempt < INVERTED_PICK_ATTEMPTS; attempt++) {
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int candidateXf = pickedKey & 15;
int candidateZf = pickedKey >> 4;
FloatingIslandSample candidateSample = samples[(candidateZf << 4) | candidateXf];
if (candidateSample == null) {
continue;
}
int candidateBottomY = candidateSample.bottomY();
if (candidateBottomY < 0) {
continue;
}
if (!isFootprintFlatBottom(fp, invertedRotation, candidateXf, candidateZf, candidateBottomY, samples, 2)
&& !isFootprintFlatBottom(fp, invertedRotation, candidateXf, candidateZf, candidateBottomY, samples, 4)) {
continue;
}
pickedXf = candidateXf;
pickedZf = candidateZf;
pickBottomY = candidateBottomY;
foundBottomAnchor = true;
break;
}
if (!foundBottomAnchor) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
int wx = minX + pickedXf - fp.getTallestKxBottom();
int wz = minZ + pickedZf - fp.getTallestKzBottom();
int wx = invertedBaseX(minX, pickedXf, fp, invertedRotation);
int wz = invertedBaseZ(minZ, pickedZf, fp, invertedRotation);
IrisObjectPlacement inverted = placement.toPlacement(obj.getLoadKey());
inverted.setMode(translateStiltModeForFloating(inverted.getMode()));
inverted.setTranslate(new IrisObjectTranslate());
inverted.setRotation(IrisObjectRotation.xFlip180());
inverted.setRotation(invertedRotation);
inverted.setForcePlace(true);
inverted.setBottom(false);
int yv = pickBottomY - 1 + fp.getHighestSolidKeyY();
int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickBottomY, IslandObjectPlacer.AnchorFace.BOTTOM);
int id = rng.i(0, Integer.MAX_VALUE);
@@ -462,7 +438,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
try {
obj.place(wx, yv, wz, islandPlacer, inverted, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
if (marker != null
&& islandPlacer.canWriteObjectBlock(b.getX(), b.getY(), b.getZ())
&& shouldWritePlacementMarker(islandPlacer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
@@ -474,9 +452,8 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
}
private static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickBottomY, FloatingIslandSample[] samples, int tolerance) {
int tallestKxBottom = fp.getTallestKxBottom();
int tallestKzBottom = fp.getTallestKzBottom();
private static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, IrisObjectRotation rotation, int pickedXf, int pickedZf, int pickBottomY, FloatingIslandSample[] samples, int tolerance) {
BlockVector anchor = invertedFootprintAnchor(fp, rotation);
int checked = 0;
boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ();
@@ -484,8 +461,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
long encoded = cells[i];
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
int colXf = pickedXf + (kx - tallestKxBottom);
int colZf = pickedZf + (kz - tallestKzBottom);
BlockVector cell = rotation.rotate(new BlockVector(kx, 0, kz), 0, 0, 0);
int colXf = pickedXf + cell.getBlockX() - anchor.getBlockX();
int colZf = pickedZf + cell.getBlockZ() - anchor.getBlockZ();
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
@@ -506,6 +484,48 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return touchedChunkEdge;
}
static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp) {
return invertedBaseX(minX, pickedXf, fp, IrisObjectRotation.xFlip180());
}
static int invertedBaseY(int pickBottomY, FloatingObjectFootprint fp) {
return invertedBaseY(pickBottomY, fp, IrisObjectRotation.xFlip180());
}
static int invertedBaseZ(int minZ, int pickedZf, FloatingObjectFootprint fp) {
return invertedBaseZ(minZ, pickedZf, fp, IrisObjectRotation.xFlip180());
}
static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return minX + pickedXf - invertedFootprintAnchor(fp, rotation).getBlockX();
}
static int invertedBaseY(int pickBottomY, FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return pickBottomY - 1 - invertedSolidAnchor(fp, rotation).getBlockY();
}
static int invertedBaseZ(int minZ, int pickedZf, FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return minZ + pickedZf - invertedFootprintAnchor(fp, rotation).getBlockZ();
}
private static BlockVector invertedFootprintAnchor(FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return rotation.rotate(new BlockVector(fp.getTallestKx(), 0, fp.getTallestKz()), 0, 0, 0);
}
private static BlockVector invertedSolidAnchor(FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return rotation.rotate(new BlockVector(fp.getTallestKx(), fp.getLowestSolidKeyY(), fp.getTallestKz()), 0, 0, 0);
}
private static boolean shouldWritePlacementMarker(IObjectPlacer placer, BlockData data, int x, int y, int z) {
if (data == null) {
return false;
}
BlockData existing = placer.get(x, y, z);
boolean wouldReplace = existing != null && B.isSolid(existing) && B.isVineBlock(data);
boolean placesBlock = !data.getMaterial().equals(Material.AIR) && !data.getMaterial().equals(Material.CAVE_AIR) && !wouldReplace;
return data instanceof IrisCustomData || placesBlock;
}
private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) {
int tallestKx = fp.getTallestKx();
int tallestKz = fp.getTallestKz();
@@ -556,10 +576,18 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) {
continue;
}
if (samples[(zf << 4) | (xf + 1)] == null) continue;
if (samples[(zf << 4) | (xf - 1)] == null) continue;
if (samples[((zf + 1) << 4) | xf] == null) continue;
if (samples[((zf - 1) << 4) | xf] == null) continue;
if (samples[(zf << 4) | (xf + 1)] == null) {
continue;
}
if (samples[(zf << 4) | (xf - 1)] == null) {
continue;
}
if (samples[((zf + 1) << 4) | xf] == null) {
continue;
}
if (samples[((zf - 1) << 4) | xf] == null) {
continue;
}
interior.add(key);
}
return interior;
@@ -592,7 +620,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override
protected int computeRadius() {
int maxObjectExtent = 0;
java.util.Set<String> objectKeys = new java.util.HashSet<>();
Set<String> objectKeys = new HashSet<>();
try {
IrisData data = getData();
for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) {
@@ -617,11 +645,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
for (String key : objectKeys) {
try {
java.io.File f = data.getObjectLoader().findFile(key);
if (f == null) continue;
org.bukkit.util.BlockVector sz = IrisObject.sampleSize(f);
File f = data.getObjectLoader().findFile(key);
if (f == null) {
continue;
}
BlockVector sz = IrisObject.sampleSize(f);
int extent = Math.max(sz.getBlockX(), sz.getBlockZ());
if (extent > maxObjectExtent) maxObjectExtent = extent;
if (extent > maxObjectExtent) {
maxObjectExtent = extent;
}
} catch (Throwable ignored) {
}
}
@@ -630,10 +662,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return Math.max(16, maxObjectExtent);
}
private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, java.util.Set<String> out) {
if (placements == null) return;
private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, Set<String> out) {
if (placements == null) {
return;
}
for (IrisObjectPlacement p : placements) {
if (p == null || p.getPlace() == null) continue;
if (p == null || p.getPlace() == null) {
continue;
}
out.addAll(p.getPlace());
}
}
@@ -18,25 +18,29 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.decorator.FloatingDecorator;
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator;
import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
import art.arcane.iris.engine.object.FloatingBottomPaletteMode;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisSlopeClip;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
@@ -45,110 +49,125 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.util.concurrent.atomic.AtomicLong;
import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<BlockData> {
public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL;
private static final AtomicLong columnsChecked = new AtomicLong();
private static final AtomicLong samplesAccepted = new AtomicLong();
private static final AtomicLong decorateInvocations = new AtomicLong();
private static final AtomicLong decorateSkippedNotAir = new AtomicLong();
private static final AtomicLong decorateSkippedNoInherit = new AtomicLong();
private static final AtomicLong decoratePhaseColumns = new AtomicLong();
private static final AtomicLong decoratePlaced = new AtomicLong();
private static final AtomicLong decorateNoChange = 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 AtomicLong decCandidatesNull = new AtomicLong();
private static final AtomicLong lastReportMs = new AtomicLong(0L);
private static final AtomicLong reportCycle = new AtomicLong(0L);
private static final Runnable INC_DEC_CANDIDATES_NULL = () -> decCandidatesNull.incrementAndGet();
private static final Runnable NOOP_DECORATION_MISS = () -> {
};
private final RNG rng;
private final EngineDecorator seaSurfaceDecorator;
public static void reportFloatingStats() {
StringBuilder topFloors = new StringBuilder();
floorMatHisto.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get()))
.limit(5)
.forEach(e -> topFloors.append(' ').append(e.getKey()).append('=').append(e.getValue().get()));
StringBuilder topAnchorY = new StringBuilder();
MantleFloatingObjectComponent.anchorYHisto.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get()))
.limit(5)
.forEach(e -> topAnchorY.append(' ').append(e.getKey()).append('=').append(e.getValue().get()));
art.arcane.iris.Iris.info("[floating-debug]"
+ " columns=" + columnsChecked.get()
+ " samples=" + samplesAccepted.get()
+ " decInvoke=" + decorateInvocations.get()
+ " decPlaced=" + decoratePlaced.get()
+ " decNoChange=" + decorateNoChange.get()
+ " decFloorNull=" + decorateFloorNull.get()
+ " decCandidatesNull=" + decCandidatesNull.get()
+ " decSkipNonAir=" + decorateSkippedNotAir.get()
+ " decSkipNoInherit=" + decorateSkippedNoInherit.get()
+ " decPhaseCols=" + decoratePhaseColumns.get()
+ " objAttempt=" + MantleFloatingObjectComponent.objectsAttempted.get()
+ " objPlaced=" + MantleFloatingObjectComponent.objectsPlaced.get()
+ " objNoFlat=" + MantleFloatingObjectComponent.objectsSkippedNoFlat.get()
+ " objNoInterior=" + MantleFloatingObjectComponent.objectsSkippedNoInterior.get()
+ " objRelax=" + MantleFloatingObjectComponent.objectsRelaxed.get()
+ " objShrinkDrop=" + MantleFloatingObjectComponent.objectsSkippedShrink.get()
+ " objNullObj=" + MantleFloatingObjectComponent.objectsSkippedNullObj.get()
+ " writeAttempt=" + MantleFloatingObjectComponent.writesAttemptedTotal.get()
+ " writeDropBelow=" + MantleFloatingObjectComponent.writesDroppedBelowTotal.get()
+ " writeDropOverhang=" + MantleFloatingObjectComponent.writesDroppedOverhangTotal.get()
+ " terrainMismatch=" + MantleFloatingObjectComponent.terrainMismatchWarnings.get()
+ " objInvAttempt=" + MantleFloatingObjectComponent.objectsInvertedAttempted.get()
+ " objInvPlaced=" + MantleFloatingObjectComponent.objectsInvertedPlaced.get()
+ " objInvNoFlat=" + MantleFloatingObjectComponent.objectsInvertedSkippedNoFlat.get()
+ " objInvFallbackNoInterior=" + MantleFloatingObjectComponent.objectsInvertedFallbackNoInterior.get()
+ " writesAboveBottom=" + MantleFloatingObjectComponent.writesDroppedAboveBottomTotal.get()
+ " writesBottomOverhang=" + MantleFloatingObjectComponent.writesDroppedBottomOverhangTotal.get()
+ " anchorY:" + (topAnchorY.length() == 0 ? " <none>" : topAnchorY.toString())
+ " topFloors:" + (topFloors.length() == 0 ? " <none>" : topFloors.toString()));
private static KList<BlockData> generateBottomPaletteLayers(IrisFloatingChildBiomes entry, IrisDimension dimension, double wx, double wz, RNG random, int paletteDepth, IrisData data, IrisComplex complex) {
if (entry == null || entry.getBottomPaletteMode() != FloatingBottomPaletteMode.CUSTOM || entry.getBottomPalette() == null || entry.getBottomPalette().isEmpty()) {
return null;
}
return generatePaletteLayers(dimension, entry.getBottomPalette(), wx, wz, random.nextParallelRNG(0xB0770B), paletteDepth, data, complex);
}
private static void maybeReport() {
long now = System.currentTimeMillis();
long last = lastReportMs.get();
if (now - last >= 10000L && lastReportMs.compareAndSet(last, now)) {
reportFloatingStats();
if (reportCycle.incrementAndGet() >= 30) {
reportCycle.set(0);
resetAllCounters();
private static KList<BlockData> generatePaletteLayers(IrisDimension dimension, KList<IrisBiomePaletteLayer> layers, double wx, double wz, RNG random, int maxDepth, IrisData data, IrisComplex complex) {
KList<BlockData> generated = new KList<>();
if (layers == null || layers.isEmpty() || maxDepth <= 0) {
return generated;
}
int generatorSeed = 7235;
for (int i = 0; i < layers.size(); i++) {
IrisBiomePaletteLayer layer = layers.get(i);
CNG heightGenerator = layer.getHeightGenerator(random.nextParallelRNG((generatorSeed++) * generatorSeed * generatorSeed * generatorSeed), data);
if (heightGenerator == null) {
continue;
}
double layerDepth = heightGenerator.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / layer.getZoom(), wz / layer.getZoom());
IrisSlopeClip slopeClip = layer.getSlopeCondition();
if (slopeClip != null && !slopeClip.isDefault() && complex != null && !slopeClip.isValid(complex.getSlopeStream().get(wx, wz))) {
layerDepth = 0;
}
if (layerDepth <= 0) {
continue;
}
for (int j = 0; j < layerDepth; j++) {
if (generated.size() >= maxDepth) {
break;
}
try {
generated.add(layer.get(random.nextParallelRNG(i + j), (wx + j) / layer.getZoom(), j, (wz - j) / layer.getZoom(), data));
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
}
if (generated.size() >= maxDepth) {
break;
}
if (dimension != null && dimension.isExplodeBiomePalettes()) {
BlockData barrier = B.get("minecraft:barrier");
for (int j = 0; j < dimension.getExplodeBiomePaletteSize(); j++) {
generated.add(barrier);
if (generated.size() >= maxDepth) {
break;
}
}
}
}
return generated;
}
private static void resetAllCounters() {
columnsChecked.set(0);
samplesAccepted.set(0);
decorateInvocations.set(0);
decorateSkippedNotAir.set(0);
decorateSkippedNoInherit.set(0);
decoratePhaseColumns.set(0);
decoratePlaced.set(0);
decorateNoChange.set(0);
decorateFloorNull.set(0);
floorMatHisto.clear();
decCandidatesNull.set(0);
MantleFloatingObjectComponent.resetObjectCounters();
private static boolean usesBottomPalette(IrisFloatingChildBiomes entry) {
FloatingBottomPaletteMode mode = entry == null || entry.getBottomPaletteMode() == null ? FloatingBottomPaletteMode.DEPTH : entry.getBottomPaletteMode();
return mode != FloatingBottomPaletteMode.DEPTH;
}
private static void recordFloorMat(String matKey) {
if (floorMatHisto.size() < 32) {
floorMatHisto.computeIfAbsent(matKey, k -> new AtomicLong()).incrementAndGet();
} else {
AtomicLong existing = floorMatHisto.get(matKey);
if (existing != null) {
existing.incrementAndGet();
} else {
floorMatHisto.computeIfAbsent("other", k -> new AtomicLong()).incrementAndGet();
}
private static int[] bottomDepths(FloatingIslandSample sample, int chunkHeight) {
int[] bottomDepths = new int[sample.solidMask.length];
for (int i = 0; i < bottomDepths.length; i++) {
bottomDepths[i] = -1;
}
int depth = 0;
int max = Math.min(sample.topIdx, sample.solidMask.length - 1);
for (int k = 0; k <= max; k++) {
if (!sample.solidMask[k]) {
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
continue;
}
bottomDepths[k] = depth++;
}
return bottomDepths;
}
private static BlockData selectPaletteBlock(IrisFloatingChildBiomes entry, KList<BlockData> topBlocks, KList<BlockData> bottomBlocks, int topDepth, int bottomDepth, BlockData fallbackSolid) {
FloatingBottomPaletteMode mode = entry == null || entry.getBottomPaletteMode() == null ? FloatingBottomPaletteMode.DEPTH : entry.getBottomPaletteMode();
if (mode == FloatingBottomPaletteMode.MIRROR_TOP) {
return paletteBlock(topBlocks, Math.min(topDepth, bottomDepth), fallbackSolid);
}
if (mode == FloatingBottomPaletteMode.CUSTOM && bottomDepth < topDepth) {
if (bottomBlocks != null && !bottomBlocks.isEmpty()) {
return paletteBlock(bottomBlocks, bottomDepth, fallbackSolid);
}
return paletteBlock(topBlocks, bottomDepth, fallbackSolid);
}
return paletteBlock(topBlocks, topDepth, fallbackSolid);
}
private static BlockData paletteBlock(KList<BlockData> blocks, int depth, BlockData fallbackSolid) {
if (blocks == null || blocks.isEmpty()) {
return fallbackSolid;
}
BlockData block = blocks.hasIndex(depth) ? blocks.get(depth) : blocks.getLast();
return block == null ? fallbackSolid : block;
}
public IrisFloatingChildBiomeModifier(Engine engine) {
@@ -174,13 +193,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
columnsChecked.incrementAndGet();
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine());
if (sample == null) {
continue;
}
samplesAccepted.incrementAndGet();
IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data);
@@ -191,8 +207,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
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;
for (int k = sample.topIdx; k >= 0; k--) {
if (!sample.solidMask[k]) {
@@ -202,13 +220,8 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (y < 0 || y >= chunkHeight) {
continue;
}
BlockData block = null;
if (blocks != null && !blocks.isEmpty()) {
block = blocks.hasIndex(depth) ? blocks.get(depth) : blocks.getLast();
}
if (block == null) {
block = fallbackSolid;
}
int bottomDepth = bottomDepths == null || bottomDepths[k] < 0 ? depth : bottomDepths[k];
BlockData block = selectPaletteBlock(entry, blocks, bottomBlocks, depth, bottomDepth, fallbackSolid);
if (block != null) {
output.set(xf, y, zf, block);
}
@@ -270,12 +283,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (sample == null) {
continue;
}
decoratePhaseColumns.incrementAndGet();
IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data);
if (!entry.isInheritDecorators() || target == null) {
decorateSkippedNoInherit.incrementAndGet();
continue;
}
@@ -284,26 +295,12 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (topY + 1 < chunkHeight) {
BlockData above = output.get(xf, topY + 1, zf);
if (above == null || B.isAir(above)) {
decorateInvocations.incrementAndGet();
BlockData floor = topY >= 0 && topY < chunkHeight ? output.get(xf, topY, zf) : null;
if (floor == null) {
decorateFloorNull.incrementAndGet();
} else {
recordFloorMat(floor.getMaterial().getKey().getKey());
}
try {
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, INC_DEC_CANDIDATES_NULL);
if (placed > 0) {
decoratePlaced.addAndGet(placed);
} else {
decorateNoChange.incrementAndGet();
}
FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng, NOOP_DECORATION_MISS);
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
} else {
decorateSkippedNotAir.incrementAndGet();
}
}
@@ -340,7 +337,6 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
}
}
}
maybeReport();
}
private void writeIslandSkyBiome(IrisBiome target, int wx, int wz, FloatingIslandSample sample, int chunkHeight) {
@@ -0,0 +1,33 @@
/*
* 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.engine.object.annotations.Desc;
@Desc("Controls how floating-child island underside blocks choose their palette.")
public enum FloatingBottomPaletteMode {
@Desc("Use the normal top-down biome layer depth for the whole island column.")
DEPTH,
@Desc("Use the target biome's top palette from both the island top and underside, meeting in the middle.")
MIRROR_TOP,
@Desc("Use bottomPalette near the underside and the target biome's normal palette near the top.")
CUSTOM
}
@@ -38,6 +38,7 @@ public final class FloatingIslandSample {
public static final int REJECT_COUNT = 7;
public static final int REJECT_CLUSTER = REJECT_NO_SEED;
private static final double EDGE_ROUNDING_BAND = 0.28;
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);
@@ -169,6 +170,9 @@ public final class FloatingIslandSample {
if (signed <= signedCut) {
return reject(REJECT_NO_SEED);
}
if (!hasFootprintNeighborSupport(footprintCng, wx, wz, signedCut)) {
return reject(REJECT_NO_SEED);
}
CNG altitudeCng = entry.getAltitudeCng(baseSeed, data);
if (altitudeCng == null) {
@@ -182,14 +186,11 @@ 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);
IrisBiome target = entry.getRealBiome(parent, data);
int topH = computeTopHeight(entry, target, engine, baseSeed, wx, wz, data);
int topH = roundedEdgeHeight(computeTopHeight(entry, target, engine, baseSeed, wx, wz, data), edgeFade);
int topY = baseY + topH;
double edge = (signed - signedCut) / 0.15;
double edgeClamped = Math.max(0, Math.min(1, edge));
double edgeFade = edgeClamped * edgeClamped * (3.0 - 2.0 * edgeClamped);
CNG bottomCng = entry.getBottomCng(baseSeed, data);
if (bottomCng == null) {
warnNullCng("bottomStyle", parent);
@@ -200,7 +201,7 @@ public final class FloatingIslandSample {
double bottomShaped = Math.pow(bottomClamped, Math.max(0.1, entry.getBottomExponent()));
int minDepth = Math.max(0, entry.getBottomDepthMin());
int maxDepth = Math.max(minDepth, entry.getBottomDepthMax());
int depth = minDepth + (int) Math.round(bottomShaped * (maxDepth - minDepth) * edgeFade);
int depth = roundedEdgeDepth(minDepth, maxDepth, bottomShaped, edgeFade);
int botY = baseY - depth;
Integer minAbsoluteY = entry.getMinAbsoluteY();
@@ -242,8 +243,6 @@ public final class FloatingIslandSample {
double carveThreshold = entry.getCarveThreshold();
boolean useWarp = wallWarp != null && warpAmp > 0;
boolean useCarve = carve != null && carveThreshold < 1.0;
int solidCount = 0;
int highestSolidIdx = -1;
for (int k = 0; k < thickness; k++) {
int wy = botY + k;
@@ -262,24 +261,14 @@ public final class FloatingIslandSample {
if (layerSigned <= signedCut) {
continue;
}
if (useCarve) {
double cn = carve.noise(wx, wy, wz);
double cnClamped = Math.max(0, Math.min(1, cn));
if (cnClamped > carveThreshold) {
continue;
}
}
solidMask[k] = true;
solidCount++;
if (k > highestSolidIdx) {
highestSolidIdx = k;
}
}
if (!useCarve) {
solidCount = solidifyUncarvedInterior(solidMask);
highestSolidIdx = highestSolidIndex(solidMask);
int solidCount = solidifyUncarvedInterior(solidMask);
if (useCarve) {
solidCount = carveSolidInterior(solidMask, botY, wx, wz, carve, carveThreshold);
}
int highestSolidIdx = highestSolidIndex(solidMask);
if (solidCount == 0 || highestSolidIdx < 0) {
return reject(REJECT_NO_SOLID);
@@ -291,6 +280,80 @@ public final class FloatingIslandSample {
return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask);
}
static double edgeFade(double signed, double signedCut) {
double edge = (signed - signedCut) / EDGE_ROUNDING_BAND;
double edgeClamped = Math.max(0, Math.min(1, edge));
return edgeClamped * edgeClamped * (3.0 - 2.0 * edgeClamped);
}
static int roundedEdgeHeight(int topHeight, double edgeFade) {
return Math.max(0, (int) Math.round(Math.max(0, topHeight) * Math.max(0, Math.min(1, edgeFade))));
}
static int roundedEdgeDepth(int minDepth, int maxDepth, double bottomShaped, double edgeFade) {
int min = Math.max(0, minDepth);
int max = Math.max(min, maxDepth);
double shaped = Math.max(0, Math.min(1, bottomShaped));
double fade = Math.max(0, Math.min(1, edgeFade));
double fullDepth = min + shaped * (max - min);
return (int) Math.round(fullDepth * fade);
}
static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, CNG carve, double carveThreshold) {
int firstSolid = -1;
int lastSolid = -1;
for (int i = 0; i < solidMask.length; i++) {
if (!solidMask[i]) {
continue;
}
if (firstSolid < 0) {
firstSolid = i;
}
lastSolid = i;
}
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;
continue;
}
}
if (solidMask[i]) {
count++;
}
}
return count;
}
static boolean hasFootprintNeighborSupport(CNG footprintCng, int wx, 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;
}
double footprintValue = footprintCng.noise(wx + dx, wz + dz);
double signed = (Math.max(0, Math.min(1, footprintValue)) * 2.0) - 1.0;
if (signed <= signedCut) {
continue;
}
if (Math.abs(dx) + Math.abs(dz) == 1) {
cardinal++;
} else {
diagonal++;
}
}
}
return cardinal > 0 || diagonal >= 2;
}
static int solidifyUncarvedInterior(boolean[] solidMask) {
int firstSolid = -1;
int lastSolid = -1;
@@ -18,21 +18,16 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.data.B;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
public class FloatingObjectFootprint {
private static final ConcurrentHashMap<String, FloatingObjectFootprint> CACHE = new ConcurrentHashMap<>();
private static final boolean DIAGNOSTIC_LOG = Boolean.parseBoolean(System.getProperty("iris.floating.footprintLog", "true"));
private final int lowestSolidKeyY;
private final int highestSolidKeyY;
@@ -111,98 +106,9 @@ public class FloatingObjectFootprint {
int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL);
int tallestKxBottom = columnStats.isEmpty() ? 0 : globalHighestKx[0];
int tallestKzBottom = columnStats.isEmpty() ? 0 : globalHighestKz[0];
if (DIAGNOSTIC_LOG) {
logFootprintDiagnostic(cacheKey, obj, cx, cy, cz, lowestSolidKeyY, tallestKx, tallestKz, columnStats);
}
return new FloatingObjectFootprint(lowestSolidKeyY, highestSolidKeyY, cx, cy, cz, tallestKx, tallestKz, tallestKxBottom, tallestKzBottom, footprintArray);
}
private static void logFootprintDiagnostic(String cacheKey, IrisObject obj, int cx, int cy, int cz, int anchorY, int tallestKx, int tallestKz, Map<Long, int[]> columnStats) {
if (columnStats.isEmpty()) {
Iris.info("[FloatingFootprint] key=" + cacheKey + " center=(" + cx + "," + cy + "," + cz + ") anchor=" + anchorY + " columns=0 (EMPTY)");
return;
}
int tallestCount = 0;
int tallestLow = Integer.MAX_VALUE;
int floorLow = Integer.MAX_VALUE;
int ceilingLow = Integer.MIN_VALUE;
int minKx = Integer.MAX_VALUE, maxKx = Integer.MIN_VALUE;
int minKz = Integer.MAX_VALUE, maxKz = Integer.MIN_VALUE;
TreeMap<Integer, Integer> lowYHisto = new TreeMap<>();
for (Map.Entry<Long, int[]> e : columnStats.entrySet()) {
long packed = e.getKey();
int kx = (int) (packed >> 32);
int kz = (int) (packed & 0xFFFFFFFFL);
if (kx < minKx) minKx = kx;
if (kx > maxKx) maxKx = kx;
if (kz < minKz) minKz = kz;
if (kz > maxKz) maxKz = kz;
int[] s = e.getValue();
int count = s[1];
int low = s[0];
if (count > tallestCount || (count == tallestCount && low < tallestLow)) {
tallestCount = count;
tallestLow = low;
}
if (low < floorLow) floorLow = low;
if (low > ceilingLow) ceilingLow = low;
lowYHisto.merge(low, 1, Integer::sum);
}
int straysBelowAnchor = 0;
for (int[] s : columnStats.values()) {
if (s[0] < anchorY) straysBelowAnchor++;
}
List<Map.Entry<Long, int[]>> sorted = new ArrayList<>(columnStats.entrySet());
sorted.sort((a, b) -> {
int cmp = Integer.compare(b.getValue()[1], a.getValue()[1]);
if (cmp != 0) return cmp;
return Integer.compare(a.getValue()[0], b.getValue()[0]);
});
StringBuilder topN = new StringBuilder();
int showN = Math.min(4, sorted.size());
for (int i = 0; i < showN; i++) {
Map.Entry<Long, int[]> e = sorted.get(i);
int kx = (int) (e.getKey() >> 32);
int kz = (int) (e.getKey() & 0xFFFFFFFFL);
int[] s = e.getValue();
if (i > 0) topN.append(",");
topN.append("(").append(kx).append(",").append(kz).append(")c=").append(s[1]).append(":y=").append(s[0]);
}
StringBuilder histo = new StringBuilder();
int histoEntries = 0;
for (Map.Entry<Integer, Integer> e : lowYHisto.entrySet()) {
if (histoEntries++ > 0) histo.append(",");
histo.append(e.getKey()).append("=").append(e.getValue());
if (histoEntries >= 6) {
if (lowYHisto.size() > 6) histo.append(",...+").append(lowYHisto.size() - 6);
break;
}
}
String keyStyle = (minKx >= 0 && minKz >= 0) ? "RAW" : "SIGNED";
Iris.info("[FloatingFootprint] key=" + cacheKey
+ " dims=" + obj.getW() + "x" + obj.getH() + "x" + obj.getD()
+ " center=(" + cx + "," + cy + "," + cz + ")"
+ " keyStyle=" + keyStyle
+ " kxRange=[" + minKx + "," + maxKx + "]"
+ " kzRange=[" + minKz + "," + maxKz + "]"
+ " cols=" + columnStats.size()
+ " anchor=" + anchorY
+ " relAnchor=" + (anchorY - cy)
+ " tallestKxKz=(" + tallestKx + "," + tallestKz + ")"
+ " floorLow=" + floorLow
+ " ceilingLow=" + ceilingLow
+ " tallest=count" + tallestCount + ":y" + tallestLow
+ " straysBelow=" + straysBelowAnchor
+ " topCols=[" + topN + "]"
+ " lowYHisto={" + histo + "}");
}
private static long resolveTallestColumn(Map<Long, int[]> columnStats) {
long bestPacked = 0L;
int tallestCount = 0;
@@ -162,6 +162,13 @@ public class IrisFloatingChildBiomes implements IRare {
@Desc("Power curve applied to the bottom noise before mapping to depth. >1 = most columns shallow with occasional deeper spikes (sparse roots). <1 = most columns deep with occasional shallow spots (dense curtains). 1.0 = linear.")
private double bottomExponent = 1.0;
@Desc("Controls the material palette near the island underside. DEPTH keeps the old top-down depth behavior. MIRROR_TOP uses the target biome's shallow/top palette from the underside upward. CUSTOM uses bottomPalette near the underside while keeping the target biome palette near the top.")
private FloatingBottomPaletteMode bottomPaletteMode = FloatingBottomPaletteMode.DEPTH;
@ArrayType(min = 1, type = IrisBiomePaletteLayer.class)
@Desc("Custom palette layers used near the underside when bottomPaletteMode=CUSTOM. The layer format is the same as normal biome layers.")
private KList<IrisBiomePaletteLayer> bottomPalette = new KList<>();
@MinNumber(1)
@MaxNumber(512)
@Desc("Hard cap on the total Y-extent (top minus bottom) of a single island column. Safety limit.")
@@ -75,6 +75,22 @@ public class IrisObjectRotation {
return rt;
}
public static IrisObjectRotation xFlip180RandomY() {
IrisObjectRotation rt = xFlip180();
rt.setYAxis(new IrisAxisRotationClamp(true, false, 0, 0, 90));
return rt;
}
public static IrisObjectRotation xFlip180WithY(double y) {
IrisObjectRotation rt = xFlip180();
IrisAxisRotationClamp rty = new IrisAxisRotationClamp();
rty.setEnabled(true);
rty.setInterval(90);
rty.minMax(y);
rt.setYAxis(rty);
return rt;
}
public static IrisObjectRotation of(double x, double y, double z) {
IrisObjectRotation rt = new IrisObjectRotation();
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
@@ -1,11 +1,43 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.object.FloatingObjectFootprint;
import art.arcane.iris.engine.object.IrisObjectRotation;
import org.junit.Test;
import java.lang.reflect.Constructor;
import static org.junit.Assert.assertEquals;
public class MantleFloatingObjectComponentInvertedCountersTest {
private FloatingObjectFootprint footprint(int lowestSolidKeyY, int highestSolidKeyY, int tallestKx, int tallestKz) throws Exception {
Constructor<FloatingObjectFootprint> constructor = FloatingObjectFootprint.class.getDeclaredConstructor(
int.class,
int.class,
int.class,
int.class,
int.class,
int.class,
int.class,
int.class,
int.class,
long[].class
);
constructor.setAccessible(true);
return constructor.newInstance(
lowestSolidKeyY,
highestSolidKeyY,
0,
0,
0,
tallestKx,
tallestKz,
99,
99,
new long[0]
);
}
@Test
public void resetObjectCounters_resetsAllInvertedCountersToZero() {
MantleFloatingObjectComponent.objectsInvertedAttempted.set(5);
@@ -39,4 +71,49 @@ public class MantleFloatingObjectComponentInvertedCountersTest {
assertEquals(0, MantleFloatingObjectComponent.objectsAttempted.get());
assertEquals(0, MantleFloatingObjectComponent.objectsPlaced.get());
}
@Test
public void invertedBaseY_anchorsOriginalLowestSolidBelowBottomFace() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
assertEquals(104, MantleFloatingObjectComponent.invertedBaseY(100, footprint));
}
@Test
public void invertedBaseX_usesTopFootprintAnchor() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
assertEquals(106, MantleFloatingObjectComponent.invertedBaseX(100, 8, footprint));
}
@Test
public void invertedBaseZ_mirrorsTopFootprintAnchor() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
assertEquals(111, MantleFloatingObjectComponent.invertedBaseZ(100, 8, footprint));
}
@Test
public void invertedBaseX_usesFixedYRotationAnchor() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
IrisObjectRotation rotation = IrisObjectRotation.xFlip180WithY(90);
assertEquals(111, MantleFloatingObjectComponent.invertedBaseX(100, 8, footprint, rotation));
}
@Test
public void invertedBaseZ_usesFixedYRotationAnchor() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
IrisObjectRotation rotation = IrisObjectRotation.xFlip180WithY(90);
assertEquals(110, MantleFloatingObjectComponent.invertedBaseZ(100, 8, footprint, rotation));
}
@Test
public void invertedBaseY_isStableAcrossFixedYRotation() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
IrisObjectRotation rotation = IrisObjectRotation.xFlip180WithY(270);
assertEquals(104, MantleFloatingObjectComponent.invertedBaseY(100, footprint, rotation));
}
}
@@ -1,5 +1,8 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.NoiseGenerator;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -15,7 +18,9 @@ public class FloatingIslandSampleBottomYTest {
}
}
for (boolean b : solidMask) {
if (b) solidCount++;
if (b) {
solidCount++;
}
}
return FloatingIslandSample.constructForTest(islandBaseY, solidMask.length, topIdx, solidCount, solidMask);
}
@@ -72,4 +77,84 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(false, mask[1]);
assertEquals(false, mask[2]);
}
@Test
public void roundedEdgeHeight_zeroFade_removesEdgeWall() {
assertEquals(0, FloatingIslandSample.roundedEdgeHeight(18, 0.0));
}
@Test
public void roundedEdgeDepth_zeroFade_removesMinimumTailAtEdge() {
assertEquals(0, FloatingIslandSample.roundedEdgeDepth(10, 20, 1.0, 0.0));
}
@Test
public void roundedEdgeDepth_fullFade_keepsConfiguredDepth() {
assertEquals(15, FloatingIslandSample.roundedEdgeDepth(10, 20, 0.5, 1.0));
}
@Test
public void carveSolidInterior_preservesOuterShell() {
boolean[] mask = {false, true, true, true, true, false};
CNG carve = new CNG(new RNG(1), 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(2, count);
assertEquals(false, mask[0]);
assertEquals(true, mask[1]);
assertEquals(false, mask[2]);
assertEquals(false, mask[3]);
assertEquals(true, mask[4]);
assertEquals(false, mask[5]);
}
@Test
public void hasFootprintNeighborSupport_rejectsSingleIsolatedColumn() {
CNG footprint = new CNG(new RNG(2)) {
@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);
}
};
assertEquals(false, FloatingIslandSample.hasFootprintNeighborSupport(footprint, 0, 0, 0.0));
}
@Test
public void hasFootprintNeighborSupport_acceptsCardinalNeighbor() {
CNG footprint = new CNG(new RNG(3)) {
@Override
public double noise(double x, double z) {
return (x == 0 && z == 0) || (x == 1 && z == 0) ? 1.0 : 0.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertEquals(true, FloatingIslandSample.hasFootprintNeighborSupport(footprint, 0, 0, 0.0));
}
}
@@ -19,6 +19,33 @@ public class IrisObjectRotationFlipTest {
assertTrue(!rot.canRotateY());
}
@Test
public void xFlip180RandomY_canRotateY_returnsTrue() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180RandomY();
assertTrue(rot.canRotateY());
}
@Test
public void xFlip180WithY_zeroYaw_stillUsesFixedYRotation() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180WithY(0);
BlockVector v = new BlockVector(1, 2, 3);
BlockVector result = rot.rotate(v, 117, 253, 91);
assertTrue(rot.canRotateY());
assertEquals(1, result.getBlockX());
assertEquals(-2, result.getBlockY());
assertEquals(-3, result.getBlockZ());
}
@Test
public void xFlip180WithY_ninetyYaw_rotatesMirroredFootprint() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180WithY(90);
BlockVector v = new BlockVector(2, 5, 3);
BlockVector result = rot.rotate(v, 0, 0, 0);
assertEquals(-3, result.getBlockX());
assertEquals(-5, result.getBlockY());
assertEquals(-2, result.getBlockZ());
}
@Test
public void xFlip180_rotateVector_negatesYandZ() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180();
@@ -51,6 +51,31 @@ public class IslandObjectPlacerAnchorFaceTest {
assertEquals(1, placer.getWritesDroppedAboveBottom());
}
@Test
public void bottomFace_canWriteObjectBlock_allowsBelowAnchorWithoutCounting() {
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() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
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];