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