From 3d128b70a78fef9e4152eabc6c3aefdbeea61baf Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 22 Apr 2026 19:23:50 -0400 Subject: [PATCH] RedoDeco --- core/plugins/Iris/cache/instance | 2 +- .../iris/core/pregenerator/PregenTask.java | 42 ++ .../methods/AsyncPregenMethod.java | 6 +- .../runtime/WorldRuntimeControlService.java | 9 +- .../iris/engine/decorator/DecoratorCore.java | 430 ++++++++++++++++++ .../engine/decorator/FloatingDecorator.java | 97 +--- .../decorator/IrisCeilingDecorator.java | 93 +--- .../engine/decorator/IrisEngineDecorator.java | 80 +--- .../decorator/IrisSeaFloorDecorator.java | 79 ++-- .../decorator/IrisSeaSurfaceDecorator.java | 66 +-- .../decorator/IrisShoreLineDecorator.java | 87 ++-- .../decorator/IrisSurfaceDecorator.java | 156 ++----- .../IrisFloatingChildBiomeModifier.java | 8 +- .../arcane/iris/engine/object/IrisBiome.java | 21 +- .../iris/engine/object/IrisDecorator.java | 61 ++- .../engine/decorator/DecoratorCoreTest.java | 129 ++++++ ...04-22-floating-island-shard-domain-warp.md | 224 +++++++++ 17 files changed, 1102 insertions(+), 488 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java create mode 100644 core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java create mode 100644 docs/plans/2026-04-22-floating-island-shard-domain-warp.md diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance index 4b1cc96e1..4b1356fce 100644 --- a/core/plugins/Iris/cache/instance +++ b/core/plugins/Iris/cache/instance @@ -1 +1 @@ -699705819 \ No newline at end of file +-1935789196 \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java index 1a29140f8..1727d7aa7 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java @@ -27,7 +27,9 @@ import art.arcane.volmlib.util.math.Spiraler; import lombok.Builder; import lombok.Data; +import java.util.ArrayList; import java.util.Comparator; +import java.util.List; @Builder @Data @@ -117,10 +119,50 @@ public class PregenTask { })); } + @FunctionalInterface + public interface InterleavedChunkConsumer { + boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion); + } + public void iterateAllChunks(Spiraled s) { iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s))); } + public void iterateAllChunksInterleaved(InterleavedChunkConsumer consumer) { + List regions = new ArrayList<>(); + iterateRegions((rX, rZ) -> regions.add(new int[]{rX, rZ})); + + List> regionChunks = new ArrayList<>(); + for (int[] region : regions) { + List chunks = new ArrayList<>(); + iterateChunks(region[0], region[1], (cx, cz) -> chunks.add(new int[]{region[0], region[1], cx, cz})); + if (!chunks.isEmpty()) { + regionChunks.add(chunks); + } + } + + int[] indices = new int[regionChunks.size()]; + boolean anyRemaining = true; + while (anyRemaining) { + anyRemaining = false; + for (int r = 0; r < regionChunks.size(); r++) { + List chunks = regionChunks.get(r); + int idx = indices[r]; + if (idx >= chunks.size()) { + continue; + } + anyRemaining = true; + int[] entry = chunks.get(idx); + boolean first = idx == 0; + boolean last = idx == chunks.size() - 1; + indices[r]++; + if (!consumer.on(entry[0], entry[1], entry[2], entry[3], first, last)) { + return; + } + } + } + } + private class Bounds { private Bound chunk = null; private Bound region = null; diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 052274132..0b0eda8bb 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -355,13 +355,13 @@ public class AsyncPregenMethod implements PregeneratorMethod { static int computePaperLikeRecommendedCap(int workerThreads) { int normalizedWorkers = Math.max(1, workerThreads); - int recommendedCap = normalizedWorkers * 4; + int recommendedCap = normalizedWorkers * 2; if (recommendedCap < 8) { return 8; } - if (recommendedCap > 128) { - return 128; + if (recommendedCap > 96) { + return 96; } return recommendedCap; diff --git a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java index 1fe82a6b2..9e0d5fc11 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java @@ -328,7 +328,14 @@ public final class WorldRuntimeControlService { int[] scanOrder = new int[maxY - minY + 1]; int index = 0; - for (int y = maxY; y >= minY; y--) { + int runtimeSurface = world.getHighestBlockYAt((int) source.getX(), (int) source.getZ()); + int startY = Math.min(maxY, runtimeSurface + 1); + + for (int y = startY; y >= minY; y--) { + scanOrder[index++] = y; + } + + for (int y = startY + 1; y <= maxY; y++) { scanOrder[index++] = y; } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java new file mode 100644 index 000000000..bce313c10 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java @@ -0,0 +1,430 @@ +/* + * 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.core.loader.IrisData; +import art.arcane.iris.engine.mantle.EngineMantle; +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.common.data.B; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockSupport; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.MultipleFacing; +import org.bukkit.block.data.type.PointedDripstone; + +final class DecoratorCore { + + private static final long SEED_OFFSET = 29356788L; + private static final long PART_FACTOR = 10439677L; + + static final ThreadLocal SCRATCH_OPTS = ThreadLocal.withInitial(PlaceOpts::new); + + static final class PlaceOpts { + boolean caveSkipFluid; + boolean underwater; + int fluidHeight; + + void reset() { + caveSkipFluid = false; + underwater = false; + fluidHeight = 0; + } + } + + static long partSeed(long baseSeed, int partOrdinal) { + return baseSeed + SEED_OFFSET - (partOrdinal * PART_FACTOR); + } + + static long partSeed(long baseSeed, IrisDecorationPart part) { + return partSeed(baseSeed, part.ordinal()); + } + + static IrisDecorator pickDecorator(IrisBiome biome, IrisDecorationPart part, RNG gRNG, + RNG colRng, IrisData data, double realX, double realZ) { + IrisDecorator[] bucket = biome.getDecoratorBucket(part); + if (bucket.length == 0) { + return null; + } + + IrisDecorator picked = null; + int count = 0; + + for (IrisDecorator d : bucket) { + try { + if (d.passesChanceGate(gRNG, realX, realZ, data)) { + count++; + if (count == 1 || colRng.nextInt(count) == 0) { + picked = d; + } + } + } catch (Throwable e) { + Iris.reportError(e); + } + } + + return picked; + } + + static void placeSingleUp(IrisDecorator decorator, int x, int z, + int realX, int height, int realZ, Hunk data, + RNG rng, IrisData irisData, boolean caveSkipFluid, EngineMantle mantle) { + BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + return; + } + + if (bd instanceof Bisected) { + BlockData top = bd.clone(); + ((Bisected) top).setHalf(Bisected.Half.TOP); + try { + if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) { + data.set(x, height + 2, z, top); + } + } catch (Throwable e) { + Iris.reportError(e); + } + bd = bd.clone(); + ((Bisected) bd).setHalf(Bisected.Half.BOTTOM); + } + + if (B.isAir(data.get(x, height + 1, z))) { + data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle)); + } + } + + static void placeSurfaceSingle(IrisDecorator decorator, + int x, int z, int realX, int height, int realZ, + Hunk data, RNG rng, IrisData irisData, + boolean underwater, boolean caveSkipFluid, EngineMantle mantle) { + BlockData bdx = data.get(x, height, z); + BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ); + + if (!underwater && !canGoOn(bd, bdx) + && !decorator.isForcePlace() && decorator.getForceBlock() == null) { + return; + } + + if (decorator.getForceBlock() != null) { + if (caveSkipFluid && B.isFluid(bdx)) { + return; + } + data.set(x, height, z, fixFacesForHunk( + decorator.getForceBlock().getBlockData(irisData), data, x, z, realX, height, realZ, mantle)); + return; + } + + if (!decorator.isForcePlace()) { + if (decorator.getWhitelist() != null + && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(irisData).equals(bdx))) { + return; + } + if (decorator.getBlacklist() != null + && decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(irisData).equals(bdx))) { + return; + } + } + + if (bd instanceof Bisected) { + BlockData top = bd.clone(); + ((Bisected) top).setHalf(Bisected.Half.TOP); + try { + if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) { + data.set(x, height + 2, z, top); + } + } catch (Throwable e) { + Iris.reportError(e); + } + bd = bd.clone(); + ((Bisected) bd).setHalf(Bisected.Half.BOTTOM); + } + + if (B.isAir(data.get(x, height + 1, z))) { + data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle)); + } + } + + static void placeSingleAt(IrisDecorator decorator, int x, int z, + int realX, int height, int realZ, Hunk data, + RNG rng, IrisData irisData, boolean applyFixFaces, EngineMantle mantle) { + BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + return; + } + if (applyFixFaces) { + bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle); + } + data.set(x, height, z, bd); + } + + static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ, + int height, int max, Hunk data, + RNG rng, IrisData irisData, PlaceOpts opts) { + int effectiveMax = max; + if (opts.underwater && height < opts.fluidHeight) { + effectiveMax = opts.fluidHeight; + } + + int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax); + + if (stack == 1) { + if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } + data.set(x, height, z, decorator.pickBlockDataTop(rng, irisData, realX, realZ)); + return; + } + + BlockData bdx = data.get(x, height, z); + + for (int i = 0; i < stack; i++) { + int h = height + i; + double threshold = ((double) i) / (stack - 1); + BlockData bd = threshold >= decorator.getTopThreshold() + ? decorator.pickBlockDataTop(rng, irisData, realX, realZ) + : decorator.pickBlockData(rng, irisData, realX, realZ); + + if (bd == null) { + break; + } + + if (i == 0 && !opts.underwater && !canGoOn(bd, bdx)) { + break; + } + + if (opts.underwater && height + 1 + i > opts.fluidHeight) { + break; + } + + if (opts.caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) { + break; + } + + if (bd instanceof PointedDripstone) { + bd = dripstoneBlock(stack, i, BlockFace.UP); + } + + data.set(x, height + 1 + i, z, bd); + } + } + + static void placeStackDown(IrisDecorator decorator, int x, int z, int realX, int realZ, + int height, int minHeight, Hunk data, + RNG rng, IrisData irisData, int max, PlaceOpts opts, EngineMantle mantle) { + int stack = computeStack(decorator, rng, realX, realZ, irisData, max); + + if (stack == 1) { + if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } + data.set(x, height, z, fixFacesForHunk( + decorator.pickBlockDataTop(rng, irisData, realX, realZ), + data, x, z, realX, height, realZ, mantle)); + return; + } + + for (int i = 0; i < stack; i++) { + int h = height - i; + if (h < minHeight) { + continue; + } + + double threshold = ((double) i) / (double) (stack - 1); + BlockData bd = threshold >= decorator.getTopThreshold() + ? decorator.pickBlockDataTop(rng, irisData, realX, realZ) + : decorator.pickBlockData(rng, irisData, realX, realZ); + + if (bd instanceof PointedDripstone) { + bd = dripstoneBlock(stack, i, BlockFace.DOWN); + } + + if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) { + break; + } + data.set(x, h, z, fixFacesForHunk(bd, data, x, z, realX, h, realZ, mantle)); + } + } + + static void placeFloatingSimple(IrisDecorator decorator, + int xf, int zf, int realX, int realZ, + int height, int max, Hunk data, + RNG rng, IrisData irisData) { + BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + return; + } + + 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); + } + } + + static int placeFloatingStacked(IrisDecorator decorator, + int xf, int zf, int realX, int realZ, + int height, int max, Hunk data, + RNG rng, IrisData irisData) { + int stack = decorator.getHeight(rng, realX, realZ, irisData); + 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.pickBlockDataTop(rng, irisData, realX, realZ) + : decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + break; + } + data.set(xf, h, zf, bd); + placed++; + } + return placed; + } + + static BlockData fixFacesForHunk(BlockData b, Hunk hunk, int rX, int rZ, + int x, int y, int z, EngineMantle mantle) { + if (!B.isVineBlock(b)) { + return b; + } + MultipleFacing data = (MultipleFacing) b.clone(); + data.getFaces().forEach(f -> data.setFace(f, false)); + + boolean found = false; + for (BlockFace f : BlockFace.values()) { + if (!f.isCartesian()) { + continue; + } + int yy = y + f.getModY(); + + BlockData r = null; + if (mantle != null) { + r = mantle.getMantle().get(x + f.getModX(), yy, z + f.getModZ(), BlockData.class); + } + if (r == null) { + r = EngineMantle.AIR; + } + if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { + found = true; + data.setFace(f, true); + continue; + } + + int xx = rX + f.getModX(); + int zz = rZ + f.getModZ(); + if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) { + continue; + } + + r = hunk.get(xx, yy, zz); + if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { + found = true; + data.setFace(f, true); + } + } + if (!found) { + data.setFace(BlockFace.DOWN, true); + } + return data; + } + + static boolean canGoOn(BlockData decorator, BlockData surface) { + return surface.isFaceSturdy(BlockFace.UP, BlockSupport.FULL); + } + + private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ, + IrisData irisData, int max) { + int stack = decorator.getHeight(rng, realX, realZ, irisData); + if (decorator.isScaleStack()) { + stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); + } else { + stack = Math.min(max, stack); + } + return stack; + } + + // Lazily populated on first dripstone decoration — avoids Bukkit API at class-load time. + // Index: 0=TIP, 1=FRUSTUM, 2=BASE. Race on init is benign (only allocation cost, not correctness). + private static volatile BlockData[] dripstoneUp; + private static volatile BlockData[] dripstoneDown; + + private static BlockData[] buildDripstoneArr(BlockFace direction) { + PointedDripstone.Thickness[] order = { + PointedDripstone.Thickness.TIP, + PointedDripstone.Thickness.FRUSTUM, + PointedDripstone.Thickness.BASE + }; + BlockData[] arr = new BlockData[3]; + for (int k = 0; k < 3; k++) { + BlockData bd = Material.POINTED_DRIPSTONE.createBlockData(); + ((PointedDripstone) bd).setThickness(order[k]); + ((PointedDripstone) bd).setVerticalDirection(direction); + arr[k] = bd; + } + return arr; + } + + private static BlockData dripstoneBlock(int stack, int i, BlockFace direction) { + int thIdx; + if (i == stack - 1) { + thIdx = 0; + } else if (i == stack - 2) { + thIdx = 1; + } else { + thIdx = 2; + } + if (direction == BlockFace.UP) { + if (dripstoneUp == null) { + dripstoneUp = buildDripstoneArr(BlockFace.UP); + } + return dripstoneUp[thIdx]; + } + if (dripstoneDown == null) { + dripstoneDown = buildDripstoneArr(BlockFace.DOWN); + } + return dripstoneDown[thIdx]; + } +} 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 index f54c553c1..c66372ac8 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java @@ -18,117 +18,38 @@ 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<>(); + int height, int max, Hunk data, RNG rng, + Runnable candidatesNullCallback) { + RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part)); + IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ); - 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(); + if (decorator == null) { + candidatesNullCallback.run(); 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; + DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData()); + return max > 1 ? 1 : 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; + return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData()); } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java index 728c76d89..39fd497c4 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java @@ -24,89 +24,42 @@ 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.common.data.B; -import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.math.RNG; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.PointedDripstone; public class IrisCeilingDecorator extends IrisEngineDecorator { + private final RNG partRNG; + public IrisCeilingDecorator(Engine engine) { super(engine, "Ceiling", IrisDecorationPart.CEILING); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.CEILING)); } @BlockCoordinates @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); - if (decorator != null) { - if (!decorator.isStacking()) { - if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { - return; - } - data.set(x, height, z, fixFaces(decorator.getBlockData100(biome, rng, realX, height, realZ, getData()), data, x, z, realX, height, realZ)); - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); - } else { - stack = Math.min(max, stack); - } - - if (stack == 1) { - if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { - return; - } - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height - i; - if (h < getEngine().getMinHeight()) { - continue; - } - - double threshold = (((double) i) / (double) (stack - 1)); - - BlockData bd = threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); - - if (bd instanceof PointedDripstone) { - PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE; - - if (stack == 2) { - th = PointedDripstone.Thickness.FRUSTUM; - - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } - } else { - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } else if (i == stack - 2) { - th = PointedDripstone.Thickness.FRUSTUM; - } - } - - - bd = Material.POINTED_DRIPSTONE.createBlockData(); - ((PointedDripstone) bd).setThickness(th); - ((PointedDripstone) bd).setVerticalDirection(BlockFace.DOWN); - } - - if (caveSkipFluid && B.isFluid(data.get(x, h, z))) { - break; - } - data.set(x, h, z, bd); - } - } + if (decorator == null) { + return; } + + if (!decorator.isStacking()) { + if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } + DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle()); + return; + } + + DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get(); + opts.reset(); + opts.caveSkipFluid = caveSkipFluid; + DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, getEngine().getMinHeight(), data, rng, getData(), max, opts, getEngine().getMantle()); } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java index 3c6636a15..89a2c36b6 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java @@ -18,102 +18,28 @@ package art.arcane.iris.engine.decorator; -import art.arcane.iris.Iris; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedComponent; import art.arcane.iris.engine.framework.EngineDecorator; -import art.arcane.iris.engine.mantle.EngineMantle; -import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDecorationPart; -import art.arcane.iris.engine.object.IrisDecorator; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.common.data.B; -import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.math.RNG; import lombok.Getter; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockSupport; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.MultipleFacing; public abstract class IrisEngineDecorator extends EngineAssignedComponent implements EngineDecorator { @Getter private final IrisDecorationPart part; - private final long seed; - private final long modX, modZ; public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) { super(engine, name + " Decorator"); this.part = part; - this.seed = getSeed() + 29356788 - (part.ordinal() * 10439677L); - this.modX = 29356788 ^ (part.ordinal() + 6); - this.modZ = 10439677 ^ (part.ordinal() + 1); } @BlockCoordinates protected RNG getRNG(int x, int z) { + long seed = DecoratorCore.partSeed(getSeed(), part); + long modX = 29356788L ^ (part.ordinal() + 6); + long modZ = 10439677L ^ (part.ordinal() + 1); return new RNG(x * modX + z * modZ + seed); } - - protected IrisDecorator getDecorator(RNG rng, IrisBiome biome, double realX, double realZ) { - KList v = new KList<>(); - - RNG gRNG = new RNG(seed); - for (IrisDecorator i : biome.getDecorators()) { - try { - if (i.getPartOf().equals(part) && i.getBlockData(biome, gRNG, realX, realZ, getData()) != null) { - v.add(i); - } - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("PART OF: " + biome.getLoadFile().getAbsolutePath() + " HAS AN INVALID DECORATOR near 'partOf'!!!"); - } - } - - if (v.isNotEmpty()) { - return v.get(rng.nextInt(v.size())); - } - - return null; - } - - protected BlockData fixFaces(BlockData b, Hunk hunk, int rX, int rZ, int x, int y, int z) { - if (B.isVineBlock(b)) { - MultipleFacing data = (MultipleFacing) b.clone(); - data.getFaces().forEach(f -> data.setFace(f, false)); - - boolean found = false; - for (BlockFace f : BlockFace.values()) { - if (!f.isCartesian()) - continue; - int yy = y + f.getModY(); - - BlockData r = getEngine().getMantle().getMantle().get(x + f.getModX(), yy, z + f.getModZ(), BlockData.class); - if (r == null) { - r = EngineMantle.AIR; - } - if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { - found = true; - data.setFace(f, true); - continue; - } - - int xx = rX + f.getModX(); - int zz = rZ + f.getModZ(); - if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) - continue; - - r = hunk.get(xx, yy, zz); - if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { - found = true; - data.setFace(f, true); - } - } - if (!found) - data.setFace(BlockFace.DOWN, true); - return data; - } - return b; - } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java index cedecbab8..c929e162b 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java @@ -22,56 +22,63 @@ 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.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.math.RNG; import org.bukkit.block.data.BlockData; public class IrisSeaFloorDecorator extends IrisEngineDecorator { + private final RNG partRNG; + public IrisSeaFloorDecorator(Engine engine) { super(engine, "Sea Floor", IrisDecorationPart.SEA_FLOOR); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_FLOOR)); } @BlockCoordinates @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); - if (decorator != null) { - if (!decorator.isStacking()) { - if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() - && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { - return; - } - if (height >= 0 || height < getEngine().getHeight()) { - data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); - } - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } else stack = Math.min(stack, max - height); - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - if (h > max || h > getEngine().getHeight()) { - continue; - } - - double threshold = ((double) i) / (stack - 1); - data.set(x, h, z, threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); - } - } + if (decorator == null) { + return; } + if (!decorator.isStacking()) { + if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() + && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { + return; + } + if (height >= 0 || height < getEngine().getHeight()) { + data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); + } + return; + } + + int stack = decorator.getHeight(rng, realX, realZ, getData()); + if (decorator.isScaleStack()) { + stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); + } else { + stack = Math.min(stack, max - height); + } + + if (stack == 1) { + data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); + return; + } + + int engineHeight = getEngine().getHeight(); + for (int i = 0; i < stack; i++) { + int h = height + i; + if (h > max || h > engineHeight) { + continue; + } + double threshold = ((double) i) / (stack - 1); + data.set(x, h, z, threshold >= decorator.getTopThreshold() + ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); + } } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java index d12713609..c82145768 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java @@ -22,51 +22,57 @@ 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.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.math.RNG; import org.bukkit.block.data.BlockData; public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { + private final RNG partRNG; + public IrisSeaSurfaceDecorator(Engine engine) { super(engine, "Sea Surface", IrisDecorationPart.SEA_SURFACE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_SURFACE)); } @BlockCoordinates @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); - if (decorator != null) { - if (!decorator.isStacking()) { - if (height >= 0 || height < getEngine().getHeight()) { - data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); - } - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } + if (decorator == null) { + return; + } - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - if (h >= max || h >= getEngine().getHeight()) { - continue; - } - - double threshold = ((double) i) / (stack - 1); - data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); - } + if (!decorator.isStacking()) { + if (height >= 0 || height < getEngine().getHeight()) { + data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); } + return; + } + + int stack = decorator.getHeight(rng, realX, realZ, getData()); + if (decorator.isScaleStack()) { + stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); + } + + if (stack == 1) { + data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); + return; + } + + int engineHeight = getEngine().getHeight(); + for (int i = 0; i < stack; i++) { + int h = height + i; + if (h >= max || h >= engineHeight) { + continue; + } + double threshold = ((double) i) / (stack - 1); + data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() + ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); } } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java index 8566927c5..38e6fdeea 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java @@ -22,59 +22,72 @@ 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.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.math.RNG; import org.bukkit.block.data.BlockData; public class IrisShoreLineDecorator extends IrisEngineDecorator { + private final RNG partRNG; + public IrisShoreLineDecorator(Engine engine) { super(engine, "Shore Line", IrisDecorationPart.SHORE_LINE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SHORE_LINE)); } @BlockCoordinates @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + if (height != getDimension().getFluidHeight()) { + return; + } - if (height == getDimension().getFluidHeight()) { - if (Math.round(getComplex().getHeightStream().get(realX1, realZ)) < getComplex().getFluidHeight() || - Math.round(getComplex().getHeightStream().get(realX_1, realZ)) < getComplex().getFluidHeight() || - Math.round(getComplex().getHeightStream().get(realX, realZ1)) < getComplex().getFluidHeight() || - Math.round(getComplex().getHeightStream().get(realX, realZ_1)) < getComplex().getFluidHeight() - ) { - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); + double complexFluidHeight = getComplex().getFluidHeight(); + ProceduralStream heightStream = getComplex().getHeightStream(); + if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight + && Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight + && Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight + && Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) { + return; + } - if (decorator != null) { - if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() - && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { - return; - } + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); - if (!decorator.isStacking()) { - data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } else stack = Math.min(max - height, stack); + if (decorator == null) { + return; + } - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } + if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() + && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { + return; + } - for (int i = 0; i < stack; i++) { - int h = height + i; - double threshold = ((double) i) / (stack - 1); - data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); - } - } - } - } + if (!decorator.isStacking()) { + data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); + return; + } + + int stack = decorator.getHeight(rng, realX, realZ, getData()); + if (decorator.isScaleStack()) { + stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); + } else { + stack = Math.min(max - height, stack); + } + + if (stack == 1) { + data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); + return; + } + + for (int i = 0; i < stack; i++) { + int h = height + i; + double threshold = ((double) i) / (stack - 1); + data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() + ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); } } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java index 3f187b72d..abe2a098b 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java @@ -18,29 +18,27 @@ package art.arcane.iris.engine.decorator; -import art.arcane.iris.Iris; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.InferredType; 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.common.data.B; -import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.math.RNG; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.Bisected; import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.PointedDripstone; public class IrisSurfaceDecorator extends IrisEngineDecorator { + private final RNG partRNG; + public IrisSurfaceDecorator(Engine engine) { super(engine, "Surface", IrisDecorationPart.NONE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE)); } protected IrisSurfaceDecorator(Engine engine, String name) { super(engine, name, IrisDecorationPart.NONE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE)); } protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) { @@ -52,133 +50,33 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { @BlockCoordinates @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - if (biome.getInferredType().equals(InferredType.SHORE) && height < getDimension().getFluidHeight()) { + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + int fluidHeight = getDimension().getFluidHeight(); + if (biome.getInferredType().equals(InferredType.SHORE) && height < fluidHeight) { return; } - BlockData bd, bdx; - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); - bdx = data.get(x, height, z); - boolean underwater = height < getDimension().getFluidHeight() && biome.getInferredType() != InferredType.CAVE; + boolean underwater = height < fluidHeight && biome.getInferredType() != InferredType.CAVE; boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); - if (decorator != null) { - if (!isSlopeValid(decorator, realX, realZ)) { - return; - } - - if (!decorator.isStacking()) { - bd = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); - - if (!underwater) { - if (!canGoOn(bd, bdx) && (!decorator.isForcePlace() && decorator.getForceBlock() == null)) { - return; - } - } - - if (decorator.getForceBlock() != null) { - if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { - return; - } - data.set(x, height, z, fixFaces(decorator.getForceBlock().getBlockData(getData()), data, x, z, realX, height, realZ)); - } else if (!decorator.isForcePlace()) { - if (decorator.getWhitelist() != null && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(getData()).equals(bdx))) { - return; - } - if (decorator.getBlacklist() != null && decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(getData()).equals(bdx))) { - return; - } - } - - if (bd instanceof Bisected) { - bd = bd.clone(); - ((Bisected) bd).setHalf(Bisected.Half.TOP); - try { - if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) { - data.set(x, height + 2, z, bd); - } - } catch (Throwable e) { - Iris.reportError(e); - } - bd = bd.clone(); - ((Bisected) bd).setHalf(Bisected.Half.BOTTOM); - } - - if (B.isAir(data.get(x, height + 1, z))) { - data.set(x, height + 1, z, fixFaces(bd, data, x, z, realX, height + 1, realZ)); - } - } else { - if (height < getDimension().getFluidHeight()) { - max = getDimension().getFluidHeight(); - } - - int stack = decorator.getHeight(rng, realX, realZ, getData()); - - if (decorator.isScaleStack()) { - stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); - } else { - stack = Math.min(max, stack); - } - - if (stack == 1) { - if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { - return; - } - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - double threshold = ((double) i) / (stack - 1); - bd = threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); - - if (bd == null) { - break; - } - - if (i == 0 && !underwater && !canGoOn(bd, bdx)) { - break; - } - - if (underwater && height + 1 + i > getDimension().getFluidHeight()) { - break; - } - - if (caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) { - break; - } - - if (bd instanceof PointedDripstone) { - PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE; - - if (stack == 2) { - th = PointedDripstone.Thickness.FRUSTUM; - - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } - } else { - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } else if (i == stack - 2) { - th = PointedDripstone.Thickness.FRUSTUM; - } - } - - - bd = Material.POINTED_DRIPSTONE.createBlockData(); - ((PointedDripstone) bd).setThickness(th); - ((PointedDripstone) bd).setVerticalDirection(BlockFace.UP); - } - - data.set(x, height + 1 + i, z, bd); - } - } + if (decorator == null || !isSlopeValid(decorator, realX, realZ)) { + return; } + + if (decorator.isStacking()) { + DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get(); + opts.reset(); + opts.underwater = underwater; + opts.fluidHeight = fluidHeight; + opts.caveSkipFluid = caveSkipFluid; + DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts); + return; + } + + DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ, + data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle()); } } 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 6534cfde3..e9cb864b9 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 @@ -59,8 +59,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier floorMatHisto = new java.util.concurrent.ConcurrentHashMap<>(); + private static final AtomicLong decCandidatesNull = new AtomicLong(); private static final AtomicLong lastReportMs = new AtomicLong(0L); private static final AtomicLong reportCycle = new AtomicLong(0L); + private static final Runnable INC_DEC_CANDIDATES_NULL = () -> decCandidatesNull.incrementAndGet(); private final RNG rng; private final EngineDecorator seaSurfaceDecorator; @@ -84,7 +86,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier buildDecoratorBuckets() { + EnumMap> staging = new EnumMap<>(IrisDecorationPart.class); + for (IrisDecorator d : decorators) { + staging.computeIfAbsent(d.getPartOf(), k -> new KList<>()).add(d); + } + EnumMap result = new EnumMap<>(IrisDecorationPart.class); + for (IrisDecorationPart part : IrisDecorationPart.values()) { + KList list = staging.get(part); + result.put(part, list == null ? EMPTY_BUCKET : list.toArray(EMPTY_BUCKET)); + } + return result; + } + public String getTypeName() { return "Biome"; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java index b642d58a2..95c6a7012 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java @@ -43,6 +43,8 @@ public class IrisDecorator { private final transient AtomicCache heightGenerator = new AtomicCache<>(); private final transient AtomicCache> blockData = new AtomicCache<>(); private final transient AtomicCache> blockDataTops = new AtomicCache<>(); + private final transient AtomicCache blockDataArray = new AtomicCache<>(); + private final transient AtomicCache blockDataTopsArray = new AtomicCache<>(); @Desc("The varience dispersion is used when multiple blocks are put in the palette. Scatter scrambles them, Wispy shows streak-looking varience") private IrisGeneratorStyle variance = NoiseStyle.STATIC.style(); @Desc("Forcefully place this decorant anywhere it is supposed to go even if it should not go on a specific surface block. For example, you could force tallgrass to place on top of stone by using this.") @@ -134,24 +136,23 @@ public class IrisDecorator { return palette; } - public BlockData getBlockData(IrisBiome b, RNG rng, double x, double z, IrisData data) { + public boolean passesChanceGate(RNG rng, double x, double z, IrisData data) { if (getBlockData(data).isEmpty()) { - Iris.warn("Empty Block Data for " + b.getName()); - return null; + return false; } - double xx = x / style.getZoom(); double zz = z / style.getZoom(); + return getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance; + } - if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { - if (getBlockData(data).size() == 1) { - return getBlockData(data).get(0); - } - - return getVarianceGenerator(rng, data).fit(getBlockData(data), z, x); //X and Z must be switched + public BlockData getBlockData(IrisBiome b, RNG rng, double x, double z, IrisData data) { + if (!passesChanceGate(rng, x, z, data)) { + return null; } - - return null; + if (getBlockData(data).size() == 1) { + return getBlockData(data).get(0); + } + return getVarianceGenerator(rng, data).fit(getBlockData(data), z, x); } public BlockData getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) { @@ -230,6 +231,42 @@ public class IrisDecorator { }); } + public BlockData[] getBlockDataArray(IrisData data) { + return blockDataArray.aquire(() -> { + KList list = getBlockData(data); + return list.toArray(new BlockData[0]); + }); + } + + public BlockData[] getBlockDataTopsArray(IrisData data) { + return blockDataTopsArray.aquire(() -> { + KList list = getBlockDataTops(data); + return list.toArray(new BlockData[0]); + }); + } + + public BlockData pickBlockData(RNG rng, IrisData data, double x, double z) { + BlockData[] arr = getBlockDataArray(data); + if (arr.length == 0) { + return null; + } + if (arr.length == 1) { + return arr[0]; + } + return arr[Math.abs((int) getVarianceGenerator(rng, data).fit(0, arr.length - 1, z, x))]; + } + + public BlockData pickBlockDataTop(RNG rng, IrisData data, double x, double z) { + BlockData[] arr = getBlockDataTopsArray(data); + if (arr.length == 0) { + return pickBlockData(rng, data, x, z); + } + if (arr.length == 1) { + return arr[0]; + } + return arr[Math.abs((int) getVarianceGenerator(rng, data).fit(0, arr.length - 1, z, x))]; + } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isStacking() { return getStackMax() > 1; diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java new file mode 100644 index 000000000..8cd00b202 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java @@ -0,0 +1,129 @@ +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +public class DecoratorCoreTest { + + @Test + public void partSeed_differsByPartOrdinal() { + long base = 123456789L; + long s0 = DecoratorCore.partSeed(base, 0); + long s1 = DecoratorCore.partSeed(base, 1); + long s2 = DecoratorCore.partSeed(base, 2); + assertNotEquals(s0, s1); + assertNotEquals(s1, s2); + } + + @Test + public void partSeed_isDeterministic() { + long base = 987654321L; + assertEquals(DecoratorCore.partSeed(base, 0), DecoratorCore.partSeed(base, 0)); + assertEquals(DecoratorCore.partSeed(base, 3), DecoratorCore.partSeed(base, 3)); + } + + @Test + public void placeOpts_resetClearsAllFields() { + DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get(); + opts.caveSkipFluid = true; + opts.underwater = true; + opts.fluidHeight = 99; + opts.reset(); + assertFalse(opts.caveSkipFluid); + assertFalse(opts.underwater); + assertEquals(0, opts.fluidHeight); + } + + @Test + public void scratchOpts_sameInstanceReturnedWithinThread() { + DecoratorCore.PlaceOpts a = DecoratorCore.SCRATCH_OPTS.get(); + DecoratorCore.PlaceOpts b = DecoratorCore.SCRATCH_OPTS.get(); + assertSame(a, b); + } + + @Test + public void pickDecorator_emptyBucket_returnsNull() { + IrisBiome biome = mock(IrisBiome.class); + IrisData data = mock(IrisData.class); + when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[0]); + + IrisDecorator result = DecoratorCore.pickDecorator( + biome, IrisDecorationPart.NONE, new RNG(1L), new RNG(2L), data, 0.0, 0.0); + assertNull(result); + } + + @Test + public void pickDecorator_nonePassChanceGate_returnsNull() { + IrisBiome biome = mock(IrisBiome.class); + IrisData data = mock(IrisData.class); + IrisDecorator d = mock(IrisDecorator.class); + when(d.passesChanceGate(any(RNG.class), anyDouble(), anyDouble(), any(IrisData.class))).thenReturn(false); + when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[]{d}); + + IrisDecorator result = DecoratorCore.pickDecorator( + biome, IrisDecorationPart.NONE, new RNG(1L), new RNG(2L), data, 0.0, 0.0); + assertNull(result); + } + + @Test + public void pickDecorator_singleCandidate_alwaysReturnsThat() { + IrisBiome biome = mock(IrisBiome.class); + IrisData data = mock(IrisData.class); + IrisDecorator d = mock(IrisDecorator.class); + when(d.passesChanceGate(any(RNG.class), anyDouble(), anyDouble(), any(IrisData.class))).thenReturn(true); + when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(new IrisDecorator[]{d}); + + RNG gRNG = new RNG(42L); + for (int t = 0; t < 50; t++) { + IrisDecorator result = DecoratorCore.pickDecorator( + biome, IrisDecorationPart.NONE, gRNG, new RNG(t * 13L + 7), data, 0.0, 0.0); + assertSame(d, result); + } + } + + @Test + public void pickDecorator_multiplePassingCandidates_selectsUniformly() { + IrisBiome biome = mock(IrisBiome.class); + IrisData data = mock(IrisData.class); + + int n = 4; + IrisDecorator[] decorators = new IrisDecorator[n]; + for (int i = 0; i < n; i++) { + IrisDecorator d = mock(IrisDecorator.class); + when(d.passesChanceGate(any(RNG.class), anyDouble(), anyDouble(), any(IrisData.class))).thenReturn(true); + decorators[i] = d; + } + when(biome.getDecoratorBucket(IrisDecorationPart.NONE)).thenReturn(decorators); + + RNG gRNG = new RNG(99L); + int[] counts = new int[n]; + int trials = 2000; + + for (int t = 0; t < trials; t++) { + IrisDecorator picked = DecoratorCore.pickDecorator( + biome, IrisDecorationPart.NONE, gRNG, new RNG(t * 31L + 3), data, 0.0, 0.0); + assertNotNull(picked); + for (int i = 0; i < n; i++) { + if (picked == decorators[i]) { + counts[i]++; + break; + } + } + } + + double expected = trials / (double) n; + for (int i = 0; i < n; i++) { + double deviation = Math.abs(counts[i] - expected) / expected; + assertTrue("Decorator " + i + " selected " + counts[i] + " times; expected ~" + (int) expected + + " (deviation " + String.format("%.0f%%", deviation * 100) + ")", deviation < 0.20); + } + } +} diff --git a/docs/plans/2026-04-22-floating-island-shard-domain-warp.md b/docs/plans/2026-04-22-floating-island-shard-domain-warp.md new file mode 100644 index 000000000..3c794d414 --- /dev/null +++ b/docs/plans/2026-04-22-floating-island-shard-domain-warp.md @@ -0,0 +1,224 @@ +# Floating Island Shard Domain Warp Implementation Plan + +Created: 2026-04-22 +Status: VERIFIED +Approved: Yes +Iterations: 0 +Worktree: No +Type: Feature + +## Summary + +**Goal:** In the Iris overworld pack (primary testbed: `roughplains.json`), make floating island shards read as less "mathy / sharply vertical" by enabling the already-wired per-layer domain warp on the two `floatingChildBiomes` entries. + +**Architecture:** `IrisFloatingChildBiomes` already defines `wallWarpStyle` + `wallWarpAmplitude` fields and exposes them through `getWallWarpCng(...)`. `FloatingIslandSample.sample(...)` already consumes them at `Iris/core/.../FloatingIslandSample.java:220-254` — for each Y layer of an island column, it offsets the footprint XZ sample by a signed 3D noise value before re-testing against `footprintThreshold`. This gives organic bulge/recession on the side walls without touching the tops (driven by `topShapeMode=BIOME`) or the bottoms (driven by `bottomStyle`). The current pack doesn't set `wallWarpStyle`, so `useWarp == false` and walls are a straight extrusion of the 2D footprint. + +**Tech Stack:** Iris pack JSON (no Java changes). + +--- + +## Scope + +### In Scope + +- Edit `[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json`. +- Add `wallWarpStyle` and `wallWarpAmplitude` to **both** entries in `floatingChildBiomes` (`mushroom/forest` and `tropical/wilds`). +- Chosen values (answers to Q1/Q2/Q3): `wallWarpStyle = {"style":"SIMPLEX","zoom":0.25}`, `wallWarpAmplitude = 10.0`. Identical on both entries. + +### Out of Scope + +- Java source changes inside `Iris/core` — the warp is already wired. No `FloatingIslandSample.java` or `IrisFloatingChildBiomes.java` edits. +- Changing the schema default for `wallWarpStyle` from `null` to a non-null value — would silently change behavior for every existing pack; user explicitly chose "pack only". +- Audit of other biomes — Grep confirms `floatingChildBiomes` only appears in `roughplains.json` across the overworld pack. +- Any change to `topShapeMode`, `bottomStyle`, `carveStyle`, or `footprintStyle` (tops and bottoms already look correct). +- Code quality improvements to the warp algorithm (independent X/Z seeds, multi-octave / iq-style warp-the-warp, vertical frequency multiplier) — user declined the "code-side improvement too" option. +- `TotalFixes.md` entry — repo policy (AGENTS.md) says append to existing sections; will be handled during implementation as part of the DoD if the repo conventions require it. + +--- + +## Approach + +**Chosen:** Pack-only enablement of the existing wall-warp feature. + +**Why:** All the plumbing already exists; the missing link is pack configuration. Zero code changes means zero risk of regressing any other floating-island behavior. The user owns this test pack, so they can tune values in follow-up without a code round-trip. + +**Alternatives considered:** + +- **Flip schema default of `wallWarpStyle` to a gentle SIMPLEX.** Rejected — silently changes behavior for every existing pack/user, and the point of having the feature opt-in is that visual style is a pack-authored choice. +- **Rewrite the warp in `FloatingIslandSample` for higher fidelity** (iq-style warp-the-warp, independent X/Z noise hashes, per-axis frequency scaling). Rejected — the current single-pass 3D warp already produces perturbed walls; the author of this ticket observed that the walls look "very sharply vertical" precisely because the feature is off in this pack, not because the existing implementation is inadequate. Revisit only if enabling the feature fails to soften the walls to the user's liking. +- **Per-entry tuning (mushroom gentler, tropical heavier).** Rejected — user selected "same warp for both". Revisit in a follow-up if the tropical shards (thicker at 112) still read as too vertical compared to mushroom (32). + +--- + +## Context for Implementer + +- **Target file (ONLY file to edit):** `/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json` +- **Do not touch:** any file under `Iris/core/src/...`. This is pack authoring, not code. +- **Schema reference:** `Iris/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java:170-176` documents `wallWarpStyle` and `wallWarpAmplitude` with accepted values, including the exact example the chosen config is based on (`{"style":"SIMPLEX","zoom":0.25}` for "gentle undulation"). Amplitude 10 falls between the labeled bands (`@Desc` line 175: "4..8 = gentle naturalization. 16+ = heavily meandering"), placing it in the lower-medium range — consistent with the user's stated Medium (~8-12 blocks) preference. +- **Field type:** `wallWarpAmplitude` is declared `double` (default `6.0`) at `IrisFloatingChildBiomes.java:176`. In JSON, write `10.0` (not `10`) to match the field's declared type and the existing default's literal form — Gson coerces either, but `10.0` is clearer for future pack authors comparing to the default. +- **Consumer reference:** `FloatingIslandSample.java:220-254` — confirms that when both `wallWarpStyle != null` and `wallWarpAmplitude > 0`, the per-layer XZ warp activates. No other branch needs to be exercised. +- **Field placement convention:** Existing entries order properties as `footprint* → picker* → altitude* → height → topShape → bottom* → maxThickness → carve* → inheritX → objectShrinkFactor`. Insert the two wall-warp fields **immediately after `maxThickness`** and **before `carveStyle`** — this keeps wall-shaping modifiers grouped and matches the @Desc-style progression from coarse (footprint) → fine (carve) detail. Preserve JSON formatting (4-space indent, trailing commas only where already present). +- **Gotchas:** + - JSON (not JSONC) — no comments, no trailing commas after the last key of an object. + - Iris CNG styles are case-sensitive — use `"SIMPLEX"` exactly, not `"simplex"`. + - The file currently has two entries in `floatingChildBiomes` — both must be updated. The whole point of the change is symmetric behavior on both. + +### Domain context + +- **What the warp does at runtime (to guide visual verification):** For each Y layer of a candidate island column, the generator computes `(sx, sz) = (wx, wz) + 10 × signed_noise3d(wx, wy, wz)` (X and Z offsets use decorrelated input coordinates), then re-samples the 2D footprint at `(sx, sz)` and re-tests against `footprintThreshold`. This means the horizontal silhouette of a shard can expand or contract by up to ~10 blocks per Y slice — the wall is no longer a vertical extrusion of the footprint. Zoom 0.25 makes the feature size of the warp ~25 blocks, which varies across the ~30-block CELLULAR shards enough to push each shard's silhouette into asymmetric bulges without high-frequency jitter. +- **Why tops/bottoms stay clean:** Tops are shaped by `topShapeMode = BIOME` (the target biome's own generators); bottoms by `bottomStyle` (CELLULAR zoom 0.3 / NOWHERE). The wall warp only perturbs the footprint test inside `sample()`, not the computed `topY` or `botY` — so the nicely-noised caps are untouched. + +--- + +## Runtime Environment + +This is a Minecraft server plugin — there is no UI to script and no HTTP surface. Verification requires running the Minecraft server with the edited pack and inspecting a generated chunk that contains a floating island. + +- **Pack hot-reload:** Iris supports `/iris studio close` + `/iris studio open` or `/iris reload ` on a live server; the user's workflow memory (`project_iris_floating_debug.md`) confirms this is the active testbed. +- **Deploy path:** The pack is already under `[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/` — editing the file **in place is the deploy step** (no build artifact to copy; per `AGENTS.md` never copy JARs into server plugin folders, but pack JSON edits are safe). +- **Health check / restart procedure:** Run `/iris studio close` in-game to close any open studio world, then `/iris create overworld` (or re-open the existing studio world) so the floating child entries re-initialize. Teleport to a floating island column and visually inspect walls. + +--- + +## Assumptions + +- Both `floatingChildBiomes` entries are intended to benefit from wall warping — supported by the user's phrasing "the shards" (plural) and Q3 choice "Same warp for both". Task 1 depends on this. +- `wallWarpStyle = SIMPLEX, zoom 0.25` + `wallWarpAmplitude = 10.0` lands in the lower-medium range between the `@Desc` labels "4..8 = gentle" and "16+ = heavily meandering" on `IrisFloatingChildBiomes.java:175`. Task 1 depends on this. +- Iris correctly serializes `wallWarpStyle` into an `IrisGeneratorStyle` on pack load. Supported by `IrisFloatingChildBiomes.getWallWarpCng(...)` reading it via `getWallWarpStyle()` and `FloatingIslandSample:221` consuming the CNG (no reflection path or gated loader). Task 1 depends on this. +- The generator does not cache island solidity in a way that would persist pre-change results — `FloatingIslandSample` uses `CHUNK_MEMO` ThreadLocal which is cleared on each chunk build. Task 1 (verification step) depends on this; if stale chunks appear post-edit, regenerate or use a fresh world. +- No other pack files reference `roughplains` via snippet-inheritance that would also need edits. Supported by Grep of `floatingChildBiomes` returning only `roughplains.json`. Task 1 depends on this. + +--- + +## Risks and Mitigations + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|-----------| +| JSON syntax error (missing comma, wrong quote) breaks pack load | Low | Medium — pack won't load until fixed | Validate with `python3 -m json.tool ` after edit; check Iris console for load errors on server restart | +| Amplitude 10 on the taller tropical/wilds shards (112 thick) still reads as too vertical | Medium | Low — purely aesthetic, follow-up tune | Document in "Deferred Ideas" the fallback of bumping tropical amplitude to ~14 or adding scale-aware per-entry tuning | +| Warp displaces thin edge columns entirely out of the footprint, shrinking island footprint noticeably | Medium | Low — this is the desired effect but could feel over-aggressive at the edges | Amplitude 10 is moderate; if islands feel too shrunken, reduce to 6-8 on follow-up | +| Enabling warp adds 2 extra 3D noise samples per Y layer inside islands — perf hit on pregen | Low | Low — bounded by island coverage × max thickness; CNG samples are cheap and already-cached | No action needed; if pregen CPS regresses (see `project_iris_pregen_perf.md`), revisit by disabling warp on one entry | +| The two entries producing identical warp patterns at the same world XZ (because the warp seed mask `0xA117BA17E0FL` is constant across entries) makes them look correlated | Low | Very low — entries don't spatially overlap (picker noise chooses one per column) | Mention as a known-limitation in "Deferred Ideas"; if user ever wants decorrelated warp, add a per-entry seed salt. Not in scope. | + +--- + +## Goal Verification + +### Truths + +1. `wallWarpStyle` and `wallWarpAmplitude` are set on **both** entries of `floatingChildBiomes` in `roughplains.json`. +2. Both entries use **identical** warp config: `{"style":"SIMPLEX","zoom":0.25}` and amplitude `10.0`. +3. `roughplains.json` remains valid JSON (parses without error). +4. The placement of the two new keys is inside each floating entry object, adjacent to other wall/interior shaping fields (between `maxThickness` and `carveStyle`). +5. No files under `Iris/core/` are modified (enforced by `git diff --stat` showing only the pack JSON changed, if the user is tracking the pack in git). +6. At runtime, a generated floating island in a roughplains region shows horizontal wall perturbation (bulge/recede) rather than a straight XZ extrusion of the 2D footprint. (Manual verification — see scenario MV-001 below.) + +### Artifacts + +- `[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json` — the edit +- `Iris/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java:170-176` — schema source (read-only reference, not modified) +- `Iris/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java:220-254` — consumer source (read-only reference, not modified) + +--- + +## Manual Verification Scenarios + +No browser automation path exists for a Minecraft server plugin. Scenarios are documented for the user to run in-game. + +### MV-001: Wall warp visually active on roughplains floating islands +**Priority:** Critical +**Preconditions:** Test world generated (or fresh chunks loaded) using the overworld pack with the edited `roughplains.json`. Fly mode + `/iris studio` access. +**Mapped Tasks:** Task 1 + +| Step | Action | Expected Result | +|------|--------|-----------------| +| 1 | Restart the server (or `/iris reload overworld`) so the pack JSON is re-parsed | Iris console logs pack load with no errors mentioning `wallWarpStyle` or `IrisFloatingChildBiomes` | +| 2 | Generate or teleport to a roughplains region (Y ≥ ~180 where floating islands live) | At least one floating island column visible | +| 3 | Fly around the side of a floating island shard | Side walls show horizontal bulges/recesses as Y changes — silhouette is NOT a perfect vertical extrusion of the top outline | +| 4 | Compare top surface with earlier screenshots/observations | Top biome surface (mushroom/forest or tropical/wilds) looks the same as before — tops unchanged | +| 5 | Compare bottom tail with earlier observations | Bottom tail (dripping/roots) looks the same as before — bottoms unchanged | + +### MV-002: Pack loads as valid JSON +**Priority:** Critical +**Preconditions:** Edit applied. +**Mapped Tasks:** Task 1 + +| Step | Action | Expected Result | +|------|--------|-----------------| +| 1 | Run `python3 -m json.tool ` from shell | Exits 0, no parse error | +| 2 | Grep the file for `wallWarpStyle` | Returns exactly 2 matches (one per floating entry) | +| 3 | Grep the file for `wallWarpAmplitude` | Returns exactly 2 matches | + +--- + +## Progress Tracking + +- [x] Task 1: Enable wall warp on both `floatingChildBiomes` entries in `roughplains.json` + +**Total Tasks:** 1 | **Completed:** 1 | **Remaining:** 0 + +--- + +## Implementation Tasks + +### Task 1: Enable wall warp on both floating child biomes in roughplains.json + +**Objective:** Add `wallWarpStyle` and `wallWarpAmplitude` to both entries of `floatingChildBiomes` in `roughplains.json`, identical values on both. +**Dependencies:** None +**Mapped Scenarios:** MV-001, MV-002 + +**Files:** + +- Modify: `/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json` + +**Key Decisions / Notes:** + +- Insert two keys in each entry, immediately after `"maxThickness": N,` and before `"carveStyle": { ... },`. +- Exact JSON to insert (indentation matches surrounding 4-space style — one leading space for the key position based on the file's existing alignment): + +```json +"wallWarpStyle": { + "style": "SIMPLEX", + "zoom": 0.25 +}, +"wallWarpAmplitude": 10.0, +``` + +- Do this on the `mushroom/forest` entry (currently the block starting at line 255) AND on the `tropical/wilds` entry (starting at line 277). +- Do NOT reorder any existing fields. +- Do NOT add `wallWarpStyle` to the parent biome fields — it is per-entry on `IrisFloatingChildBiomes`, not on `IrisBiome`. +- Preserve the existing (minor) indentation quirks in the file as-is; this is a targeted additive edit. +- No performance concern — the warp adds two noise samples per Y layer inside island columns only; CNG results are cached per-entry via `AtomicCache` in `IrisFloatingChildBiomes`. + +**Definition of Done:** + +- [ ] Both `floatingChildBiomes` entries contain `wallWarpStyle: {"style":"SIMPLEX","zoom":0.25}` and `wallWarpAmplitude: 10.0`. +- [ ] Pre-edit sanity: `grep -rl 'floatingChildBiomes' /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/` returns only `roughplains.json` (no other pack file needs the same edit). +- [ ] `python3 -m json.tool ` exits 0 (valid JSON). +- [ ] `grep -c 'wallWarpStyle' ` returns `2`. +- [ ] `grep -c 'wallWarpAmplitude' ` returns `2`. +- [ ] MV-001 passes end-to-end on a live test world: side walls of roughplains floating shards exhibit horizontal bulge/recession, while tops and bottoms look unchanged from prior observation. +- [ ] No files under `Iris/core/` or any other plugin source tree are modified. + +**Verify:** + +```bash +# JSON validity +python3 -m json.tool /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json > /dev/null + +# Field counts +grep -c 'wallWarpStyle' /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json +grep -c 'wallWarpAmplitude' /Users/brianfopiano/Developer/RemoteGit/[Minecraft\ Server]/consumers/plugin-consumers/shared-plugin-data/iris/packs/overworld/biomes/temperate/roughplains.json +``` + +Then MV-001 via manual in-game verification on the user's running test server. + +--- + +## Deferred Ideas + +- **Per-entry scale-aware tuning:** If the tropical/wilds shards (112 thick) still read as too vertical compared to mushroom/forest (32 thick), bump tropical's `wallWarpAmplitude` to ~14 while leaving mushroom at 10. +- **Per-entry warp decorrelation:** Both entries currently sample `wallWarp` noise from the same RNG mask (`0xA117BA17E0FL` in `IrisFloatingChildBiomes:81`). Entries never spatially overlap (the picker routes each column to exactly one), but if visually noticeable correlation ever appears where biomes are adjacent, add a per-entry seed salt. +- **Higher-fidelity warp (iq-style):** Chain a second noise sample: `P' = P + A·n1(P); P'' = P' + B·n2(P');` then sample footprint at `P''`. Only pursue if the single-pass warp is not expressive enough after Task 1 is tuned. +- **Vertical frequency scaling:** Expose a `wallWarpVerticalScale` so the warp varies faster with Y than with XZ — would make adjacent Y slices more different, reducing any residual vertical banding without changing horizontal feature size. +- **Schema default flip:** Changing `IrisFloatingChildBiomes.wallWarpStyle` default from `null` to a gentle SIMPLEX. Out of scope this plan; would silently change behavior for any existing pack.