This commit is contained in:
Brian Neumann-Fopiano
2026-04-22 09:42:45 -04:00
parent 787c728060
commit 6df718e6ca
10 changed files with 926 additions and 216 deletions
@@ -46,7 +46,6 @@ import org.bukkit.inventory.ItemStack;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public interface INMSBinding {
@@ -162,10 +161,6 @@ public interface INMSBinding {
KMap<Material, List<BlockProperty>> getBlockProperties();
default Map<String, byte[]> extractVanillaDatapack() {
return Map.of();
}
private void validateDimensionTypes(WorldCreator c) {
if (c.generator() instanceof PlatformChunkGenerator gen
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
@@ -0,0 +1,134 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.decorator;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import java.util.concurrent.atomic.AtomicLong;
/*
* Floating island decoration path. Bypasses all canGoOn, slope, whitelist, and blacklist
* gating from IrisSurfaceDecorator — the island top IS the biome's designated surface by
* construction, so those material-compatibility checks are never meaningful here.
*/
public class FloatingDecorator {
public static final AtomicLong decCandidatesNull = new AtomicLong();
private static final long SEED_SALT = 29356788L;
private static final long PART_SALT = 10439677L;
public static int decorateColumn(Engine engine, IrisBiome target, IrisDecorationPart part,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng) {
long gSeed = engine.getSeedManager().getDecorator() + SEED_SALT - (part.ordinal() * PART_SALT);
RNG gRNG = new RNG(gSeed);
KList<IrisDecorator> candidates = new KList<>();
for (IrisDecorator d : target.getDecorators()) {
try {
if (d.getPartOf().equals(part) && d.getBlockData(target, gRNG, realX, realZ, engine.getData()) != null) {
candidates.add(d);
}
} catch (Throwable e) {
Iris.reportError(e);
}
}
if (candidates.isEmpty()) {
decCandidatesNull.incrementAndGet();
return 0;
}
IrisDecorator decorator = candidates.get(rng.nextInt(candidates.size()));
if (!decorator.isStacking()) {
return placeSimple(decorator, target, xf, zf, realX, realZ, height, max, data, rng, engine);
} else {
return placeStacked(decorator, target, xf, zf, realX, realZ, height, max, data, rng, engine);
}
}
private static int placeSimple(IrisDecorator decorator, IrisBiome target,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng, Engine engine) {
BlockData bd = decorator.getBlockData100(target, rng, realX, height, realZ, engine.getData());
if (bd == null) {
return 0;
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (max > 2) {
data.set(xf, height + 2, zf, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (max > 1) {
data.set(xf, height + 1, zf, bd);
return 1;
}
return 0;
}
private static int placeStacked(IrisDecorator decorator, IrisBiome target,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng, Engine engine) {
int stack = decorator.getHeight(rng, realX, realZ, engine.getData());
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
int placed = 0;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max) {
break;
}
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(target, rng, realX, height + i, realZ, engine.getData())
: decorator.getBlockData100(target, rng, realX, height + i, realZ, engine.getData());
if (bd == null) {
break;
}
data.set(xf, h, zf, bd);
placed++;
}
return placed;
}
}
@@ -1,33 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDecorator;
public class IrisFloatingSurfaceDecorator extends IrisSurfaceDecorator {
public IrisFloatingSurfaceDecorator(Engine engine) {
super(engine, "Floating Surface");
}
@Override
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) {
return true;
}
}
@@ -0,0 +1,213 @@
/*
* 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.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.util.common.data.B;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.Nullable;
public class IslandObjectPlacer implements IObjectPlacer {
private static final int OVERHANG_RADIUS = 2;
private final MantleWriter wrapped;
private final FloatingIslandSample[] samples;
private final int minX;
private final int minZ;
private final int chunkMaxIslandTopY;
private final int anchorTopY;
private int writesAttempted;
private int writesDroppedBelow;
private int writesDroppedOverhang;
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorTopY) {
this.wrapped = wrapped;
this.samples = samples;
this.minX = minX;
this.minZ = minZ;
this.anchorTopY = anchorTopY;
int maxY = -1;
for (FloatingIslandSample s : samples) {
if (s != null) {
int ty = s.topY();
if (ty > maxY) {
maxY = ty;
}
}
}
this.chunkMaxIslandTopY = maxY;
}
public int getWritesAttempted() {
return writesAttempted;
}
public int getWritesDroppedBelow() {
return writesDroppedBelow;
}
public int getWritesDroppedOverhang() {
return writesDroppedOverhang;
}
private boolean shouldSkipAirColumn(int x, int y, int z) {
writesAttempted++;
if (sampleAt(x, z) != null) {
return false;
}
if (y <= anchorTopY) {
writesDroppedBelow++;
return true;
}
if (!hasIslandNeighborWithin(x, z, OVERHANG_RADIUS)) {
writesDroppedOverhang++;
return true;
}
return false;
}
private boolean hasIslandNeighborWithin(int x, int z, int radius) {
int xf = x - minX;
int zf = z - minZ;
boolean touchedChunkEdge = false;
for (int dx = -radius; dx <= radius; dx++) {
int nxf = xf + dx;
for (int dz = -radius; dz <= radius; dz++) {
int nzf = zf + dz;
if (nxf < 0 || nxf >= 16 || nzf < 0 || nzf >= 16) {
touchedChunkEdge = true;
continue;
}
if (samples[(nzf << 4) | nxf] != null) {
return true;
}
}
}
return touchedChunkEdge;
}
private @Nullable FloatingIslandSample sampleAt(int x, int z) {
int xf = x - minX;
int zf = z - minZ;
if (xf < 0 || xf >= 16 || zf < 0 || zf >= 16) {
return null;
}
return samples[(zf << 4) | xf];
}
@Override
public int getHighest(int x, int z, IrisData data) {
FloatingIslandSample s = sampleAt(x, z);
if (s != null) {
return s.topY();
}
return chunkMaxIslandTopY;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
FloatingIslandSample s = sampleAt(x, z);
if (s != null) {
return s.topY();
}
return chunkMaxIslandTopY;
}
@Override
public boolean isUnderwater(int x, int z) {
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
FloatingIslandSample s = sampleAt(x, z);
if (s != null) {
int idx = y - s.islandBaseY;
if (idx >= 0 && idx < s.solidMask.length) {
return s.solidMask[idx];
}
return false;
}
return wrapped.isSolid(x, y, z);
}
@Override
public boolean isCarved(int x, int y, int z) {
return wrapped.isCarved(x, y, z);
}
@Override
public void set(int x, int y, int z, BlockData d) {
if (shouldSkipAirColumn(x, y, z)) {
return;
}
wrapped.set(x, y, z, d);
}
@Override
public BlockData get(int x, int y, int z) {
return wrapped.get(x, y, z);
}
@Override
public boolean isPreventingDecay() {
return wrapped.isPreventingDecay();
}
@Override
public int getFluidHeight() {
return wrapped.getFluidHeight();
}
@Override
public boolean isDebugSmartBore() {
return wrapped.isDebugSmartBore();
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
if (shouldSkipAirColumn(xx, yy, zz)) {
return;
}
wrapped.setTile(xx, yy, zz, tile);
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
if (shouldSkipAirColumn(xx, yy, zz)) {
return;
}
wrapped.setData(xx, yy, zz, data);
}
@Override
public <T> @Nullable T getData(int xx, int yy, int zz, Class<T> t) {
return wrapped.getData(xx, yy, zz, t);
}
@Override
public Engine getEngine() {
return wrapped.getEngine();
}
}
@@ -27,12 +27,14 @@ import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.CarvingMode;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.FloatingObjectFootprint;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObject;
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.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
@@ -40,13 +42,108 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import java.util.concurrent.atomic.AtomicLong;
@ComponentFlag(ReservedFlag.FLOATING_OBJECT)
public class MantleFloatingObjectComponent extends IrisMantleComponent {
public static final AtomicLong objectsAttempted = new AtomicLong();
public static final AtomicLong objectsPlaced = new AtomicLong();
public static final AtomicLong objectsSkippedNoFlat = new AtomicLong();
public static final AtomicLong objectsSkippedNoInterior = new AtomicLong();
public static final AtomicLong objectsRelaxed = new AtomicLong();
public static final AtomicLong objectsSkippedShrink = new AtomicLong();
public static final AtomicLong objectsSkippedNullObj = new AtomicLong();
public static final AtomicLong terrainMismatchWarnings = new AtomicLong();
public static final AtomicLong writesAttemptedTotal = new AtomicLong();
public static final AtomicLong writesDroppedBelowTotal = new AtomicLong();
public static final AtomicLong writesDroppedOverhangTotal = 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;
public static final java.util.concurrent.ConcurrentHashMap<String, AtomicLong> anchorYHisto = new java.util.concurrent.ConcurrentHashMap<>();
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
}
public static void resetObjectCounters() {
objectsAttempted.set(0);
objectsPlaced.set(0);
objectsSkippedNoFlat.set(0);
objectsSkippedNoInterior.set(0);
objectsRelaxed.set(0);
objectsSkippedShrink.set(0);
objectsSkippedNullObj.set(0);
terrainMismatchWarnings.set(0);
writesAttemptedTotal.set(0);
writesDroppedBelowTotal.set(0);
writesDroppedOverhangTotal.set(0);
heavyClipWarnings.set(0);
anchorYHisto.clear();
}
private static void recordWriteStats(IrisObject obj, int wx, int wz, int pickTopY, 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 verifyTerrainBelowObject(MantleWriter writer, IrisObject obj, int wx, int wz, int pickTopY, FloatingIslandSample sample) {
long warned = terrainMismatchWarnings.get();
if (warned >= TERRAIN_MISMATCH_WARNING_CAP) {
return;
}
boolean mantleSolid;
try {
mantleSolid = writer.isSolid(wx, pickTopY, wz);
} catch (Throwable t) {
mantleSolid = false;
}
boolean sampleSolid = sample != null
&& sample.solidMask != null
&& sample.topIdx >= 0
&& sample.topIdx < sample.solidMask.length
&& sample.solidMask[sample.topIdx];
if (mantleSolid && sampleSolid) {
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 + ")"
+ " expected solid below at y=" + pickTopY
+ " mantleSolid=" + mantleSolid
+ " sampleSolid=" + sampleSolid
+ " sampleTopY=" + sampleTop
+ " sampleBaseY=" + sampleBase
+ " sampleTopIdx=" + sampleTopIdx
+ " sampleMaskLen=" + sampleMaskLen);
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
@@ -68,7 +165,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngineMantle().getEngine());
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngineMantle().getEngine());
if (sample != null) {
samples[(zf << 4) | xf] = sample;
}
@@ -103,14 +200,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (entry.isInheritObjects() && target != null) {
for (IrisObjectPlacement placement : target.getSurfaceObjects()) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry, target);
}
}
KList<IrisObjectPlacement> extras = entry.getExtraObjects();
if (extras != null && !extras.isEmpty()) {
for (IrisObjectPlacement placement : extras) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry, target);
}
}
}
@@ -124,20 +221,24 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
objectsAttempted.incrementAndGet();
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
objectsSkippedNullObj.incrementAndGet();
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
}
@@ -156,6 +257,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsPlaced.incrementAndGet();
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -163,67 +265,138 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
@ChunkCoordinates
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry, IrisBiome target) {
if (placement == null || columns.isEmpty()) {
return;
}
KList<Integer> interior = interiorColumns(samples, columns);
KList<Integer> pickPool = interior.isEmpty() ? columns : interior;
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
KList<Integer> interior = interiorColumns(samples, columns);
for (int i = 0; i < density; i++) {
objectsAttempted.incrementAndGet();
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
objectsSkippedNullObj.incrementAndGet();
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
objectsSkippedShrink.incrementAndGet();
continue;
}
}
final IrisObject obj = obj0;
int key = pickPool.get(rng.i(0, pickPool.size() - 1));
int xf = key & 15;
int zf = key >> 4;
FloatingIslandSample sample = samples[(zf << 4) | xf];
if (sample == null) {
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
if (interior.isEmpty()) {
objectsSkippedNoInterior.incrementAndGet();
}
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int pickedXf = pickedKey & 15;
int pickedZf = pickedKey >> 4;
FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf];
if (pickedSample == null) {
objectsSkippedNoFlat.incrementAndGet();
continue;
}
int wx = minX + xf;
int wz = minZ + zf;
int pickTopY = pickedSample.topY();
int anchorY = sample.topY() + 1 + obj.getCenter().getBlockY();
int id = rng.i(0, Integer.MAX_VALUE);
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 2)) {
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 4)) {
objectsSkippedNoFlat.incrementAndGet();
continue;
}
objectsRelaxed.incrementAndGet();
}
int wx = minX + pickedXf - fp.getTallestKx();
int wz = minZ + pickedZf - fp.getTallestKz();
IrisObjectPlacement anchored = placement.toPlacement(obj.getLoadKey());
anchored.setCarvingSupport(CarvingMode.ANYWHERE);
anchored.setMode(translateStiltModeForFloating(anchored.getMode()));
anchored.setTranslate(new IrisObjectTranslate());
anchored.setRotation(IrisObjectRotation.of(0, 0, 0));
anchored.setForcePlace(true);
anchored.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
anchored.setBore(false);
anchored.setMeld(false);
anchored.setBottom(false);
int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickTopY);
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(wx, anchorY, wz, writer, anchored, rng, (b, bd) -> {
obj.place(wx, yv, wz, islandPlacer, anchored, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsPlaced.incrementAndGet();
recordAnchorYHisto(pickTopY);
int trunkWx = minX + pickedXf;
int trunkWz = minZ + pickedZf;
verifyTerrainBelowObject(writer, obj, trunkWx, trunkWz, pickTopY, pickedSample);
recordWriteStats(obj, trunkWx, trunkWz, pickTopY, islandPlacer);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) {
int tallestKx = fp.getTallestKx();
int tallestKz = fp.getTallestKz();
int checked = 0;
boolean touchedChunkEdge = false;
for (long encoded : fp.footprintXZ()) {
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
int colXf = pickedXf + (kx - tallestKx);
int colZf = pickedZf + (kz - tallestKz);
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
}
FloatingIslandSample s = samples[(colZf << 4) | colXf];
if (s == null || Math.abs(s.topY() - pickTopY) > tolerance) {
return false;
}
checked++;
}
if (checked >= MIN_FOOTPRINT_CELLS_CHECKED) {
return true;
}
return touchedChunkEdge;
}
private static void recordAnchorYHisto(int topY) {
String bucket = String.valueOf(topY >> 3);
if (anchorYHisto.size() < 32) {
anchorYHisto.computeIfAbsent(bucket, k -> new AtomicLong()).incrementAndGet();
} else {
AtomicLong existing = anchorYHisto.get(bucket);
if (existing != null) {
existing.incrementAndGet();
} else {
anchorYHisto.computeIfAbsent("other", k -> new AtomicLong()).incrementAndGet();
}
}
}
private static KList<Integer> interiorColumns(FloatingIslandSample[] samples, KList<Integer> columns) {
KList<Integer> interior = new KList<>();
for (int key : columns) {
@@ -252,23 +425,64 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return key + "@" + id;
}
private static ObjectPlaceMode translateStiltModeForFloating(ObjectPlaceMode m) {
return switch (m) {
case STILT -> ObjectPlaceMode.MAX_HEIGHT;
case FAST_STILT -> ObjectPlaceMode.FAST_MAX_HEIGHT;
case MIN_STILT -> ObjectPlaceMode.MIN_HEIGHT;
case FAST_MIN_STILT -> ObjectPlaceMode.FAST_MIN_HEIGHT;
case CENTER_STILT -> ObjectPlaceMode.CENTER_HEIGHT;
case ERODE_STILT -> ObjectPlaceMode.MAX_HEIGHT;
case STRUCTURE_PIECE -> ObjectPlaceMode.CENTER_HEIGHT;
default -> m;
};
}
@Override
protected int computeRadius() {
int maxThickness = 0;
int maxHeightAbove = 0;
int maxObjectExtent = 0;
java.util.Set<String> objectKeys = new java.util.HashSet<>();
try {
IrisData data = getData();
for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) {
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
if (entries == null || entries.isEmpty()) {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
maxThickness = Math.max(maxThickness, entry.getMaxThickness());
maxHeightAbove = Math.max(maxHeightAbove, entry.getMaxHeightAboveSurface());
collectPlacementKeys(entry.getFloatingObjects(), objectKeys);
collectPlacementKeys(entry.getExtraObjects(), objectKeys);
if (entry.isInheritObjects()) {
try {
IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) {
collectPlacementKeys(target.getSurfaceObjects(), objectKeys);
}
} catch (Throwable ignored) {
}
}
}
}
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);
int extent = Math.max(sz.getBlockX(), sz.getBlockZ());
if (extent > maxObjectExtent) maxObjectExtent = extent;
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return Math.max(1, (maxThickness + maxHeightAbove) >> 4);
return Math.max(16, maxObjectExtent);
}
private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, java.util.Set<String> out) {
if (placements == null) return;
for (IrisObjectPlacement p : placements) {
if (p == null || p.getPlace() == null) continue;
out.addAll(p.getPlace());
}
}
}
@@ -21,15 +21,17 @@ package art.arcane.iris.engine.modifier;
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.IrisFloatingSurfaceDecorator;
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.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome;
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.util.common.data.B;
@@ -43,21 +45,23 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.util.concurrent.atomic.AtomicLong;
public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<BlockData> {
public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL;
private static final java.util.concurrent.atomic.AtomicLong columnsChecked = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong samplesAccepted = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateInvocations = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateSkippedNotAir = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateSkippedNoInherit = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decoratePhaseColumns = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decoratePlaced = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateNoChange = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateFloorNull = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.ConcurrentHashMap<String, java.util.concurrent.atomic.AtomicLong> floorMatHisto = new java.util.concurrent.ConcurrentHashMap<>();
private static final java.util.concurrent.atomic.AtomicLong lastReportMs = new java.util.concurrent.atomic.AtomicLong(0L);
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 lastReportMs = new AtomicLong(0L);
private static final AtomicLong reportCycle = new AtomicLong(0L);
private final RNG rng;
private final EngineDecorator surfaceDecorator;
private final EngineDecorator seaSurfaceDecorator;
public static void reportFloatingStats() {
@@ -66,15 +70,36 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
.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()));
art.arcane.iris.Iris.info("[floating-debug] columns=" + columnsChecked.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=" + FloatingDecorator.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()
+ " anchorY:" + (topAnchorY.length() == 0 ? " <none>" : topAnchorY.toString())
+ " topFloors:" + (topFloors.length() == 0 ? " <none>" : topFloors.toString()));
}
@@ -83,13 +108,44 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
long last = lastReportMs.get();
if (now - last >= 10000L && lastReportMs.compareAndSet(last, now)) {
reportFloatingStats();
if (reportCycle.incrementAndGet() >= 30) {
reportCycle.set(0);
resetAllCounters();
}
}
}
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();
FloatingDecorator.decCandidatesNull.set(0);
MantleFloatingObjectComponent.resetObjectCounters();
}
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();
}
}
}
public IrisFloatingChildBiomeModifier(Engine engine) {
super(engine, "FloatingChildBiomes");
rng = new RNG(engine.getSeedManager().getTerrain() ^ 0x7EB0A73F1DCE514DL);
surfaceDecorator = new IrisFloatingSurfaceDecorator(engine);
seaSurfaceDecorator = new IrisSeaSurfaceDecorator(engine);
}
@@ -112,7 +168,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
}
columnsChecked.incrementAndGet();
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngine());
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine());
if (sample == null) {
continue;
}
@@ -202,7 +258,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngine());
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine());
if (sample == null) {
continue;
}
@@ -225,20 +281,19 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
if (floor == null) {
decorateFloorNull.incrementAndGet();
} else {
String matKey = floor.getMaterial().getKey().getKey();
floorMatHisto.computeIfAbsent(matKey, k -> new java.util.concurrent.atomic.AtomicLong()).incrementAndGet();
recordFloorMat(floor.getMaterial().getKey().getKey());
}
try {
surfaceDecorator.decorate(xf, zf, wx, wz, output, target, topY, max);
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);
if (placed > 0) {
decoratePlaced.addAndGet(placed);
} else {
decorateNoChange.incrementAndGet();
}
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
BlockData afterAbove = output.get(xf, topY + 1, zf);
if (afterAbove != null && !B.isAir(afterAbove)) {
decoratePlaced.incrementAndGet();
} else {
decorateNoChange.incrementAndGet();
}
} else {
decorateSkippedNotAir.incrementAndGet();
}
@@ -269,10 +324,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
}
if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight && B.isAir(output.get(xf, fluidTopY + 1, zf))) {
try {
seaSurfaceDecorator.decorate(xf, zf,
wx, wx + 1, wx - 1,
wz, wz + 1, wz - 1,
output, target, fluidTopY, chunkHeight);
seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight);
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
@@ -20,7 +20,6 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
@@ -63,13 +62,13 @@ public final class FloatingIslandSample {
CHUNK_MEMO.get().clear();
}
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, IrisComplex complex, Engine engine) {
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL);
HashMap<Long, FloatingIslandSample> memo = CHUNK_MEMO.get();
if (memo.containsKey(key)) {
return memo.get(key);
}
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, complex, engine);
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine);
memo.put(key, result);
return result;
}
@@ -111,7 +110,7 @@ public final class FloatingIslandSample {
return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L);
}
public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, IrisComplex complex, Engine engine) {
public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
KList<IrisFloatingChildBiomes> entries = parent.getFloatingChildBiomes();
if (entries == null || entries.isEmpty()) {
return reject(REJECT_NO_ENTRIES);
@@ -153,8 +152,6 @@ public final class FloatingIslandSample {
return reject(REJECT_NO_SEED);
}
int surfaceY = (int) Math.round(complex.getHeightStream().get(wx & ~63, wz & ~63));
CNG altitudeCng = entry.getAltitudeCng(baseSeed, data);
if (altitudeCng == null) {
warnNullCng("altitudeStyle", parent);
@@ -162,9 +159,10 @@ public final class FloatingIslandSample {
}
double altNoise = altitudeCng.noise(wx, wz);
double altClamped = Math.max(0, Math.min(1, altNoise));
int minAlt = Math.max(0, entry.getMinHeightAboveSurface());
int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface());
int baseY = surfaceY + minAlt + (int) Math.round(altClamped * (maxAlt - minAlt));
int worldMin = engine.getWorld().minHeight();
int minAlt = Math.max(0, entry.getMinHeightAboveSurface() - worldMin);
int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface() - worldMin);
int baseY = minAlt + (int) Math.round(altClamped * (maxAlt - minAlt));
IrisBiome target = entry.getRealBiome(parent, data);
int topH = computeTopHeight(entry, target, engine, baseSeed, wx, wz, data);
@@ -188,12 +186,12 @@ public final class FloatingIslandSample {
int botY = baseY - depth;
Integer minAbsoluteY = entry.getMinAbsoluteY();
if (minAbsoluteY != null && botY < minAbsoluteY) {
botY = minAbsoluteY;
if (minAbsoluteY != null && botY < minAbsoluteY - worldMin) {
botY = minAbsoluteY - worldMin;
}
Integer maxAbsoluteY = entry.getMaxAbsoluteY();
if (maxAbsoluteY != null && topY > maxAbsoluteY) {
topY = maxAbsoluteY;
if (maxAbsoluteY != null && topY > maxAbsoluteY - worldMin) {
topY = maxAbsoluteY - worldMin;
}
if (botY < 0) {
@@ -0,0 +1,242 @@
/*
* 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.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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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 centerX;
private final int centerY;
private final int centerZ;
private final int tallestKx;
private final int tallestKz;
private final Set<Long> footprintXZ;
private FloatingObjectFootprint(int lowestSolidKeyY, int centerX, int centerY, int centerZ, int tallestKx, int tallestKz, Set<Long> footprintXZ) {
this.lowestSolidKeyY = lowestSolidKeyY;
this.centerX = centerX;
this.centerY = centerY;
this.centerZ = centerZ;
this.tallestKx = tallestKx;
this.tallestKz = tallestKz;
this.footprintXZ = Collections.unmodifiableSet(footprintXZ);
}
public static FloatingObjectFootprint compute(IrisObject obj) {
String cacheKey = obj.getLoadKey() + "@" + obj.getW() + "x" + obj.getH() + "x" + obj.getD();
return CACHE.computeIfAbsent(cacheKey, k -> doCompute(obj, k));
}
private static FloatingObjectFootprint doCompute(IrisObject obj, String cacheKey) {
int cx = obj.getCenter().getBlockX();
int cy = obj.getCenter().getBlockY();
int cz = obj.getCenter().getBlockZ();
Set<Long> xzSet = new HashSet<>();
Map<Long, int[]> columnStats = new HashMap<>();
obj.getBlocks().forEach((BlockVector key, BlockData bd) -> {
if (!B.isSolid(bd)) {
return;
}
int kx = key.getBlockX();
int ky = key.getBlockY();
int kz = key.getBlockZ();
long packed = ((long) kx << 32) | (kz & 0xFFFFFFFFL);
xzSet.add(packed);
int[] stats = columnStats.get(packed);
if (stats == null) {
stats = new int[]{ky, 1};
columnStats.put(packed, stats);
} else {
if (ky < stats[0]) {
stats[0] = ky;
}
stats[1]++;
}
});
long tallestPacked = resolveTallestColumn(columnStats);
int lowestSolidKeyY = columnStats.isEmpty()
? cy
: columnStats.get(tallestPacked)[0];
int tallestKx = columnStats.isEmpty() ? 0 : (int) (tallestPacked >> 32);
int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL);
if (DIAGNOSTIC_LOG) {
logFootprintDiagnostic(cacheKey, obj, cx, cy, cz, lowestSolidKeyY, tallestKx, tallestKz, columnStats);
}
return new FloatingObjectFootprint(lowestSolidKeyY, cx, cy, cz, tallestKx, tallestKz, xzSet);
}
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;
int tallestLow = Integer.MAX_VALUE;
for (Map.Entry<Long, int[]> e : columnStats.entrySet()) {
int[] stats = e.getValue();
int count = stats[1];
int low = stats[0];
if (count > tallestCount || (count == tallestCount && low < tallestLow)) {
tallestCount = count;
tallestLow = low;
bestPacked = e.getKey();
}
}
return bestPacked;
}
public boolean columnInFootprint(int objKeyX, int objKeyZ) {
return footprintXZ.contains(((long) objKeyX << 32) | (objKeyZ & 0xFFFFFFFFL));
}
public int lowestSolidRelCenterY() {
return lowestSolidKeyY - centerY;
}
public int getLowestSolidKeyY() {
return lowestSolidKeyY;
}
public int getCenterX() {
return centerX;
}
public int getCenterY() {
return centerY;
}
public int getCenterZ() {
return centerZ;
}
public int getTallestKx() {
return tallestKx;
}
public int getTallestKz() {
return tallestKz;
}
public Set<Long> footprintXZ() {
return footprintXZ;
}
}
@@ -114,13 +114,13 @@ public class IrisFloatingChildBiomes implements IRare {
@MinNumber(0)
@MaxNumber(2032)
@Desc("Minimum blocks above the parent biome surface where the island base can sit.")
private int minHeightAboveSurface = 60;
@Desc("Minimum absolute world Y where the island base can sit. Island altitude is independent of the parent biome's terrain height; altitudeStyle noise varies the base between min and max per column.")
private int minHeightAboveSurface = 160;
@MinNumber(0)
@MaxNumber(2032)
@Desc("Maximum blocks above the parent biome surface where the island base can sit.")
private int maxHeightAboveSurface = 110;
@Desc("Maximum absolute world Y where the island base can sit. Island altitude is independent of the parent biome's terrain height; altitudeStyle noise varies the base between min and max per column.")
private int maxHeightAboveSurface = 210;
@Desc("Optional absolute minimum world Y for the island base. When set, baseY is clamped upward so the tail bottom stays above this value.")
private Integer minAbsoluteY = null;