From 6df718e6ca6120a9085467c51f408255e0fcaa9f Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 22 Apr 2026 09:42:45 -0400 Subject: [PATCH] floatt --- .../art/arcane/iris/core/nms/INMSBinding.java | 5 - .../engine/decorator/FloatingDecorator.java | 134 +++++++++ .../IrisFloatingSurfaceDecorator.java | 33 --- .../mantle/components/IslandObjectPlacer.java | 213 ++++++++++++++ .../MantleFloatingObjectComponent.java | 266 ++++++++++++++++-- .../IrisFloatingChildBiomeModifier.java | 112 ++++++-- .../engine/object/FloatingIslandSample.java | 24 +- .../object/FloatingObjectFootprint.java | 242 ++++++++++++++++ .../object/IrisFloatingChildBiomes.java | 8 +- .../iris/core/nms/v1_21_R7/NMSBinding.java | 105 ------- 10 files changed, 926 insertions(+), 216 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/FloatingObjectFootprint.java diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index e4618d704..2289017c1 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -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> getBlockProperties(); - default Map extractVanillaDatapack() { - return Map.of(); - } - private void validateDimensionTypes(WorldCreator c) { if (c.generator() instanceof PlatformChunkGenerator gen && missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) { diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java new file mode 100644 index 000000000..f54c553c1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java @@ -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 . + */ + +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 data, RNG rng) { + long gSeed = engine.getSeedManager().getDecorator() + SEED_SALT - (part.ordinal() * PART_SALT); + RNG gRNG = new RNG(gSeed); + KList 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 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 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; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java deleted file mode 100644 index a2d257adc..000000000 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java +++ /dev/null @@ -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 . - */ - -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; - } -} 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 new file mode 100644 index 000000000..159af8ad1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java @@ -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 . + */ + +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 void setData(int xx, int yy, int zz, T data) { + if (shouldSkipAirColumn(xx, yy, zz)) { + return; + } + wrapped.setData(xx, yy, zz, data); + } + + @Override + public @Nullable T getData(int xx, int yy, int zz, Class t) { + return wrapped.getData(xx, yy, zz, t); + } + + @Override + public Engine getEngine() { + return wrapped.getEngine(); + } +} 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 04d08f439..50fe179e1 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 @@ -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 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 ? "" : 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 ? "" : 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 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 columns, int minX, int minZ, IrisFloatingChildBiomes entry) { + private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList columns, int minX, int minZ, IrisFloatingChildBiomes entry, IrisBiome target) { if (placement == null || columns.isEmpty()) { return; } - KList interior = interiorColumns(samples, columns); - KList pickPool = interior.isEmpty() ? columns : interior; int density = placement.getDensity(rng, minX, minZ, data); double perAttempt = placement.getChance(); + KList 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 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 interiorColumns(FloatingIslandSample[] samples, KList columns) { KList 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 objectKeys = new java.util.HashSet<>(); try { + IrisData data = getData(); for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) { KList 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 placements, java.util.Set out) { + if (placements == null) return; + for (IrisObjectPlacement p : placements) { + 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 db6e8a1c3..6534cfde3 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 @@ -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 { 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 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 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 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 ? " " : topAnchorY.toString()) + " topFloors:" + (topFloors.length() == 0 ? " " : topFloors.toString())); } @@ -83,13 +108,44 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier= 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 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 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); } 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 767cde743..a2d57c31a 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 @@ -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 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 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) { 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 new file mode 100644 index 000000000..ed9ea46ef --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingObjectFootprint.java @@ -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 . + */ + +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 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 footprintXZ; + + private FloatingObjectFootprint(int lowestSolidKeyY, int centerX, int centerY, int centerZ, int tallestKx, int tallestKz, Set 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 xzSet = new HashSet<>(); + Map 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 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; + int tallestLow = Integer.MAX_VALUE; + for (Map.Entry 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 footprintXZ() { + return footprintXZ; + } +} 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 0ddc0e276..1ba6fa957 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 @@ -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; diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java index a6a18347b..5ab3a2d49 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java @@ -60,8 +60,6 @@ import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.levelgen.FlatLevelSource; import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; -import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; -import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.ServerLevelData; import org.bukkit.*; @@ -86,28 +84,15 @@ import org.jetbrains.annotations.NotNull; import java.awt.Color; import java.io.File; -import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.net.URI; -import java.net.URL; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.*; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; public class NMSBinding implements INMSBinding { private final KMap baseBiomeCache = new KMap<>(); @@ -788,96 +773,6 @@ public class NMSBinding implements INMSBinding { return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName); } - private static final Pattern VANILLA_DATAPACK_ENTRY = Pattern.compile( - "^data/minecraft/(?:worldgen/(?:structure|structure_set|template_pool|processor_list|configured_feature|placed_feature)/.+\\.json" - + "|structures?/.+\\.nbt" - + "|tags/worldgen/biome/has_structure/.+\\.json)$" - ); - - @Override - public Map extractVanillaDatapack() { - Map entries = new LinkedHashMap<>(); - File serverJar = resolveServerJar(); - if (serverJar == null) { - Iris.error("Unable to locate server JAR for vanilla datapack extraction."); - return entries; - } - - Iris.info("Extracting vanilla datapack from " + serverJar.getName() + "..."); - try (ZipFile zip = new ZipFile(serverJar)) { - Enumeration zipEntries = zip.entries(); - while (zipEntries.hasMoreElements()) { - ZipEntry entry = zipEntries.nextElement(); - if (entry.isDirectory()) { - continue; - } - String name = entry.getName(); - if (!VANILLA_DATAPACK_ENTRY.matcher(name).matches()) { - continue; - } - String datapackPath = normalizeStructurePath(name); - try (InputStream is = zip.getInputStream(entry)) { - entries.put(datapackPath, is.readAllBytes()); - } - } - } catch (IOException e) { - Iris.error("Failed to read vanilla datapack entries from " + serverJar.getName()); - e.printStackTrace(); - } - - Iris.info("Extracted " + entries.size() + " vanilla datapack entries."); - return entries; - } - - private static String normalizeStructurePath(String path) { - return path.replace("data/minecraft/structures/", "data/minecraft/structure/"); - } - - private static File resolveServerJar() { - try { - URL url = MinecraftServer.class.getProtectionDomain().getCodeSource().getLocation(); - if (url != null) { - File file = Path.of(url.toURI()).toFile(); - if (file.isFile() && file.getName().endsWith(".jar")) { - return file; - } - if (file.isDirectory()) { - return resolveServerJarFromDirectory(file); - } - } - } catch (Exception ignored) { - } - - String classpath = System.getProperty("java.class.path", ""); - for (String entry : classpath.split(File.pathSeparator)) { - File file = new File(entry); - if (file.isFile() && file.getName().endsWith(".jar") && containsVanillaData(file)) { - return file; - } - } - return null; - } - - private static File resolveServerJarFromDirectory(File dir) { - File[] jars = dir.listFiles((d, name) -> name.endsWith(".jar")); - if (jars == null) return null; - for (File jar : jars) { - if (containsVanillaData(jar)) { - return jar; - } - } - return null; - } - - private static boolean containsVanillaData(File jar) { - try (ZipFile zip = new ZipFile(jar)) { - return zip.getEntry("data/minecraft/worldgen/structure_set/villages.json") != null - || zip.getEntry("data/minecraft/worldgen/structure_set/village_plains.json") != null; - } catch (Exception e) { - return false; - } - } - @Override public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) { if (!(registryAccess instanceof RegistryAccess access)) {