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
+1 -1
View File
@@ -7,7 +7,7 @@ The master branch is for the latest version of minecraft.
# Building # Building
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development. development.[README.md](README.md)
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone. Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
@@ -126,7 +126,13 @@ public class IslandObjectPlacer implements IObjectPlacer {
} }
private boolean shouldSkipAirColumn(int x, int y, int z) { 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 xf = x - minX;
int zf = z - minZ; int zf = z - minZ;
if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) { if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) {
@@ -136,27 +142,37 @@ public class IslandObjectPlacer implements IObjectPlacer {
return false; return false;
} }
if (y >= anchorY) { if (y >= anchorY) {
writesDroppedAboveBottom++; if (countWrite) {
writesDroppedAboveBottom++;
}
return true; return true;
} }
return false; return false;
} }
if (face == AnchorFace.TOP) { if (face == AnchorFace.TOP) {
if (y <= anchorY) { if (y <= anchorY) {
writesDroppedBelow++; if (countWrite) {
writesDroppedBelow++;
}
return true; return true;
} }
if (!overhangAllowed[idx]) { if (!overhangAllowed[idx]) {
writesDroppedOverhang++; if (countWrite) {
writesDroppedOverhang++;
}
return true; return true;
} }
} else { } else {
if (y >= anchorY) { if (y >= anchorY) {
writesDroppedBottomOverhang++; if (countWrite) {
writesDroppedBottomOverhang++;
}
return true; return true;
} }
if (!overhangAllowed[idx]) { if (!overhangAllowed[idx]) {
writesDroppedBottomOverhang++; if (countWrite) {
writesDroppedBottomOverhang++;
}
return true; return true;
} }
} }
@@ -164,19 +180,29 @@ public class IslandObjectPlacer implements IObjectPlacer {
} }
if (face == AnchorFace.TOP) { if (face == AnchorFace.TOP) {
if (y <= anchorY) { if (y <= anchorY) {
writesDroppedBelow++; if (countWrite) {
writesDroppedBelow++;
}
return true; return true;
} }
} else { } else {
if (y >= anchorY) { if (y >= anchorY) {
writesDroppedBottomOverhang++; if (countWrite) {
writesDroppedBottomOverhang++;
}
return true; return true;
} }
} }
writesDroppedOverhang++; if (countWrite) {
writesDroppedOverhang++;
}
return true; 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) { private @Nullable FloatingIslandSample sampleAt(int x, int z) {
int xf = x - minX; int xf = x - minX;
int zf = z - minZ; 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.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.FloatingIslandSample; import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.FloatingObjectFootprint; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes; import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObject; 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.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectTranslate; import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.ObjectPlaceMode; 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.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG; 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; import java.util.concurrent.atomic.AtomicLong;
@ComponentFlag(ReservedFlag.FLOATING_OBJECT) @ComponentFlag(ReservedFlag.FLOATING_OBJECT)
@@ -53,7 +65,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
public static final AtomicLong objectsRelaxed = new AtomicLong(); public static final AtomicLong objectsRelaxed = new AtomicLong();
public static final AtomicLong objectsSkippedShrink = new AtomicLong(); public static final AtomicLong objectsSkippedShrink = new AtomicLong();
public static final AtomicLong objectsSkippedNullObj = 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 writesAttemptedTotal = new AtomicLong();
public static final AtomicLong writesDroppedBelowTotal = new AtomicLong(); public static final AtomicLong writesDroppedBelowTotal = new AtomicLong();
public static final AtomicLong writesDroppedOverhangTotal = 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 objectsInvertedSkippedNullObj = new AtomicLong();
public static final AtomicLong writesDroppedAboveBottomTotal = new AtomicLong(); public static final AtomicLong writesDroppedAboveBottomTotal = new AtomicLong();
public static final AtomicLong writesDroppedBottomOverhangTotal = 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 MIN_FOOTPRINT_CELLS_CHECKED = 3;
private static final int INVERTED_PICK_ATTEMPTS = 8;
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0); 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) { public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2); super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
@@ -85,11 +93,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
objectsRelaxed.set(0); objectsRelaxed.set(0);
objectsSkippedShrink.set(0); objectsSkippedShrink.set(0);
objectsSkippedNullObj.set(0); objectsSkippedNullObj.set(0);
terrainMismatchWarnings.set(0);
writesAttemptedTotal.set(0); writesAttemptedTotal.set(0);
writesDroppedBelowTotal.set(0); writesDroppedBelowTotal.set(0);
writesDroppedOverhangTotal.set(0); writesDroppedOverhangTotal.set(0);
heavyClipWarnings.set(0);
anchorYHisto.clear(); anchorYHisto.clear();
objectsInvertedAttempted.set(0); objectsInvertedAttempted.set(0);
objectsInvertedPlaced.set(0); objectsInvertedPlaced.set(0);
@@ -101,26 +107,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writesDroppedBottomOverhangTotal.set(0); 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 attempted = islandPlacer.getWritesAttempted();
int below = islandPlacer.getWritesDroppedBelow(); int below = islandPlacer.getWritesDroppedBelow();
int overhang = islandPlacer.getWritesDroppedOverhang(); int overhang = islandPlacer.getWritesDroppedOverhang();
writesAttemptedTotal.addAndGet(attempted); writesAttemptedTotal.addAndGet(attempted);
writesDroppedBelowTotal.addAndGet(below); writesDroppedBelowTotal.addAndGet(below);
writesDroppedOverhangTotal.addAndGet(overhang); 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) { private static void recordInvertedWriteStats(IslandObjectPlacer islandPlacer) {
@@ -128,34 +121,6 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writesDroppedBottomOverhangTotal.addAndGet(islandPlacer.getWritesDroppedBottomOverhang()); 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 @Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex(); 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++) { for (int i = 0; i < 256; i++) {
FloatingIslandSample s = samples[i]; FloatingIslandSample s = samples[i];
if (s == null || s.entry == null) { if (s == null || s.entry == null) {
@@ -193,7 +158,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
entryColumns.computeIfAbsent(s.entry, e -> new KList<>()).add(i); 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(); IrisFloatingChildBiomes entry = ec.getKey();
KList<Integer> columns = ec.getValue(); KList<Integer> columns = ec.getValue();
if (columns.isEmpty()) { if (columns.isEmpty()) {
@@ -280,7 +245,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
try { try {
obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> { obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> {
String marker = placementMarker(obj, id); 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); writer.setData(b.getX(), b.getY(), b.getZ(), marker);
} }
}, null, data); }, null, data);
@@ -367,16 +332,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
try { try {
obj.place(wx, yv, wz, islandPlacer, anchored, rng, (b, bd) -> { obj.place(wx, yv, wz, islandPlacer, anchored, rng, (b, bd) -> {
String marker = placementMarker(obj, id); 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); writer.setData(b.getX(), b.getY(), b.getZ(), marker);
} }
}, null, data); }, null, data);
objectsPlaced.incrementAndGet(); objectsPlaced.incrementAndGet();
recordAnchorYHisto(pickTopY); recordAnchorYHisto(pickTopY);
int trunkWx = minX + pickedXf; recordWriteStats(islandPlacer);
int trunkWz = minZ + pickedZf;
verifyTerrainBelowObject(obj, trunkWx, trunkWz, pickTopY, pickedSample);
recordWriteStats(obj, trunkWx, trunkWz, pickTopY, islandPlacer);
} catch (Throwable e) { } catch (Throwable e) {
Iris.reportError(e); Iris.reportError(e);
} }
@@ -417,44 +381,56 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
final IrisObject obj = obj0; final IrisObject obj = obj0;
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj); FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
int invertedYRotation = rng.i(0, 3) * 90;
IrisObjectRotation invertedRotation = IrisObjectRotation.xFlip180WithY(invertedYRotation);
KList<Integer> pool = interior.isEmpty() ? columns : interior; KList<Integer> pool = interior.isEmpty() ? columns : interior;
if (interior.isEmpty()) { if (interior.isEmpty()) {
objectsInvertedFallbackNoInterior.incrementAndGet(); objectsInvertedFallbackNoInterior.incrementAndGet();
} }
int pickedKey = pool.get(rng.i(0, pool.size() - 1)); int pickedXf = -1;
int pickedXf = pickedKey & 15; int pickedZf = -1;
int pickedZf = pickedKey >> 4; int pickBottomY = -1;
FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf]; boolean foundBottomAnchor = false;
if (pickedSample == null) { for (int attempt = 0; attempt < INVERTED_PICK_ATTEMPTS; attempt++) {
objectsInvertedSkippedNoFlat.incrementAndGet(); int pickedKey = pool.get(rng.i(0, pool.size() - 1));
continue; int candidateXf = pickedKey & 15;
} int candidateZf = pickedKey >> 4;
int pickBottomY = pickedSample.bottomY(); FloatingIslandSample candidateSample = samples[(candidateZf << 4) | candidateXf];
if (pickBottomY < 0) { if (candidateSample == null) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
if (!isFootprintFlatBottom(fp, pickedXf, pickedZf, pickBottomY, samples, 2)) {
if (!isFootprintFlatBottom(fp, pickedXf, pickedZf, pickBottomY, samples, 4)) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue; 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 wx = invertedBaseX(minX, pickedXf, fp, invertedRotation);
int wz = minZ + pickedZf - fp.getTallestKzBottom(); int wz = invertedBaseZ(minZ, pickedZf, fp, invertedRotation);
IrisObjectPlacement inverted = placement.toPlacement(obj.getLoadKey()); IrisObjectPlacement inverted = placement.toPlacement(obj.getLoadKey());
inverted.setMode(translateStiltModeForFloating(inverted.getMode())); inverted.setMode(translateStiltModeForFloating(inverted.getMode()));
inverted.setTranslate(new IrisObjectTranslate()); inverted.setTranslate(new IrisObjectTranslate());
inverted.setRotation(IrisObjectRotation.xFlip180()); inverted.setRotation(invertedRotation);
inverted.setForcePlace(true); inverted.setForcePlace(true);
inverted.setBottom(false); 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); IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickBottomY, IslandObjectPlacer.AnchorFace.BOTTOM);
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
@@ -462,7 +438,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
try { try {
obj.place(wx, yv, wz, islandPlacer, inverted, rng, (b, bd) -> { obj.place(wx, yv, wz, islandPlacer, inverted, rng, (b, bd) -> {
String marker = placementMarker(obj, id); 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); writer.setData(b.getX(), b.getY(), b.getZ(), marker);
} }
}, null, data); }, 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) { private static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, IrisObjectRotation rotation, int pickedXf, int pickedZf, int pickBottomY, FloatingIslandSample[] samples, int tolerance) {
int tallestKxBottom = fp.getTallestKxBottom(); BlockVector anchor = invertedFootprintAnchor(fp, rotation);
int tallestKzBottom = fp.getTallestKzBottom();
int checked = 0; int checked = 0;
boolean touchedChunkEdge = false; boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ(); long[] cells = fp.footprintXZ();
@@ -484,8 +461,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
long encoded = cells[i]; long encoded = cells[i];
int kx = (int) (encoded >> 32); int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL); int kz = (int) (encoded & 0xFFFFFFFFL);
int colXf = pickedXf + (kx - tallestKxBottom); BlockVector cell = rotation.rotate(new BlockVector(kx, 0, kz), 0, 0, 0);
int colZf = pickedZf + (kz - tallestKzBottom); int colXf = pickedXf + cell.getBlockX() - anchor.getBlockX();
int colZf = pickedZf + cell.getBlockZ() - anchor.getBlockZ();
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) { if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true; touchedChunkEdge = true;
continue; continue;
@@ -506,6 +484,48 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return touchedChunkEdge; 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) { private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) {
int tallestKx = fp.getTallestKx(); int tallestKx = fp.getTallestKx();
int tallestKz = fp.getTallestKz(); int tallestKz = fp.getTallestKz();
@@ -556,10 +576,18 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) { if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) {
continue; continue;
} }
if (samples[(zf << 4) | (xf + 1)] == null) continue; if (samples[(zf << 4) | (xf + 1)] == null) {
if (samples[(zf << 4) | (xf - 1)] == null) continue; 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 + 1) << 4) | xf] == null) {
continue;
}
if (samples[((zf - 1) << 4) | xf] == null) {
continue;
}
interior.add(key); interior.add(key);
} }
return interior; return interior;
@@ -592,7 +620,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override @Override
protected int computeRadius() { protected int computeRadius() {
int maxObjectExtent = 0; int maxObjectExtent = 0;
java.util.Set<String> objectKeys = new java.util.HashSet<>(); Set<String> objectKeys = new HashSet<>();
try { try {
IrisData data = getData(); IrisData data = getData();
for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) { for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) {
@@ -617,11 +645,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
for (String key : objectKeys) { for (String key : objectKeys) {
try { try {
java.io.File f = data.getObjectLoader().findFile(key); File f = data.getObjectLoader().findFile(key);
if (f == null) continue; if (f == null) {
org.bukkit.util.BlockVector sz = IrisObject.sampleSize(f); continue;
}
BlockVector sz = IrisObject.sampleSize(f);
int extent = Math.max(sz.getBlockX(), sz.getBlockZ()); int extent = Math.max(sz.getBlockX(), sz.getBlockZ());
if (extent > maxObjectExtent) maxObjectExtent = extent; if (extent > maxObjectExtent) {
maxObjectExtent = extent;
}
} catch (Throwable ignored) { } catch (Throwable ignored) {
} }
} }
@@ -630,10 +662,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return Math.max(16, maxObjectExtent); return Math.max(16, maxObjectExtent);
} }
private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, java.util.Set<String> out) { private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, Set<String> out) {
if (placements == null) return; if (placements == null) {
return;
}
for (IrisObjectPlacement p : placements) { for (IrisObjectPlacement p : placements) {
if (p == null || p.getPlace() == null) continue; if (p == null || p.getPlace() == null) {
continue;
}
out.addAll(p.getPlace()); out.addAll(p.getPlace());
} }
} }
@@ -18,25 +18,29 @@
package art.arcane.iris.engine.modifier; package art.arcane.iris.engine.modifier;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.decorator.FloatingDecorator; import art.arcane.iris.engine.decorator.FloatingDecorator;
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator; 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.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier; import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.framework.EngineDecorator; import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent; 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.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome; 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.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes; 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.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk; 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.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterBiomeInject; 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.Biome;
import org.bukkit.block.data.BlockData; 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 class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<BlockData> {
public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL; public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL;
private static final AtomicLong columnsChecked = new AtomicLong(); private static final Runnable NOOP_DECORATION_MISS = () -> {
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 final RNG rng; private final RNG rng;
private final EngineDecorator seaSurfaceDecorator; private final EngineDecorator seaSurfaceDecorator;
public static void reportFloatingStats() { private static KList<BlockData> generateBottomPaletteLayers(IrisFloatingChildBiomes entry, IrisDimension dimension, double wx, double wz, RNG random, int paletteDepth, IrisData data, IrisComplex complex) {
StringBuilder topFloors = new StringBuilder(); if (entry == null || entry.getBottomPaletteMode() != FloatingBottomPaletteMode.CUSTOM || entry.getBottomPalette() == null || entry.getBottomPalette().isEmpty()) {
floorMatHisto.entrySet().stream() return null;
.sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get())) }
.limit(5) return generatePaletteLayers(dimension, entry.getBottomPalette(), wx, wz, random.nextParallelRNG(0xB0770B), paletteDepth, data, complex);
.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 void maybeReport() { private static KList<BlockData> generatePaletteLayers(IrisDimension dimension, KList<IrisBiomePaletteLayer> layers, double wx, double wz, RNG random, int maxDepth, IrisData data, IrisComplex complex) {
long now = System.currentTimeMillis(); KList<BlockData> generated = new KList<>();
long last = lastReportMs.get(); if (layers == null || layers.isEmpty() || maxDepth <= 0) {
if (now - last >= 10000L && lastReportMs.compareAndSet(last, now)) { return generated;
reportFloatingStats(); }
if (reportCycle.incrementAndGet() >= 30) {
reportCycle.set(0); int generatorSeed = 7235;
resetAllCounters(); 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() { private static boolean usesBottomPalette(IrisFloatingChildBiomes entry) {
columnsChecked.set(0); FloatingBottomPaletteMode mode = entry == null || entry.getBottomPaletteMode() == null ? FloatingBottomPaletteMode.DEPTH : entry.getBottomPaletteMode();
samplesAccepted.set(0); return mode != FloatingBottomPaletteMode.DEPTH;
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 void recordFloorMat(String matKey) { private static int[] bottomDepths(FloatingIslandSample sample, int chunkHeight) {
if (floorMatHisto.size() < 32) { int[] bottomDepths = new int[sample.solidMask.length];
floorMatHisto.computeIfAbsent(matKey, k -> new AtomicLong()).incrementAndGet(); for (int i = 0; i < bottomDepths.length; i++) {
} else { bottomDepths[i] = -1;
AtomicLong existing = floorMatHisto.get(matKey);
if (existing != null) {
existing.incrementAndGet();
} else {
floorMatHisto.computeIfAbsent("other", k -> new AtomicLong()).incrementAndGet();
}
} }
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) { public IrisFloatingChildBiomeModifier(Engine engine) {
@@ -174,13 +193,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue; continue;
} }
columnsChecked.incrementAndGet();
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine()); FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine());
if (sample == null) { if (sample == null) {
continue; continue;
} }
samplesAccepted.incrementAndGet();
IrisFloatingChildBiomes entry = sample.entry; IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data); IrisBiome target = entry.getRealBiome(parent, data);
@@ -191,8 +207,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (blocks == null || blocks.isEmpty()) { if (blocks == null || blocks.isEmpty()) {
blocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex); 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"); BlockData fallbackSolid = B.get("minecraft:stone");
int[] bottomDepths = usesBottomPalette(entry) ? bottomDepths(sample, chunkHeight) : null;
int depth = 0; int depth = 0;
for (int k = sample.topIdx; k >= 0; k--) { for (int k = sample.topIdx; k >= 0; k--) {
if (!sample.solidMask[k]) { if (!sample.solidMask[k]) {
@@ -202,13 +220,8 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (y < 0 || y >= chunkHeight) { if (y < 0 || y >= chunkHeight) {
continue; continue;
} }
BlockData block = null; int bottomDepth = bottomDepths == null || bottomDepths[k] < 0 ? depth : bottomDepths[k];
if (blocks != null && !blocks.isEmpty()) { BlockData block = selectPaletteBlock(entry, blocks, bottomBlocks, depth, bottomDepth, fallbackSolid);
block = blocks.hasIndex(depth) ? blocks.get(depth) : blocks.getLast();
}
if (block == null) {
block = fallbackSolid;
}
if (block != null) { if (block != null) {
output.set(xf, y, zf, block); output.set(xf, y, zf, block);
} }
@@ -270,12 +283,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (sample == null) { if (sample == null) {
continue; continue;
} }
decoratePhaseColumns.incrementAndGet();
IrisFloatingChildBiomes entry = sample.entry; IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data); IrisBiome target = entry.getRealBiome(parent, data);
if (!entry.isInheritDecorators() || target == null) { if (!entry.isInheritDecorators() || target == null) {
decorateSkippedNoInherit.incrementAndGet();
continue; continue;
} }
@@ -284,26 +295,12 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (topY + 1 < chunkHeight) { if (topY + 1 < chunkHeight) {
BlockData above = output.get(xf, topY + 1, zf); BlockData above = output.get(xf, topY + 1, zf);
if (above == null || B.isAir(above)) { 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 { try {
RNG colRng = rng.nextParallelRNG((int) FloatingIslandSample.columnSeed(baseSeed, wx, wz)); RNG colRng = rng.nextParallelRNG((int) FloatingIslandSample.columnSeed(baseSeed, wx, wz));
int placed = FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng, INC_DEC_CANDIDATES_NULL); FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng, NOOP_DECORATION_MISS);
if (placed > 0) {
decoratePlaced.addAndGet(placed);
} else {
decorateNoChange.incrementAndGet();
}
} catch (Throwable e) { } catch (Throwable e) {
art.arcane.iris.Iris.reportError(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) { 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_COUNT = 7;
public static final int REJECT_CLUSTER = REJECT_NO_SEED; 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<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]); 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); private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
@@ -169,6 +170,9 @@ public final class FloatingIslandSample {
if (signed <= signedCut) { if (signed <= signedCut) {
return reject(REJECT_NO_SEED); return reject(REJECT_NO_SEED);
} }
if (!hasFootprintNeighborSupport(footprintCng, wx, wz, signedCut)) {
return reject(REJECT_NO_SEED);
}
CNG altitudeCng = entry.getAltitudeCng(baseSeed, data); CNG altitudeCng = entry.getAltitudeCng(baseSeed, data);
if (altitudeCng == null) { if (altitudeCng == null) {
@@ -182,14 +186,11 @@ public final class FloatingIslandSample {
int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface() - worldMin); int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface() - worldMin);
int baseY = minAlt + (int) Math.round(altClamped * (maxAlt - minAlt)); int baseY = minAlt + (int) Math.round(altClamped * (maxAlt - minAlt));
double edgeFade = edgeFade(signed, signedCut);
IrisBiome target = entry.getRealBiome(parent, data); 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; 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); CNG bottomCng = entry.getBottomCng(baseSeed, data);
if (bottomCng == null) { if (bottomCng == null) {
warnNullCng("bottomStyle", parent); warnNullCng("bottomStyle", parent);
@@ -200,7 +201,7 @@ public final class FloatingIslandSample {
double bottomShaped = Math.pow(bottomClamped, Math.max(0.1, entry.getBottomExponent())); double bottomShaped = Math.pow(bottomClamped, Math.max(0.1, entry.getBottomExponent()));
int minDepth = Math.max(0, entry.getBottomDepthMin()); int minDepth = Math.max(0, entry.getBottomDepthMin());
int maxDepth = Math.max(minDepth, entry.getBottomDepthMax()); 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; int botY = baseY - depth;
Integer minAbsoluteY = entry.getMinAbsoluteY(); Integer minAbsoluteY = entry.getMinAbsoluteY();
@@ -242,8 +243,6 @@ public final class FloatingIslandSample {
double carveThreshold = entry.getCarveThreshold(); double carveThreshold = entry.getCarveThreshold();
boolean useWarp = wallWarp != null && warpAmp > 0; boolean useWarp = wallWarp != null && warpAmp > 0;
boolean useCarve = carve != null && carveThreshold < 1.0; boolean useCarve = carve != null && carveThreshold < 1.0;
int solidCount = 0;
int highestSolidIdx = -1;
for (int k = 0; k < thickness; k++) { for (int k = 0; k < thickness; k++) {
int wy = botY + k; int wy = botY + k;
@@ -262,24 +261,14 @@ public final class FloatingIslandSample {
if (layerSigned <= signedCut) { if (layerSigned <= signedCut) {
continue; 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; solidMask[k] = true;
solidCount++;
if (k > highestSolidIdx) {
highestSolidIdx = k;
}
} }
if (!useCarve) { int solidCount = solidifyUncarvedInterior(solidMask);
solidCount = solidifyUncarvedInterior(solidMask); if (useCarve) {
highestSolidIdx = highestSolidIndex(solidMask); solidCount = carveSolidInterior(solidMask, botY, wx, wz, carve, carveThreshold);
} }
int highestSolidIdx = highestSolidIndex(solidMask);
if (solidCount == 0 || highestSolidIdx < 0) { if (solidCount == 0 || highestSolidIdx < 0) {
return reject(REJECT_NO_SOLID); return reject(REJECT_NO_SOLID);
@@ -291,6 +280,80 @@ public final class FloatingIslandSample {
return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask); 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) { static int solidifyUncarvedInterior(boolean[] solidMask) {
int firstSolid = -1; int firstSolid = -1;
int lastSolid = -1; int lastSolid = -1;
@@ -18,21 +18,16 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.B;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector; import org.bukkit.util.BlockVector;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class FloatingObjectFootprint { public class FloatingObjectFootprint {
private static final ConcurrentHashMap<String, FloatingObjectFootprint> CACHE = new ConcurrentHashMap<>(); 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 lowestSolidKeyY;
private final int highestSolidKeyY; private final int highestSolidKeyY;
@@ -111,98 +106,9 @@ public class FloatingObjectFootprint {
int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL); int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL);
int tallestKxBottom = columnStats.isEmpty() ? 0 : globalHighestKx[0]; int tallestKxBottom = columnStats.isEmpty() ? 0 : globalHighestKx[0];
int tallestKzBottom = columnStats.isEmpty() ? 0 : globalHighestKz[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); 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) { private static long resolveTallestColumn(Map<Long, int[]> columnStats) {
long bestPacked = 0L; long bestPacked = 0L;
int tallestCount = 0; 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.") @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; 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) @MinNumber(1)
@MaxNumber(512) @MaxNumber(512)
@Desc("Hard cap on the total Y-extent (top minus bottom) of a single island column. Safety limit.") @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; 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) { public static IrisObjectRotation of(double x, double y, double z) {
IrisObjectRotation rt = new IrisObjectRotation(); IrisObjectRotation rt = new IrisObjectRotation();
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp(); IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
@@ -1,11 +1,43 @@
package art.arcane.iris.engine.mantle.components; 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 org.junit.Test;
import java.lang.reflect.Constructor;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class MantleFloatingObjectComponentInvertedCountersTest { 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 @Test
public void resetObjectCounters_resetsAllInvertedCountersToZero() { public void resetObjectCounters_resetsAllInvertedCountersToZero() {
MantleFloatingObjectComponent.objectsInvertedAttempted.set(5); MantleFloatingObjectComponent.objectsInvertedAttempted.set(5);
@@ -39,4 +71,49 @@ public class MantleFloatingObjectComponentInvertedCountersTest {
assertEquals(0, MantleFloatingObjectComponent.objectsAttempted.get()); assertEquals(0, MantleFloatingObjectComponent.objectsAttempted.get());
assertEquals(0, MantleFloatingObjectComponent.objectsPlaced.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; 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 org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -15,7 +18,9 @@ public class FloatingIslandSampleBottomYTest {
} }
} }
for (boolean b : solidMask) { for (boolean b : solidMask) {
if (b) solidCount++; if (b) {
solidCount++;
}
} }
return FloatingIslandSample.constructForTest(islandBaseY, solidMask.length, topIdx, solidCount, solidMask); return FloatingIslandSample.constructForTest(islandBaseY, solidMask.length, topIdx, solidCount, solidMask);
} }
@@ -72,4 +77,84 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(false, mask[1]); assertEquals(false, mask[1]);
assertEquals(false, mask[2]); 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()); 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 @Test
public void xFlip180_rotateVector_negatesYandZ() { public void xFlip180_rotateVector_negatesYandZ() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180(); IrisObjectRotation rot = IrisObjectRotation.xFlip180();
@@ -51,6 +51,31 @@ public class IslandObjectPlacerAnchorFaceTest {
assertEquals(1, placer.getWritesDroppedAboveBottom()); 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 @Test
public void topFace_existingConstructor_dropsBelowAnchor_noRegression() { public void topFace_existingConstructor_dropsBelowAnchor_noRegression() {
FloatingIslandSample[] samples = new FloatingIslandSample[256]; FloatingIslandSample[] samples = new FloatingIslandSample[256];