This commit is contained in:
Brian Neumann-Fopiano
2026-07-18 19:16:14 -04:00
parent ad41b6795c
commit 1d194e880b
12 changed files with 1208 additions and 128 deletions
@@ -19,6 +19,7 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -46,13 +47,16 @@ import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import lombok.Data;
import lombok.experimental.Accessors;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -230,6 +234,7 @@ public class IrisCreator {
addToBukkitYml();
J.s(() -> IrisServices.get(MultiverseCoreLink.class).updateWorld(world, dimension));
}
scheduleSenderTeleport(world);
if (pregen != null) {
CompletableFuture<Boolean> ff = new CompletableFuture<>();
@@ -254,6 +259,73 @@ public class IrisCreator {
return world;
}
static Player createTeleportTarget(VolmitSender sender, boolean studio, boolean benchmark) {
if (studio || benchmark || sender == null || !sender.isPlayer()) {
return null;
}
return sender.player();
}
static CompletableFuture<Boolean> teleportSenderToCreatedWorld(
Player player,
World world,
WorldRuntimeControlService runtimeControl
) {
Player requiredPlayer = Objects.requireNonNull(player, "player");
World requiredWorld = Objects.requireNonNull(world, "world");
WorldRuntimeControlService requiredRuntime = Objects.requireNonNull(runtimeControl, "runtimeControl");
Location entryAnchor = requiredRuntime.resolveEntryAnchor(requiredWorld);
if (entryAnchor == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Unable to resolve the entry anchor for world \"" + requiredWorld.getName() + "\"."));
}
int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4;
return requiredRuntime.requestChunkAsync(requiredWorld, chunkX, chunkZ, true)
.thenCompose(chunk -> requiredRuntime.resolveSafeEntry(requiredWorld, entryAnchor))
.thenCompose(safeEntry -> {
if (safeEntry == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Unable to resolve a safe entry for world \"" + requiredWorld.getName() + "\"."));
}
return requiredRuntime.teleport(requiredPlayer, safeEntry);
});
}
private void scheduleSenderTeleport(World world) {
Player player = createTeleportTarget(sender, studio, benchmark);
if (player == null) {
return;
}
CompletableFuture<Boolean> teleportFuture;
try {
teleportFuture = teleportSenderToCreatedWorld(player, world, WorldRuntimeControlService.get());
} catch (Throwable e) {
reportSenderTeleportFailure(player, world, e);
return;
}
teleportFuture.whenComplete((success, throwable) -> {
if (throwable != null) {
reportSenderTeleportFailure(player, world, throwable);
return;
}
if (!Boolean.TRUE.equals(success)) {
reportSenderTeleportFailure(player, world, new IllegalStateException(
"The runtime teleport operation returned false for player \"" + player.getName() + "\"."));
}
});
}
private void reportSenderTeleportFailure(Player player, World world, Throwable throwable) {
IrisLogging.reportError("World \"" + world.getName()
+ "\" was created, but automatic teleport failed for player \"" + player.getName() + "\".", throwable);
J.runEntity(player, () -> new VolmitSender(player).sendMessage(C.YELLOW
+ "The world was created, but automatic teleport failed. Try /iris teleport world=" + world.getName()));
}
private void reportStudioProgress(double progress, String stage) {
BiConsumer<Double, String> consumer = studioProgressConsumer;
if (consumer == null) {
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.NativeStructureSuppression;
import art.arcane.iris.engine.object.ObjectPlaceMode;
@@ -403,10 +404,12 @@ public final class IrisStructureLocator {
}
int sideExtension = placement.isOverbore()
? Math.max(1, placement.getOverboreRadius())
? Math.max(0, placement.getOverboreRadius())
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
IrisStructureCarveShape overboreShape = placement.resolvedOverboreShape();
int topExtension = placement.isOverbore()
? (int) Math.ceil(Math.max(1D, placement.getOverboreHeight()) * 1.8D)
? overboreShape.maximumCeilingExtension(
placement.getOverboreHeight(), placement.resolvedOverboreErosionStrength())
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
int bottomExtension = placement.isOverbore() ? Math.max(0, placement.getOverboreFloor()) : 0;
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
@@ -419,7 +422,8 @@ public final class IrisStructureLocator {
Long2IntOpenHashMap surfaceHeights = new Long2IntOpenHashMap();
surfaceHeights.defaultReturnValue(Integer.MIN_VALUE);
if (placement.isBore() && !placement.isOverbore()) {
if ((placement.isBore() && !placement.isOverbore())
|| (placement.isOverbore() && overboreShape == IrisStructureCarveShape.BOX)) {
maximumShift = resolveBurialEnvelopeShift(
engine,
bounds[0] - sideExtension,
@@ -37,6 +37,7 @@ import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.iris.spi.IrisLogging;
@@ -65,10 +66,8 @@ import java.util.Objects;
public class IrisStructureComponent extends IrisMantleComponent {
private static final long MAX_BORE_VOLUME = 6_000_000L;
private static final long MAX_OVERBORE_VOLUME = 48_000_000L;
private static final double OVERBORE_MIN_BOUNDARY = 0.2;
private static final double OVERBORE_MIN_BOUNDARY_SQUARED = OVERBORE_MIN_BOUNDARY * OVERBORE_MIN_BOUNDARY;
private static final double OVERBORE_BOUNDARY_SPAN = 0.8;
private static final double OVERBORE_MAX_UP_REACH = 1.8;
private static final int MAX_OVERBORE_FOOTPRINT_COLUMNS = 1_048_576;
private static final double OVERBORE_ROLL_FREQUENCY_RATIO = 3D / 7D;
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
public IrisStructureComponent(EngineMantle engineMantle) {
@@ -128,7 +127,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
}
if (placement.isOverbore()) {
overboreStructure(writer, pieces, placement.getOverboreRadius(), placement.getOverboreHeight(), placement.getOverboreFloor());
overboreStructure(writer, pieces, placement);
} else if (placement.isBore()) {
boreStructure(writer, pieces, placement.getBorePadding());
}
@@ -286,102 +285,95 @@ public class IrisStructureComponent extends IrisMantleComponent {
}
}
private void overboreStructure(MantleWriter writer, KList<PlacedStructurePiece> pieces, int radius, int ceiling, int floorDepth) {
private void overboreStructure(MantleWriter writer, KList<PlacedStructurePiece> pieces,
IrisStructurePlacement placement) {
int[] bounds = computePieceBounds(pieces);
if (bounds == null) {
return;
}
int margin = Math.max(1, radius);
int head = Math.max(0, ceiling);
int floorCut = Math.max(0, floorDepth);
IrisStructureCarveShape shape = placement.resolvedOverboreShape();
int margin = Math.max(0, placement.getOverboreRadius());
int head = Math.max(0, placement.getOverboreHeight());
int floorCut = Math.max(0, placement.getOverboreFloor());
double strength = placement.resolvedOverboreErosionStrength();
int mantleOffset = getEngineMantle().getEngine().getMinHeight();
int worldMin = getEngineMantle().getEngine().getMinHeight() + 1;
int worldMax = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1;
int upExtension = shape.maximumCeilingExtension(head, strength);
double freq = 0.07;
double rollFreq = 0.03;
double reachSide = margin;
double reachUp = head < 1 ? 1.0 : head;
double reachDown = floorCut < 1 ? 1.0 : floorCut;
double upReachMin = 0.4;
double upReachSpan = 1.4;
int sideExt = overboreSideExtension(margin);
int upExt = overboreUpExtension(reachUp);
long work = 0L;
for (PlacedStructurePiece p : pieces) {
long wx = (long) (p.getMaxX() - p.getMinX() + 1) + 2L * sideExt;
long wz = (long) (p.getMaxZ() - p.getMinZ() + 1) + 2L * sideExt;
long wy = (long) (p.getMaxY() - p.getMinY() + 1) + upExt + floorCut;
work += wx * wy * wz;
}
if (work > MAX_OVERBORE_VOLUME) {
IrisLogging.warn("Skipping structure overbore of " + work + " blocks (cap " + MAX_OVERBORE_VOLUME + "); reduce overboreRadius/overboreHeight or use larger spacing.");
if (shape == IrisStructureCarveShape.BOX) {
carveOverboreBox(writer, bounds, margin, head, floorCut, mantleOffset, worldMin, worldMax);
return;
}
RNG noiseRng = new RNG(seed() + Cache.key(bounds[0], bounds[2]));
CNG blob = CNG.signature(noiseRng);
CNG roll = CNG.signature(noiseRng.nextParallelRNG(0x2A17));
if (IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("Overbore carving organic cavern: pieces=" + pieces.size() + " margin=" + margin + " head=" + head + " floorCut=" + floorCut + " work=" + work);
long work = overboreCandidateVolume(bounds, margin, upExtension, floorCut);
if (work > MAX_OVERBORE_VOLUME) {
warnOverboreVolume(work);
return;
}
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(
pieces, margin, MAX_OVERBORE_FOOTPRINT_COLUMNS);
if (footprint == null) {
IrisLogging.warn("Structure overbore footprint was empty or exceeded "
+ MAX_OVERBORE_FOOTPRINT_COLUMNS + " columns; skipping its expanded carve.");
return;
}
for (PlacedStructurePiece p : pieces) {
int pMinX = p.getMinX();
int pMinY = p.getMinY();
int pMinZ = p.getMinZ();
int pMaxX = p.getMaxX();
int pMaxY = p.getMaxY();
int pMaxZ = p.getMaxZ();
int exMinX = pMinX - sideExt;
int exMaxX = pMaxX + sideExt;
int exMinZ = pMinZ - sideExt;
int exMaxZ = pMaxZ + sideExt;
int exMinY = Math.max(worldMin, pMinY - floorCut);
int exMaxY = Math.min(worldMax, pMaxY + upExt);
for (int bx = exMinX; bx <= exMaxX; bx++) {
double dx = bx < pMinX ? pMinX - bx : bx > pMaxX ? bx - pMaxX : 0;
double nx = dx / reachSide;
for (int bz = exMinZ; bz <= exMaxZ; bz++) {
double dz = bz < pMinZ ? pMinZ - bz : bz > pMaxZ ? bz - pMaxZ : 0;
double nz = dz / reachSide;
double nxz = nx * nx + nz * nz;
if (nxz > 1.0) {
double frequency = placement.resolvedOverboreErosionFrequency();
boolean eroded = shape == IrisStructureCarveShape.ERODED && strength > 0D;
CNG blob = null;
CNG roll = null;
if (eroded) {
RNG noiseRng = new RNG(seed() + Cache.key(bounds[0], bounds[2]));
blob = CNG.signature(noiseRng);
roll = CNG.signature(noiseRng.nextParallelRNG(0x2A17));
}
if (IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("Overbore carving structure cavern: shape=" + shape + " pieces=" + pieces.size()
+ " footprint=" + footprint.width() + "x" + footprint.depth()
+ " margin=" + margin + " head=" + head + " floorCut=" + floorCut + " work=" + work);
}
double sideReachSquared = (double) margin * margin;
double downReach = Math.max(1D, floorCut);
for (int bz = footprint.minZ(); bz <= footprint.maxZ(); bz++) {
int footprintIndex = footprint.indexAt(footprint.minX(), bz);
for (int bx = footprint.minX(); bx <= footprint.maxX(); bx++, footprintIndex++) {
long horizontalDistanceSquared = footprint.distanceSquaredAt(footprintIndex);
if (horizontalDistanceSquared < 0L || horizontalDistanceSquared > sideReachSquared) {
continue;
}
double normalizedHorizontalDistanceSquared = sideReachSquared == 0D
? 0D : horizontalDistanceSquared / sideReachSquared;
int sourceMinY = footprint.sourceMinYAt(footprintIndex);
int sourceMaxY = footprint.sourceMaxYAt(footprintIndex);
double upReach = eroded
? erodedUpReach(roll, frequency, strength, bx, bz, head)
: Math.max(1D, head);
int columnUpExtension = eroded
? (int) Math.ceil(upReach) : head;
int minY = Math.max(worldMin, sourceMinY - floorCut);
int maxY = Math.min(worldMax, sourceMaxY + columnUpExtension);
for (int by = minY; by <= maxY; by++) {
double normalizedY = normalizedVerticalDistance(
by, sourceMinY, sourceMaxY, upReach, downReach);
double distanceSquared = normalizedHorizontalDistanceSquared
+ normalizedY * normalizedY;
if (distanceSquared <= 0D) {
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
continue;
}
double w = roll.fitDouble(0.0, 1.0, bx * rollFreq, bz * rollFreq) * 0.7
+ roll.fitDouble(0.0, 1.0, bx * rollFreq * 3.0, bz * rollFreq * 3.0) * 0.3;
double contrast = (w - 0.5) * 2.6 + 0.5;
if (contrast < 0.0) {
contrast = 0.0;
} else if (contrast > 1.0) {
contrast = 1.0;
if (distanceSquared > 1D) {
continue;
}
double upReach = reachUp * (upReachMin + upReachSpan * contrast);
if (upReach < 1.0) {
upReach = 1.0;
if (!eroded) {
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
continue;
}
for (int by = exMinY; by <= exMaxY; by++) {
double ny;
if (by > pMaxY) {
ny = (by - pMaxY) / upReach;
} else if (by < pMinY) {
ny = (pMinY - by) / reachDown;
} else {
ny = 0.0;
}
double distanceSquared = nxz + ny * ny;
if (distanceSquared > 1.0) {
continue;
}
if (distanceSquared > OVERBORE_MIN_BOUNDARY_SQUARED) {
double n = blob.fitDouble(0.0, 1.0, bx * freq, by * freq, bz * freq);
if (!shouldCarveOverboreCell(distanceSquared, n)) {
continue;
}
}
double noise = blob.fitDouble(
0D, 1D, bx * frequency, by * frequency, bz * frequency);
if (shouldCarveOverboreCell(shape, distanceSquared, noise, strength)) {
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
}
}
@@ -389,28 +381,100 @@ public class IrisStructureComponent extends IrisMantleComponent {
}
}
static int overboreSideExtension(int radius) {
return Math.max(1, radius);
private void carveOverboreBox(MantleWriter writer, int[] bounds, int margin, int head, int floorCut,
int mantleOffset, int worldMin, int worldMax) {
int minX = bounds[0] - margin;
int minY = Math.max(worldMin, bounds[1] - floorCut);
int minZ = bounds[2] - margin;
int maxX = bounds[3] + margin;
int maxY = Math.min(worldMax, bounds[4] + head);
int maxZ = bounds[5] + margin;
long volume = inclusiveVolume(minX, minY, minZ, maxX, maxY, maxZ);
if (volume > MAX_OVERBORE_VOLUME) {
warnOverboreVolume(volume);
return;
}
for (int bx = minX; bx <= maxX; bx++) {
for (int by = minY; by <= maxY; by++) {
for (int bz = minZ; bz <= maxZ; bz++) {
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
}
}
}
}
static int overboreUpExtension(double reachUp) {
return (int) Math.ceil(Math.max(1.0, reachUp) * OVERBORE_MAX_UP_REACH);
static double erodedUpReach(CNG roll, double frequency, double strength, int x, int z, int head) {
if (head <= 0) {
return 0D;
}
double clampedStrength = Math.max(0D, Math.min(1D, strength));
if (clampedStrength <= 0D) {
return Math.max(1D, head);
}
double rollFrequency = frequency * OVERBORE_ROLL_FREQUENCY_RATIO;
double sample = roll.fitDouble(0D, 1D, x * rollFrequency, z * rollFrequency) * 0.7D
+ roll.fitDouble(0D, 1D, x * rollFrequency * 3D, z * rollFrequency * 3D) * 0.3D;
double contrast = Math.max(0D, Math.min(1D, (sample - 0.5D) * 2.6D + 0.5D));
double roundedReach = Math.max(1D, head);
double erodedReach = Math.max(1D, head * (0.4D + 1.4D * contrast));
return roundedReach + clampedStrength * (erodedReach - roundedReach);
}
static double overboreBoundaryLimit(double noise) {
private static double normalizedVerticalDistance(int y, int sourceMinY, int sourceMaxY,
double upReach, double downReach) {
if (y > sourceMaxY) {
return (y - sourceMaxY) / upReach;
}
if (y < sourceMinY) {
return (sourceMinY - y) / downReach;
}
return 0D;
}
private static long overboreCandidateVolume(int[] bounds, int margin, int upExtension,
int floorCut) {
return inclusiveVolume(
bounds[0] - margin, bounds[1] - floorCut, bounds[2] - margin,
bounds[3] + margin, bounds[4] + upExtension, bounds[5] + margin);
}
private static long inclusiveVolume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
long width = (long) maxX - minX + 1L;
long height = (long) maxY - minY + 1L;
long depth = (long) maxZ - minZ + 1L;
if (width <= 0L || height <= 0L || depth <= 0L
|| width > Long.MAX_VALUE / height
|| width * height > Long.MAX_VALUE / depth) {
return Long.MAX_VALUE;
}
return width * height * depth;
}
private static void warnOverboreVolume(long volume) {
IrisLogging.warn("Skipping structure overbore of " + volume + " blocks (cap "
+ MAX_OVERBORE_VOLUME + "); reduce overboreRadius/overboreHeight or use larger spacing.");
}
static double overboreBoundaryLimit(double noise, double strength) {
double clampedNoise = Math.max(0.0, Math.min(1.0, noise));
return OVERBORE_MIN_BOUNDARY + OVERBORE_BOUNDARY_SPAN * clampedNoise;
double clampedStrength = Math.max(0.0, Math.min(1.0, strength));
return 1D - clampedStrength * (1D - clampedNoise);
}
static boolean shouldCarveOverboreCell(double distanceSquared, double noise) {
static boolean shouldCarveOverboreCell(IrisStructureCarveShape shape, double distanceSquared,
double noise, double strength) {
if (distanceSquared <= 0.0) {
return true;
}
if (distanceSquared > 1.0) {
return false;
}
double limit = overboreBoundaryLimit(noise);
return distanceSquared <= limit * limit;
IrisStructureCarveShape resolvedShape = shape == null ? IrisStructureCarveShape.ERODED : shape;
return switch (resolvedShape) {
case BOX -> true;
case ROUNDED -> distanceSquared <= 1D;
case ERODED -> {
double limit = overboreBoundaryLimit(noise, strength);
yield distanceSquared <= 1D && distanceSquared <= limit * limit;
}
};
}
private void clearIntersectingObjectTrees(MantleWriter writer, IrisStructureLocator.ResolvedPlacement resolved) {
@@ -0,0 +1,420 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KList;
import it.unimi.dsi.fastutil.longs.Long2LongMap;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import java.util.Arrays;
final class StructureCarvingFootprint {
static final int DEFAULT_MAX_CELLS = 1_048_576;
private final int minX;
private final int maxX;
private final int minZ;
private final int maxZ;
private final int width;
private final int depth;
private final long[] distanceSquared;
private final int[] nearestSourceIds;
private final int[] sourceMinY;
private final int[] sourceMaxY;
private StructureCarvingFootprint(FootprintData data) {
minX = data.minX();
maxX = data.maxX();
minZ = data.minZ();
maxZ = data.maxZ();
width = data.width();
depth = data.depth();
distanceSquared = data.distanceSquared();
nearestSourceIds = data.nearestSourceIds();
sourceMinY = data.sourceMinY();
sourceMaxY = data.sourceMaxY();
}
static StructureCarvingFootprint from(KList<PlacedStructurePiece> pieces, int padding) {
return from(pieces, padding, DEFAULT_MAX_CELLS);
}
static StructureCarvingFootprint from(KList<PlacedStructurePiece> pieces, int padding, int maxCells) {
if (padding < 0) {
throw new IllegalArgumentException("padding must be non-negative");
}
if (maxCells < 1) {
throw new IllegalArgumentException("maxCells must be positive");
}
if (pieces == null || pieces.isEmpty()) {
return null;
}
SourceCollection sources = collectSources(pieces);
if (sources == null || sources.columns().isEmpty()) {
return null;
}
Bounds bounds = paddedBounds(sources.bounds(), padding, maxCells);
if (bounds == null) {
return null;
}
int[] nearestSourceIds = new int[bounds.cellCount()];
Arrays.fill(nearestSourceIds, -1);
int sourceCount = sources.columns().size();
int[] sourceX = new int[sourceCount];
int[] sourceZ = new int[sourceCount];
int[] sourceMinY = new int[sourceCount];
int[] sourceMaxY = new int[sourceCount];
int sourceId = 0;
for (Long2LongMap.Entry entry : sources.columns().long2LongEntrySet()) {
int localX = unpackX(entry.getLongKey()) - bounds.minX();
int localZ = unpackZ(entry.getLongKey()) - bounds.minZ();
long heights = entry.getLongValue();
sourceX[sourceId] = localX;
sourceZ[sourceId] = localZ;
sourceMinY[sourceId] = unpackMinY(heights);
sourceMaxY[sourceId] = unpackMaxY(heights);
nearestSourceIds[localZ * bounds.width() + localX] = sourceId;
sourceId++;
}
transformRows(nearestSourceIds, sourceX, sourceZ, bounds.width(), bounds.depth());
long[] distanceSquared = transformColumns(
nearestSourceIds, sourceX, sourceZ, bounds.width(), bounds.depth());
FootprintData data = new FootprintData(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.width(), bounds.depth(), distanceSquared, nearestSourceIds,
sourceMinY, sourceMaxY);
return new StructureCarvingFootprint(data);
}
int minX() {
return minX;
}
int maxX() {
return maxX;
}
int minZ() {
return minZ;
}
int maxZ() {
return maxZ;
}
int width() {
return width;
}
int depth() {
return depth;
}
boolean contains(int worldX, int worldZ) {
return worldX >= minX && worldX <= maxX && worldZ >= minZ && worldZ <= maxZ;
}
int indexAt(int worldX, int worldZ) {
if (!contains(worldX, worldZ)) {
return -1;
}
return (worldZ - minZ) * width + worldX - minX;
}
long distanceSquaredAt(int worldX, int worldZ) {
return distanceSquaredAt(requireIndex(worldX, worldZ));
}
long distanceSquaredAt(int index) {
return distanceSquared[index];
}
int sourceMinYAt(int worldX, int worldZ) {
return sourceMinYAt(requireIndex(worldX, worldZ));
}
int sourceMinYAt(int index) {
return sourceMinY[nearestSourceIds[index]];
}
int sourceMaxYAt(int worldX, int worldZ) {
return sourceMaxYAt(requireIndex(worldX, worldZ));
}
int sourceMaxYAt(int index) {
return sourceMaxY[nearestSourceIds[index]];
}
Column sample(int worldX, int worldZ) {
int index = indexAt(worldX, worldZ);
if (index < 0) {
return null;
}
return new Column(distanceSquaredAt(index), sourceMinYAt(index), sourceMaxYAt(index));
}
private static SourceCollection collectSources(KList<PlacedStructurePiece> pieces) {
Long2LongOpenHashMap columns = new Long2LongOpenHashMap();
MutableBounds bounds = new MutableBounds();
for (PlacedStructurePiece piece : pieces) {
if (piece == null) {
continue;
}
IrisObject object = piece.getObject();
IrisObjectRotation rotation = piece.getRotation();
if (object == null || rotation == null || object.getBlocks() == null) {
continue;
}
for (IrisBlockVector local : object.getBlocks().keys()) {
if (local == null || B.isAir(object.getBlocks().get(local))) {
continue;
}
IrisBlockVector rotated = rotation.rotate(local.clone());
long worldX = (long) piece.getX() + rotated.getBlockX();
long worldY = (long) piece.getY() + rotated.getBlockY();
long worldZ = (long) piece.getZ() + rotated.getBlockZ();
if (!fitsInteger(worldX) || !fitsInteger(worldY) || !fitsInteger(worldZ)) {
return null;
}
mergeColumn(columns, (int) worldX, (int) worldY, (int) worldZ);
bounds.include((int) worldX, (int) worldZ);
}
}
return columns.isEmpty() ? null : new SourceCollection(columns, bounds.freeze());
}
private static Bounds paddedBounds(MutableBoundsSnapshot sourceBounds, int padding, int maxCells) {
long paddedMinX = (long) sourceBounds.minX() - padding;
long paddedMaxX = (long) sourceBounds.maxX() + padding;
long paddedMinZ = (long) sourceBounds.minZ() - padding;
long paddedMaxZ = (long) sourceBounds.maxZ() + padding;
if (!fitsInteger(paddedMinX) || !fitsInteger(paddedMaxX)
|| !fitsInteger(paddedMinZ) || !fitsInteger(paddedMaxZ)) {
return null;
}
long width = paddedMaxX - paddedMinX + 1L;
long depth = paddedMaxZ - paddedMinZ + 1L;
if (width < 1L || depth < 1L || width > Integer.MAX_VALUE || depth > Integer.MAX_VALUE
|| width > maxCells / depth) {
return null;
}
int cellCount = (int) (width * depth);
return new Bounds(
(int) paddedMinX, (int) paddedMaxX, (int) paddedMinZ, (int) paddedMaxZ,
(int) width, (int) depth, cellCount);
}
private static void mergeColumn(Long2LongOpenHashMap columns, int x, int y, int z) {
long key = pack(x, z);
if (!columns.containsKey(key)) {
columns.put(key, packHeights(y, y));
return;
}
long existing = columns.get(key);
columns.put(key, packHeights(
Math.min(y, unpackMinY(existing)), Math.max(y, unpackMaxY(existing))));
}
private static void transformRows(int[] nearestSourceIds, int[] sourceX, int[] sourceZ,
int width, int depth) {
for (int z = 0; z < depth; z++) {
int rowStart = z * width;
int previousSource = -1;
for (int x = 0; x < width; x++) {
int index = rowStart + x;
int sourceId = nearestSourceIds[index];
if (isExplicitSource(sourceId, x, z, sourceX, sourceZ)) {
previousSource = sourceId;
} else {
nearestSourceIds[index] = previousSource;
}
}
int nextSource = -1;
for (int x = width - 1; x >= 0; x--) {
int index = rowStart + x;
int currentSource = nearestSourceIds[index];
if (isExplicitSource(currentSource, x, z, sourceX, sourceZ)) {
nextSource = currentSource;
}
if (isBetterHorizontalSource(nextSource, currentSource, x, sourceX)) {
nearestSourceIds[index] = nextSource;
}
}
}
}
private static long[] transformColumns(int[] nearestSourceIds, int[] sourceX, int[] sourceZ,
int width, int depth) {
long[] distanceSquared = new long[nearestSourceIds.length];
int[] siteRows = new int[depth];
int[] siteSourceIds = new int[depth];
int[] starts = new int[depth];
long[] siteCosts = new long[depth];
for (int x = 0; x < width; x++) {
int siteCount = buildEnvelope(
nearestSourceIds, sourceX, width, depth, x,
siteRows, siteSourceIds, starts, siteCosts);
int siteIndex = 0;
for (int z = 0; z < depth; z++) {
while (siteIndex + 1 < siteCount && starts[siteIndex + 1] <= z) {
siteIndex++;
}
int sourceId = siteSourceIds[siteIndex];
int index = z * width + x;
long deltaX = (long) x - sourceX[sourceId];
long deltaZ = (long) z - sourceZ[sourceId];
nearestSourceIds[index] = sourceId;
distanceSquared[index] = deltaX * deltaX + deltaZ * deltaZ;
}
}
return distanceSquared;
}
private static int buildEnvelope(int[] nearestSourceIds, int[] sourceX, int width, int depth,
int x, int[] siteRows, int[] siteSourceIds,
int[] starts, long[] siteCosts) {
int siteCount = 0;
for (int z = 0; z < depth; z++) {
int sourceId = nearestSourceIds[z * width + x];
if (sourceId < 0) {
continue;
}
long deltaX = (long) x - sourceX[sourceId];
long cost = deltaX * deltaX;
int start = Integer.MIN_VALUE;
while (siteCount > 0) {
start = firstStrictlyBetterAt(
siteRows[siteCount - 1], siteCosts[siteCount - 1], z, cost);
if (start > starts[siteCount - 1]) {
break;
}
siteCount--;
}
if (siteCount == 0) {
start = Integer.MIN_VALUE;
}
siteRows[siteCount] = z;
siteSourceIds[siteCount] = sourceId;
siteCosts[siteCount] = cost;
starts[siteCount] = start;
siteCount++;
}
return siteCount;
}
private static int firstStrictlyBetterAt(int existingRow, long existingCost,
int candidateRow, long candidateCost) {
long numerator = candidateCost + (long) candidateRow * candidateRow
- existingCost - (long) existingRow * existingRow;
long denominator = 2L * (candidateRow - existingRow);
long start = Math.floorDiv(numerator, denominator) + 1L;
if (start < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
if (start > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int) start;
}
private static boolean isExplicitSource(int sourceId, int x, int z,
int[] sourceX, int[] sourceZ) {
return sourceId >= 0 && sourceX[sourceId] == x && sourceZ[sourceId] == z;
}
private static boolean isBetterHorizontalSource(int candidateId, int currentId,
int x, int[] sourceX) {
if (candidateId < 0) {
return false;
}
if (currentId < 0) {
return true;
}
long candidateDelta = (long) x - sourceX[candidateId];
long currentDelta = (long) x - sourceX[currentId];
long candidateDistance = candidateDelta * candidateDelta;
long currentDistance = currentDelta * currentDelta;
return candidateDistance < currentDistance
|| candidateDistance == currentDistance && sourceX[candidateId] < sourceX[currentId];
}
private int requireIndex(int worldX, int worldZ) {
int index = indexAt(worldX, worldZ);
if (index < 0) {
throw new IndexOutOfBoundsException(
"coordinate outside carving footprint: " + worldX + "," + worldZ);
}
return index;
}
private static boolean fitsInteger(long value) {
return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE;
}
private static long pack(int x, int z) {
return (long) x << 32 | z & 0xffffffffL;
}
private static int unpackX(long packed) {
return (int) (packed >> 32);
}
private static int unpackZ(long packed) {
return (int) packed;
}
private static long packHeights(int minY, int maxY) {
return (long) minY << 32 | maxY & 0xffffffffL;
}
private static int unpackMinY(long packed) {
return (int) (packed >> 32);
}
private static int unpackMaxY(long packed) {
return (int) packed;
}
record Column(long distanceSquared, int sourceMinY, int sourceMaxY) {
}
private record Bounds(int minX, int maxX, int minZ, int maxZ,
int width, int depth, int cellCount) {
}
private record FootprintData(int minX, int maxX, int minZ, int maxZ,
int width, int depth, long[] distanceSquared,
int[] nearestSourceIds, int[] sourceMinY, int[] sourceMaxY) {
}
private record SourceCollection(Long2LongOpenHashMap columns, MutableBoundsSnapshot bounds) {
}
private record MutableBoundsSnapshot(int minX, int maxX, int minZ, int maxZ) {
}
private static final class MutableBounds {
private int minX = Integer.MAX_VALUE;
private int maxX = Integer.MIN_VALUE;
private int minZ = Integer.MAX_VALUE;
private int maxZ = Integer.MIN_VALUE;
private void include(int x, int z) {
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minZ = Math.min(minZ, z);
maxZ = Math.max(maxZ, z);
}
private MutableBoundsSnapshot freeze() {
return new MutableBoundsSnapshot(minX, maxX, minZ, maxZ);
}
}
}
@@ -0,0 +1,36 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Controls the shape of the expanded terrain carve around an overbored structure.")
public enum IrisStructureCarveShape {
@Desc("Carves the full expanded rectangular volume with straight walls, ceiling, and floor.")
BOX,
@Desc("Carves a smooth rounded volume around the structure without erosion noise.")
ROUNDED,
@Desc("Carves a rounded volume whose outer boundary is eroded by deterministic noise.")
ERODED;
public double maximumCeilingScale() {
return maximumCeilingScale(1D);
}
public double maximumCeilingScale(double erosionStrength) {
double strength = Double.isFinite(erosionStrength)
? Math.max(0D, Math.min(1D, erosionStrength)) : 1D;
return switch (this) {
case BOX, ROUNDED -> 1D;
case ERODED -> 1D + 0.8D * strength;
};
}
public int maximumCeilingExtension(int configuredHeight) {
return maximumCeilingExtension(configuredHeight, 1D);
}
public int maximumCeilingExtension(int configuredHeight, double erosionStrength) {
return (int) Math.ceil(Math.max(0, configuredHeight) * maximumCeilingScale(erosionStrength));
}
}
@@ -35,6 +35,11 @@ import lombok.experimental.Accessors;
@Desc("Attaches structures to a biome, region or dimension and controls where and how often they generate. This is independent of a structure's own native generation, so you can add custom placements in tandem with native generation or instead of it.")
@Data
public class IrisStructurePlacement {
private static final double DEFAULT_OVERBORE_EROSION_STRENGTH = 0.8D;
private static final double DEFAULT_OVERBORE_EROSION_FREQUENCY = 0.07D;
private static final double MIN_OVERBORE_EROSION_FREQUENCY = 0.001D;
private static final double MAX_OVERBORE_EROSION_FREQUENCY = 1D;
@ArrayType(type = String.class, min = 1)
@RegistryListStructure
@Desc("Editable Iris structure resources to place here. Every key must resolve to a structures/*.json resource in this pack. Live vanilla, mod, and datapack registry keys are controlled through importedStructures unless explicitly cloned into Iris resources.")
@@ -98,7 +103,7 @@ public class IrisStructurePlacement {
@MinNumber(0)
@MaxNumber(128)
@Desc("when overbore=true, how many blocks of terrain to carve horizontally outward from the structure's bounding box. Larger values produce a wider open cavern around the structure. Larger values also widen the mantle write window for every chunk near the structure, so increase it deliberately.")
@Desc("when overbore=true, how many blocks of terrain to carve horizontally outward. BOX expands the assembled structure bounds; ROUNDED and ERODED expand the transformed non-air block footprint. Larger values produce a wider open cavern and widen the mantle write window for nearby chunks, so increase this deliberately.")
private int overboreRadius = 24;
@MinNumber(0)
@@ -111,9 +116,41 @@ public class IrisStructurePlacement {
@Desc("when overbore=true, how many blocks of terrain to carve below the structure's floor. 0 keeps the floor solid for support; small values recess the cavern floor around the structure.")
private int overboreFloor = 0;
@Desc("when overbore=true, controls whether the expanded carve has straight, rounded, or noise-eroded boundaries.")
private IrisStructureCarveShape overboreShape = IrisStructureCarveShape.ERODED;
@MinNumber(0)
@MaxNumber(1)
@Desc("when overboreShape=ERODED, controls how strongly noise cuts into the rounded outer shell. 0 matches ROUNDED; 1 may erode back to the mandatory structure footprint but never remove its required clearance.")
private double overboreErosionStrength = DEFAULT_OVERBORE_EROSION_STRENGTH;
@MinNumber(MIN_OVERBORE_EROSION_FREQUENCY)
@MaxNumber(MAX_OVERBORE_EROSION_FREQUENCY)
@Desc("when overboreShape=ERODED, controls the spatial frequency of boundary noise. Higher values produce smaller, busier erosion features.")
private double overboreErosionFrequency = DEFAULT_OVERBORE_EROSION_FREQUENCY;
@Desc("Optional foundation columns placed beneath the assembled structure's occupied bottom cells. Columns pass through air and fluids until they reach solid ground, up to maxDepth.")
private IrisStructureStiltSettings stilt = null;
@Desc("If false, this placement is skipped underwater.")
private boolean underwater = false;
public IrisStructureCarveShape resolvedOverboreShape() {
return overboreShape == null ? IrisStructureCarveShape.ERODED : overboreShape;
}
public double resolvedOverboreErosionStrength() {
if (!Double.isFinite(overboreErosionStrength)) {
return DEFAULT_OVERBORE_EROSION_STRENGTH;
}
return Math.max(0D, Math.min(1D, overboreErosionStrength));
}
public double resolvedOverboreErosionFrequency() {
if (!Double.isFinite(overboreErosionFrequency)) {
return DEFAULT_OVERBORE_EROSION_FREQUENCY;
}
return Math.max(MIN_OVERBORE_EROSION_FREQUENCY,
Math.min(MAX_OVERBORE_EROSION_FREQUENCY, overboreErosionFrequency));
}
}
@@ -153,9 +153,20 @@ public class SchemaBuilderParityTest {
.getJSONObject(stilt.getString("$ref").substring("#/definitions/".length()));
JSONObject stiltProperties = stiltDefinition.getJSONObject("properties");
JSONObject maxDepth = stiltProperties.getJSONObject("maxDepth");
JSONObject overboreShape = properties.getJSONObject("overboreShape");
JSONObject overboreErosionStrength = properties.getJSONObject("overboreErosionStrength");
JSONObject overboreErosionFrequency = properties.getJSONObject("overboreErosionFrequency");
String overboreShapeDefinition = overboreShape.getString("$ref")
.substring("#/definitions/".length());
assertTrue(properties.has("structures"));
assertTrue(properties.has("distribution"));
assertEquals(List.of("BOX", "ROUNDED", "ERODED"), oneOfValues(
schema.getJSONObject("definitions"), overboreShapeDefinition));
assertEquals(0D, overboreErosionStrength.getDouble("minimum"), 0D);
assertEquals(1D, overboreErosionStrength.getDouble("maximum"), 0D);
assertEquals(0.001D, overboreErosionFrequency.getDouble("minimum"), 0D);
assertEquals(1D, overboreErosionFrequency.getDouble("maximum"), 0D);
assertEquals("object", stilt.getString("type"));
assertTrue(stiltProperties.has("palette"));
assertTrue(stiltProperties.has("supportNonOccluding"));
@@ -190,6 +201,15 @@ public class SchemaBuilderParityTest {
return values;
}
private static List<String> oneOfValues(JSONObject definitions, String key) {
JSONArray array = definitions.getJSONObject(key).getJSONArray("oneOf");
List<String> values = new ArrayList<>(array.length());
for (int index = 0; index < array.length(); index++) {
values.add(array.getJSONObject(index).getString("const"));
}
return values;
}
@Desc("Registry dependent model.")
public static class RegistryModel {
@Desc("Potion field.")
@@ -0,0 +1,102 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.InOrder;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
public class IrisCreatorTeleportTest {
@Test
public void createTeleportTarget_onlySelectsProductionPlayerSender() {
Player player = mock(Player.class);
VolmitSender playerSender = mock(VolmitSender.class);
VolmitSender consoleSender = mock(VolmitSender.class);
doReturn(true).when(playerSender).isPlayer();
doReturn(player).when(playerSender).player();
doReturn(false).when(consoleSender).isPlayer();
assertSame(player, IrisCreator.createTeleportTarget(playerSender, false, false));
assertNull(IrisCreator.createTeleportTarget(consoleSender, false, false));
assertNull(IrisCreator.createTeleportTarget(playerSender, true, false));
assertNull(IrisCreator.createTeleportTarget(playerSender, false, true));
}
@Test
public void teleportSenderToCreatedWorld_loadsAndResolvesSafeEntryBeforeTeleport() {
Player player = mock(Player.class);
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
Location anchor = new Location(world, 32.5D, 80D, -15.5D);
Location safeEntry = new Location(world, 32.5D, 94D, -15.5D);
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
doReturn(CompletableFuture.completedFuture(chunk))
.when(runtimeControl).requestChunkAsync(world, 2, -1, true);
doReturn(CompletableFuture.completedFuture(safeEntry))
.when(runtimeControl).resolveSafeEntry(world, anchor);
doReturn(CompletableFuture.completedFuture(true))
.when(runtimeControl).teleport(player, safeEntry);
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
assertTrue(result.join());
InOrder order = inOrder(runtimeControl);
order.verify(runtimeControl).resolveEntryAnchor(world);
order.verify(runtimeControl).requestChunkAsync(world, 2, -1, true);
order.verify(runtimeControl).resolveSafeEntry(world, anchor);
order.verify(runtimeControl).teleport(player, safeEntry);
}
@Test
public void teleportSenderToCreatedWorld_preservesFalseTeleportResult() {
Player player = mock(Player.class);
World world = mock(World.class);
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
Location anchor = new Location(world, 0.5D, 80D, 0.5D);
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
doReturn(CompletableFuture.completedFuture(mock(Chunk.class)))
.when(runtimeControl).requestChunkAsync(world, 0, 0, true);
doReturn(CompletableFuture.completedFuture(anchor))
.when(runtimeControl).resolveSafeEntry(world, anchor);
doReturn(CompletableFuture.completedFuture(false))
.when(runtimeControl).teleport(player, anchor);
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
assertFalse(result.join());
}
@Test
public void teleportSenderToCreatedWorld_failsWhenSafeEntryCannotResolve() {
Player player = mock(Player.class);
World world = mock(World.class);
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
Location anchor = new Location(world, 0.5D, 80D, 0.5D);
doReturn("irisworld").when(world).getName();
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
doReturn(CompletableFuture.completedFuture(mock(Chunk.class)))
.when(runtimeControl).requestChunkAsync(world, 0, 0, true);
doReturn(CompletableFuture.completedFuture(null))
.when(runtimeControl).resolveSafeEntry(world, anchor);
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
assertThrows(CompletionException.class, result::join);
}
}
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.NativeStructureSuppression;
import art.arcane.iris.engine.object.ObjectPlaceMode;
@@ -469,8 +470,41 @@ public class IrisStructureLocatorContractTest {
KList<PlacedStructurePiece> pieces = new KList<>();
pieces.add(piece(0, 60, 0, 1, 80, 1));
assertEquals(Integer.valueOf(-17), IrisStructureLocator.resolveUndergroundBurialShift(
assertEquals(Integer.valueOf(-14), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
placement.setOverboreErosionStrength(0D);
assertEquals(Integer.valueOf(-1), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
placement.setOverboreShape(IrisStructureCarveShape.ROUNDED);
assertEquals(Integer.valueOf(-1), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
placement.setOverboreHeight(0);
assertEquals(Integer.valueOf(0), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
}
@Test
public void zeroOverboreRadiusDoesNotExpandTheBurialEnvelope() {
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(164);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setUnderground(true)
.setMinHeight(-64)
.setMaxHeight(100)
.setOverbore(true)
.setOverboreShape(IrisStructureCarveShape.ROUNDED)
.setOverboreRadius(0)
.setOverboreHeight(0);
KList<PlacedStructurePiece> pieces = new KList<>();
pieces.add(piece(4, 60, 7, 4, 80, 7));
assertEquals(Integer.valueOf(0), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
verify(engine, times(1)).getHeight(4, 7, true);
}
@Test
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -8,47 +9,56 @@ import static org.junit.Assert.assertTrue;
public class IrisStructureComponentOverboreTest {
@Test
public void exactPieceInteriorAlwaysCarves() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.0, 0.0));
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.0, 1.0));
}
@Test
public void lowNoiseRemovesTheFormerFlatShell() {
double oneBlockAtRadiusSix = 1.0 / 6.0;
public void exactStructureFootprintAlwaysCarves() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
oneBlockAtRadiusSix * oneBlockAtRadiusSix, 0.0));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.25 * 0.25, 0.0));
IrisStructureCarveShape.ERODED, 0D, 0D, 1D));
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
IrisStructureCarveShape.ROUNDED, 0D, 0D, 1D));
}
@Test
public void maximumNoiseStopsAtConfiguredReach() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(1.0, 1.0));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(1.000001, 1.0));
public void boxModeKeepsStraightCandidateVolume() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
IrisStructureCarveShape.BOX, 100D, 0D, 1D));
}
@Test
public void diagonalCornersUseEuclideanFalloff() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(1.0, 1.0));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.8 * 0.8 + 0.8 * 0.8, 1.0));
public void roundedModeStopsAtConfiguredReach() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
IrisStructureCarveShape.ROUNDED, 1D, 0D, 1D));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(
IrisStructureCarveShape.ROUNDED, 1.000001D, 1D, 0D));
}
@Test
public void boundaryNoiseIsClampedAndDeterministic() {
assertEquals(0.2, IrisStructureComponent.overboreBoundaryLimit(-10.0), 0.0);
assertEquals(1.0, IrisStructureComponent.overboreBoundaryLimit(10.0), 0.0);
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.36, 0.5));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.360001, 0.5));
public void zeroErosionStrengthMatchesRoundedMode() {
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
IrisStructureCarveShape.ERODED, 1D, 0D, 0D));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(
IrisStructureCarveShape.ERODED, 1.000001D, 1D, 0D));
}
@Test
public void candidateExtensionsDoNotScanImpossibleOuterShell() {
assertEquals(6, IrisStructureComponent.overboreSideExtension(6));
assertEquals(18, IrisStructureComponent.overboreUpExtension(10.0));
public void zeroConfiguredCeilingHasNoErodedExtension() {
assertEquals(0D, IrisStructureComponent.erodedUpReach(null, 0.16D, 1D, 0, 0, 0), 0D);
}
long oldVolume = (41L + 18L) * (31L + 27L) * (35L + 18L);
long newVolume = (41L + 12L) * (31L + 18L) * (35L + 12L);
assertTrue(newVolume < oldVolume);
@Test
public void zeroErosionStrengthKeepsTheRoundedCeiling() {
assertEquals(10D, IrisStructureComponent.erodedUpReach(null, 0.16D, 0D, 0, 0, 10), 0D);
}
@Test
public void erosionStrengthAndNoiseAreClampedDeterministically() {
assertEquals(0.2D, IrisStructureComponent.overboreBoundaryLimit(-10D, 0.8D), 1.0E-12D);
assertEquals(1D, IrisStructureComponent.overboreBoundaryLimit(10D, 0.8D), 0D);
assertEquals(0.5D, IrisStructureComponent.overboreBoundaryLimit(0.5D, 1D), 0D);
assertEquals(1D, IrisStructureComponent.overboreBoundaryLimit(0D, -1D), 0D);
assertEquals(0D, IrisStructureComponent.overboreBoundaryLimit(0D, 10D), 0D);
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
null, 0.25D, 0.5D, 1D));
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(
null, 0.250001D, 0.5D, 1D));
}
@Test
@@ -0,0 +1,192 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class StructureCarvingFootprintTest {
@Test
public void preservesIrregularFootprintInsteadOfFillingItsBoundingBox() {
PlacedStructurePiece piece = piece(10, 40, 20, IrisObjectRotation.of(0, 0, 0),
block(0, 0, 0), block(1, 0, 0), block(2, 0, 0),
block(0, 0, 1), block(0, 0, 2));
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 0);
assertNotNull(footprint);
assertEquals(10, footprint.minX());
assertEquals(12, footprint.maxX());
assertEquals(20, footprint.minZ());
assertEquals(22, footprint.maxZ());
assertEquals(3, footprint.width());
assertEquals(3, footprint.depth());
assertEquals(0L, footprint.distanceSquaredAt(12, 20));
assertEquals(4L, footprint.distanceSquaredAt(12, 22));
}
@Test
public void reportsExactDistanceAndNearestColumnHeights() {
PlacedStructurePiece piece = piece(0, 0, 0, IrisObjectRotation.of(0, 0, 0),
block(0, 10, 0), block(0, 12, 0),
block(4, 30, 0), block(4, 35, 0));
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 2);
StructureCarvingFootprint.Column nearestRight = footprint.sample(3, 2);
StructureCarvingFootprint.Column tied = footprint.sample(2, 0);
assertNotNull(nearestRight);
assertEquals(5L, nearestRight.distanceSquared());
assertEquals(30, nearestRight.sourceMinY());
assertEquals(35, nearestRight.sourceMaxY());
assertNotNull(tied);
assertEquals(4L, tied.distanceSquared());
assertEquals(10, tied.sourceMinY());
assertEquals(12, tied.sourceMaxY());
}
@Test
public void rotatesAndMergesOverlappingColumns() {
PlacedStructurePiece rotated = piece(10, 20, 30, IrisObjectRotation.of(0, 90, 0),
block(2, 1, 0));
PlacedStructurePiece overlapping = piece(10, 5, 28, IrisObjectRotation.of(0, 0, 0),
block(0, 3, 0), block(0, 9, 0));
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(
pieces(rotated, overlapping), 0);
assertNotNull(footprint);
assertEquals(10, footprint.minX());
assertEquals(10, footprint.maxX());
assertEquals(28, footprint.minZ());
assertEquals(28, footprint.maxZ());
assertEquals(0L, footprint.distanceSquaredAt(10, 28));
assertEquals(8, footprint.sourceMinYAt(10, 28));
assertEquals(21, footprint.sourceMaxYAt(10, 28));
}
@Test
public void matchesBruteForceSquaredDistancesAcrossSparseColumns() {
int[][] sourceBlocks = new int[][]{
block(0, 2, 0), block(3, 4, 1), block(1, 6, 4), block(5, 8, 5)
};
PlacedStructurePiece piece = piece(
-8, 30, 11, IrisObjectRotation.of(0, 0, 0), sourceBlocks);
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 3);
assertNotNull(footprint);
for (int z = footprint.minZ(); z <= footprint.maxZ(); z++) {
for (int x = footprint.minX(); x <= footprint.maxX(); x++) {
long expected = Long.MAX_VALUE;
for (int[] source : sourceBlocks) {
long deltaX = (long) x - (piece.getX() + source[0]);
long deltaZ = (long) z - (piece.getZ() + source[2]);
expected = Math.min(expected, deltaX * deltaX + deltaZ * deltaZ);
}
assertEquals(expected, footprint.distanceSquaredAt(x, z));
}
}
}
@Test
public void returnsNullForEmptyInvalidOrOversizedInputs() {
assertNull(StructureCarvingFootprint.from(null, 0));
assertNull(StructureCarvingFootprint.from(new KList<>(), 0));
KList<PlacedStructurePiece> invalidPieces = new KList<>();
invalidPieces.add((PlacedStructurePiece) null);
invalidPieces.add(new PlacedStructurePiece(
null, null, 0, 0, 0, null, 0, 0, 0, 0, 0, 0));
assertNull(StructureCarvingFootprint.from(invalidPieces, 0));
PlacedStructurePiece piece = piece(0, 0, 0, IrisObjectRotation.of(0, 0, 0), block(0, 0, 0));
assertNull(StructureCarvingFootprint.from(pieces(piece), 2, 24));
PlacedStructurePiece overflowing = piece(
Integer.MAX_VALUE, 0, 0, IrisObjectRotation.of(0, 0, 0), block(1, 0, 0));
assertNull(StructureCarvingFootprint.from(pieces(overflowing), 0));
try {
StructureCarvingFootprint.from(pieces(piece), -1);
fail("Expected negative padding to fail");
} catch (IllegalArgumentException e) {
assertEquals("padding must be non-negative", e.getMessage());
}
}
@Test
public void includesTheEntirePaddingBoundary() {
PlacedStructurePiece piece = piece(7, 14, -4, IrisObjectRotation.of(0, 0, 0), block(0, 0, 0));
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 2, 25);
assertNotNull(footprint);
assertEquals(5, footprint.minX());
assertEquals(9, footprint.maxX());
assertEquals(-6, footprint.minZ());
assertEquals(-2, footprint.maxZ());
assertEquals(5, footprint.width());
assertEquals(5, footprint.depth());
assertTrue(footprint.contains(5, -6));
assertTrue(footprint.contains(9, -2));
assertFalse(footprint.contains(4, -6));
assertEquals(8L, footprint.distanceSquaredAt(5, -6));
assertNull(footprint.sample(4, -6));
}
@Test
public void explicitAirDoesNotExpandTheExteriorFootprint() {
IrisObject object = new IrisObject(1, 1, 1);
PlatformBlockState solid = mock(PlatformBlockState.class);
PlatformBlockState air = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true);
object.getBlocks().put(new IrisBlockVector(0, 0, 0), solid);
object.getBlocks().put(new IrisBlockVector(8, 4, 8), air);
PlacedStructurePiece piece = new PlacedStructurePiece(
null, object, 10, 20, 30, IrisObjectRotation.of(0, 0, 0),
10, 20, 30, 18, 24, 38);
StructureCarvingFootprint footprint = StructureCarvingFootprint.from(pieces(piece), 0);
assertNotNull(footprint);
assertEquals(10, footprint.minX());
assertEquals(10, footprint.maxX());
assertEquals(30, footprint.minZ());
assertEquals(30, footprint.maxZ());
}
private static PlacedStructurePiece piece(int x, int y, int z, IrisObjectRotation rotation,
int[]... blocks) {
IrisObject object = new IrisObject(1, 1, 1);
PlatformBlockState state = mock(PlatformBlockState.class);
for (int[] block : blocks) {
object.getBlocks().put(new IrisBlockVector(block[0], block[1], block[2]), state);
}
return new PlacedStructurePiece(
null, object, x, y, z, rotation, x, y, z, x, y, z);
}
private static KList<PlacedStructurePiece> pieces(PlacedStructurePiece... pieces) {
KList<PlacedStructurePiece> result = new KList<>();
for (PlacedStructurePiece piece : pieces) {
result.add(piece);
}
return result;
}
private static int[] block(int x, int y, int z) {
return new int[]{x, y, z};
}
}
@@ -0,0 +1,89 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import org.junit.Test;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class IrisStructurePlacementCarveSettingsTest {
@Test
public void defaultsSelectErodedOverbore() {
IrisStructurePlacement placement = new IrisStructurePlacement();
assertEquals(IrisStructureCarveShape.ERODED, placement.getOverboreShape());
assertEquals(IrisStructureCarveShape.ERODED, placement.resolvedOverboreShape());
assertEquals(0.8D, placement.getOverboreErosionStrength(), 0D);
assertEquals(0.8D, placement.resolvedOverboreErosionStrength(), 0D);
assertEquals(0.07D, placement.getOverboreErosionFrequency(), 0D);
assertEquals(0.07D, placement.resolvedOverboreErosionFrequency(), 0D);
}
@Test
public void carveShapesExposeTheirMaximumCeilingScale() {
assertEquals(1D, IrisStructureCarveShape.BOX.maximumCeilingScale(), 0D);
assertEquals(1D, IrisStructureCarveShape.ROUNDED.maximumCeilingScale(), 0D);
assertEquals(1.8D, IrisStructureCarveShape.ERODED.maximumCeilingScale(), 0D);
assertEquals(10, IrisStructureCarveShape.BOX.maximumCeilingExtension(10));
assertEquals(10, IrisStructureCarveShape.ROUNDED.maximumCeilingExtension(10));
assertEquals(18, IrisStructureCarveShape.ERODED.maximumCeilingExtension(10));
assertEquals(10, IrisStructureCarveShape.ERODED.maximumCeilingExtension(10, 0D));
assertEquals(14, IrisStructureCarveShape.ERODED.maximumCeilingExtension(10, 0.5D));
assertEquals(0, IrisStructureCarveShape.ERODED.maximumCeilingExtension(0));
}
@Test
public void nullAndNonFiniteValuesResolveToDefaults() {
IrisStructurePlacement placement = new IrisStructurePlacement()
.setOverboreShape(null)
.setOverboreErosionStrength(Double.NaN)
.setOverboreErosionFrequency(Double.POSITIVE_INFINITY);
assertEquals(IrisStructureCarveShape.ERODED, placement.resolvedOverboreShape());
assertEquals(0.8D, placement.resolvedOverboreErosionStrength(), 0D);
assertEquals(0.07D, placement.resolvedOverboreErosionFrequency(), 0D);
placement.setOverboreErosionStrength(Double.NEGATIVE_INFINITY);
placement.setOverboreErosionFrequency(Double.NaN);
assertEquals(0.8D, placement.resolvedOverboreErosionStrength(), 0D);
assertEquals(0.07D, placement.resolvedOverboreErosionFrequency(), 0D);
}
@Test
public void finiteValuesClampToAuthoredBounds() {
IrisStructurePlacement placement = new IrisStructurePlacement()
.setOverboreErosionStrength(-1D)
.setOverboreErosionFrequency(0D);
assertEquals(0D, placement.resolvedOverboreErosionStrength(), 0D);
assertEquals(0.001D, placement.resolvedOverboreErosionFrequency(), 0D);
placement.setOverboreErosionStrength(2D);
placement.setOverboreErosionFrequency(2D);
assertEquals(1D, placement.resolvedOverboreErosionStrength(), 0D);
assertEquals(1D, placement.resolvedOverboreErosionFrequency(), 0D);
}
@Test
public void numericSchemaBoundsMatchRuntimeBounds() throws NoSuchFieldException {
assertBounds("overboreErosionStrength", 0D, 1D);
assertBounds("overboreErosionFrequency", 0.001D, 1D);
}
private void assertBounds(String fieldName, double expectedMinimum,
double expectedMaximum) throws NoSuchFieldException {
Field field = IrisStructurePlacement.class.getDeclaredField(fieldName);
MinNumber minimum = field.getAnnotation(MinNumber.class);
MaxNumber maximum = field.getAnnotation(MaxNumber.class);
assertNotNull(minimum);
assertNotNull(maximum);
assertEquals(expectedMinimum, minimum.value(), 0D);
assertEquals(expectedMaximum, maximum.value(), 0D);
}
}