True placement reach bound

- Add PlacementReach: centered half-extent reach from loaded object keys, scale and shrink chains, enumerated rotation kernels, translate, warp and vacuum
- Size the object pass write radius and read radius from PlacementReach instead of full object width
- Size floating reach from scaled and shrunk object spans
- Count cave profile vacuum modes and upper-dimension effective rotation in the object reach
- Keep the transition-band footprint check on its existing footprint
- Add runner-backed and pack-wide reach tests
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 20:24:46 -04:00
1 parent e2430500cf
commit 7aaf346ce3
7 files changed
+1549 -271

No files matched your search

@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -598,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override
protected int computeRadius() {
int maxObjectExtent = 0;
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
Map<String, PlacementReach.Extent> extents = new HashMap<>();
Set<String> warnedLargeObjects = new HashSet<>();
try {
IrisData data = getData();
@@ -608,15 +607,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
PlacementScan scan = new PlacementScan(data, extents, warnedLargeObjects,
entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D);
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getFloatingObjects(), false));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getExtraObjects(), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getTopObjectOverrides(), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getBottomObjectOverrides(), true));
try {
IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) {
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveTopObjects(target), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveBottomObjects(target), true));
} else {
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
}
@@ -635,44 +636,66 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return maxObjectExtent;
}
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
Map<String, IrisBlockVector> sizeCache,
Set<String> warnedLargeObjects) {
if (placements == null) {
return MINIMUM_PLACEMENT_RADIUS;
private final class PlacementScan {
private final IrisData data;
private final Map<String, PlacementReach.Extent> extents;
private final Set<String> warnedLargeObjects;
private final double shrink;
private PlacementScan(IrisData data, Map<String, PlacementReach.Extent> extents, Set<String> warnedLargeObjects,
double shrink) {
this.data = data;
this.extents = extents;
this.warnedLargeObjects = warnedLargeObjects;
this.shrink = shrink;
}
if (placements.isEmpty()) {
return 0;
}
int radius = MINIMUM_PLACEMENT_RADIUS;
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
if (placements == null) {
return MINIMUM_PLACEMENT_RADIUS;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = sizeCache.get(objectKey);
if (size == null) {
File file = data.getObjectLoader().findFile(objectKey);
if (file == null) {
if (placements.isEmpty()) {
return 0;
}
int radius = MINIMUM_PLACEMENT_RADIUS;
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
}
PlacementReach.Profile profile = PlacementReach.profile(placement);
double scale = placement.getMaximumScale(getDimension());
for (String objectKey : placement.getPlace()) {
try {
PlacementReach.Extent extent = extent(objectKey);
if (extent == null) {
continue;
}
size = IrisObject.sampleSize(file);
sizeCache.put(objectKey, size);
PlacementReach.Extent scaled = extent.scaled(scale).scaled(shrink);
int reach = anchored ? profile.anchored(scaled).horizontal() : profile.of(scaled).horizontal();
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
return radius;
}
private PlacementReach.Extent extent(String objectKey) {
if (extents.containsKey(objectKey)) {
return extents.get(objectKey);
}
IrisObject object = data.getObjectLoader().load(objectKey);
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
extents.put(objectKey, extent);
return extent;
}
return radius;
}
private static final class PlacementSequence {
private int ordinal;
@@ -54,7 +54,6 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -69,12 +68,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.DataProvider;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -94,6 +93,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private final ObjectSourcePlanCache sourcePlans;
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
private volatile int collisionRuleState;
private volatile int inputReach;
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 2);
@@ -134,7 +134,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public int getInputRadius() {
return calculateInputRadius(getRadius(), hasCollisionRules());
int radius = getRadius();
return calculateInputRadius(radius, inputReach, hasCollisionRules());
}
@Override
@@ -297,11 +298,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) {
static int calculateInputRadius(int radius, int inputReach, boolean sourceAnchoredCollisions) {
int normalizedRadius = Math.max(0, radius);
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + normalizedRadius + 1L;
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + Math.max(normalizedRadius, inputReach) + 1L;
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
@@ -1370,7 +1371,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
placement.setRotation(buildUpsideDownRotation());
placement.setRotation(upsideDownRotation());
placement.setCarvingSupport(CarvingMode.ANYWHERE);
if (forcePlace) {
placement.setForcePlace(true);
@@ -1408,7 +1409,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private IrisObjectRotation buildUpsideDownRotation() {
private static IrisObjectRotation upsideDownRotation() {
IrisObjectRotation rt = new IrisObjectRotation();
rt.setEnabled(true);
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
@@ -1785,135 +1786,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
protected int computeRadius() {
KSet<String> warnedLargeObjects = new KSet<>();
int radius = computeDimensionRadius(getDimension(), this::getData, warnedLargeObjects);
PlacementReach reach = new ReachScan(getDimension(), this::getData, warnedLargeObjects).dimension();
UpperDimensionContext upperContext = activeUpperContext();
if (upperContext != null) {
radius = Math.max(radius, computeDimensionRadius(
upperContext.getDimension(),
upperContext,
warnedLargeObjects
));
reach = reach.union(new ReachScan(upperContext.getDimension(), upperContext, warnedLargeObjects).upperDimension());
}
return radius;
}
private int computeDimensionRadius(
IrisDimension dimension,
DataProvider dataProvider,
KSet<String> warnedLargeObjects
) {
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
int radius = 0;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(
region.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(
biome.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
}
return radius;
}
private int computePlacementRadius(
KList<IrisObjectPlacement> placements,
IrisDimension dimension,
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
KSet<String> warnedLargeObjects
) {
int radius = 0;
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = loadObjectSize(dataProvider, sizeCache, objectKey);
if (size == null) {
continue;
}
int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
}
return radius;
}
private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
if (procedural == null || procedural.isEmpty()) {
return 0;
}
int radius = 0;
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
}
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement, objectPlacement.getMaximumScale(dimension)));
}
}
return radius;
}
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
if (size == null) {
return 0;
}
if (placement == null) {
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
}
int width = scaledDimension(size.getBlockX(), scale);
int height = scaledDimension(size.getBlockY(), scale);
int depth = scaledDimension(size.getBlockZ(), scale);
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
inputReach = reach.input();
return reach.horizontal();
}
static boolean allowsObjectPlacement(
@@ -1926,9 +1805,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (complex == null || object == null) {
return true;
}
int reach = calculatePlacementReach(
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
placement, 1D);
int reach = transitionFootprint(object, placement);
return complex.allowsNewGenerationFootprint(
saturatedOffset(anchorX, -reach),
saturatedOffset(anchorZ, -reach),
@@ -1936,20 +1813,34 @@ public class MantleObjectComponent extends IrisMantleComponent {
saturatedOffset(anchorZ, reach));
}
private static int transitionFootprint(IrisObject object, IrisObjectPlacement placement) {
int width = Math.abs(object.getW());
int height = Math.abs(object.getH());
int depth = Math.abs(object.getD());
if (placement == null) {
return Math.max(width, depth);
}
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = transitionTranslation(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension);
if (scale == 1D) {
return absoluteDimension;
}
return (int) Math.ceil(absoluteDimension * scale);
}
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
private static int transitionTranslation(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
if (translate == null || !translate.canTranslate()) {
return 0;
}
@@ -1963,22 +1854,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z));
}
private IrisBlockVector loadObjectSize(
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
String objectKey
) {
return sizeCache.computeIfAbsent(objectKey, k -> {
try {
return IrisObject.sampleSize(dataProvider.getData().getObjectLoader().findFile(objectKey));
} catch (IOException e) {
IrisLogging.reportError(e);
}
return null;
});
}
private UpperDimensionContext activeUpperContext() {
if (!getDimension().isUpperDimensionObjects()) {
return null;
@@ -2047,6 +1922,187 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private static final class ReachScan {
private final IrisDimension dimension;
private final DataProvider dataProvider;
private final KSet<String> warnedLargeObjects;
private final Map<String, PlacementReach.Extent> extents;
private final Set<ObjectPlaceMode> caveVacuumModes;
private ReachScan(IrisDimension dimension, DataProvider dataProvider, KSet<String> warnedLargeObjects) {
this.dimension = dimension;
this.dataProvider = dataProvider;
this.warnedLargeObjects = warnedLargeObjects;
this.extents = new HashMap<>();
this.caveVacuumModes = caveVacuumModes(dimension, dataProvider);
}
private PlacementReach dimension() {
PlacementReach reach = PlacementReach.NONE;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
reach = reach.union(placements(region.getObjects())).union(procedural(region.getProceduralObjects()));
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
reach = reach.union(placements(biome.getObjects())).union(procedural(biome.getProceduralObjects()));
}
}
return reach.withReadMargin(PlacementReach.supportReadMargin(dimension.getObjectSurfaceSupportBuffer()));
}
private PlacementReach upperDimension() {
PlacementReach reach = PlacementReach.NONE;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
reach = reach.union(upperPlacements(region.getSurfaceObjects()));
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
reach = reach.union(upperPlacements(biome.getSurfaceObjects()));
}
}
return reach;
}
private PlacementReach placements(KList<IrisObjectPlacement> placements) {
PlacementReach reach = PlacementReach.NONE;
if (placements == null) {
return reach;
}
for (IrisObjectPlacement placement : placements) {
if (placement != null) {
reach = reach.union(objects(placement, profiles(placement)));
}
}
return reach;
}
private PlacementReach upperPlacements(KList<IrisObjectPlacement> placements) {
PlacementReach reach = PlacementReach.NONE;
if (placements == null) {
return reach;
}
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
IrisObjectPlacement upper = placement.toPlacement(placement.getPlace().toArray(new String[0]));
upper.setMode(ObjectPlaceMode.CENTER_HEIGHT);
upper.setRotation(upsideDownRotation());
upper.setCarvingSupport(CarvingMode.ANYWHERE);
reach = reach.union(objects(placement, new KList<>(PlacementReach.profile(upper))));
}
return reach;
}
private PlacementReach objects(IrisObjectPlacement placement, KList<PlacementReach.Profile> profiles) {
PlacementReach reach = PlacementReach.NONE;
double scale = placement.getMaximumScale(dimension);
for (String objectKey : placement.getPlace()) {
try {
PlacementReach.Extent extent = extent(objectKey);
if (extent == null) {
continue;
}
PlacementReach.Extent scaled = extent.scaled(scale);
for (PlacementReach.Profile profile : profiles) {
PlacementReach objectReach = profile.of(scaled);
if (objectReach.horizontal() > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + objectReach.horizontal() + " blocks) and may increase memory usage!");
}
reach = reach.union(objectReach);
}
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
return reach;
}
private PlacementReach procedural(IrisProceduralObjects procedural) {
PlacementReach reach = PlacementReach.NONE;
if (procedural == null || procedural.isEmpty()) {
return reach;
}
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
}
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
KList<PlacementReach.Profile> profiles = profiles(objectPlacement);
double scale = objectPlacement.getMaximumScale(dimension);
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
PlacementReach.Extent extent = PlacementReach.Extent.of(variant).scaled(scale);
for (PlacementReach.Profile profile : profiles) {
reach = reach.union(profile.of(extent));
}
}
}
return reach;
}
private KList<PlacementReach.Profile> profiles(IrisObjectPlacement placement) {
KList<PlacementReach.Profile> profiles = new KList<>(PlacementReach.profile(placement));
if (placement.getMode() != ObjectPlaceMode.CENTER_HEIGHT
|| placement.getCarvingSupport() == CarvingMode.SURFACE_ONLY) {
return profiles;
}
for (ObjectPlaceMode mode : caveVacuumModes) {
IrisObjectPlacement cave = placement.toPlacement(placement.getPlace().toArray(new String[0]));
cave.setMode(mode);
profiles.add(PlacementReach.profile(cave));
}
return profiles;
}
private PlacementReach.Extent extent(String objectKey) {
if (extents.containsKey(objectKey)) {
return extents.get(objectKey);
}
IrisObject object = dataProvider.getData().getObjectLoader().load(objectKey);
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
extents.put(objectKey, extent);
return extent;
}
private static Set<ObjectPlaceMode> caveVacuumModes(IrisDimension dimension, DataProvider dataProvider) {
Set<ObjectPlaceMode> modes = EnumSet.noneOf(ObjectPlaceMode.class);
addCaveVacuumMode(modes, dimension.getCaveProfile());
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
addCaveVacuumMode(modes, region.getCaveProfile());
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
addCaveVacuumMode(modes, biome.getCaveProfile());
}
}
return modes;
}
private static void addCaveVacuumMode(Set<ObjectPlaceMode> modes, IrisCaveProfile profile) {
if (profile == null) {
return;
}
ObjectPlaceMode mode = profile.getDefaultObjectPlaceMode();
if (mode != null && IrisObjectVacuum.isVacuumMode(mode)) {
modes.add(mode);
}
}
}
@FunctionalInterface
interface SourceChunkConsumer {
void accept(int chunkX, int chunkZ);
@@ -0,0 +1,310 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectVacuum;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.Vector3i;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
public record PlacementReach(int horizontal, int readMargin) {
public static final PlacementReach NONE = new PlacementReach(0, 0);
private static final int SPIN_PERIOD = 360;
private static final int MAXIMUM_ROTATION_KERNELS = 1024;
private static final double ROUNDING_SLACK = 1.0E-9D;
public PlacementReach {
if (horizontal < 0 || readMargin < 0) {
throw new IllegalArgumentException("Placement reach must be non-negative: " + horizontal + "+" + readMargin);
}
}
public static PlacementReach of(IrisObject object, IrisObjectPlacement placement, double scale) {
return of(Extent.of(object).scaled(scale), placement);
}
public static PlacementReach of(Extent extent, IrisObjectPlacement placement) {
return profile(placement).of(extent);
}
public static PlacementReach anchored(Extent extent, IrisObjectPlacement placement) {
return profile(placement).anchored(extent);
}
public static Profile profile(IrisObjectPlacement placement) {
return new Profile(Objects.requireNonNull(placement, "placement"));
}
public int input() {
return saturate((long) horizontal + readMargin);
}
public PlacementReach union(PlacementReach other) {
int unionHorizontal = Math.max(horizontal, other.horizontal);
int unionInput = Math.max(input(), other.input());
return new PlacementReach(unionHorizontal, unionInput - unionHorizontal);
}
public PlacementReach withReadMargin(int minimumReadMargin) {
return minimumReadMargin <= readMargin ? this : new PlacementReach(horizontal, minimumReadMargin);
}
public static int supportReadMargin(int surfaceSupportBuffer) {
return 1 + Math.max(0, surfaceSupportBuffer);
}
private static int roundedBound(double maximumMagnitude) {
return saturate((long) Math.floor(maximumMagnitude + 0.5D + ROUNDING_SLACK));
}
private static int saturate(long value) {
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
}
private static IrisBlockVector corner(Extent extent, int corner) {
return new IrisBlockVector(
(corner & 1) == 0 ? extent.x().minimum() : extent.x().maximum(),
(corner & 2) == 0 ? extent.y().minimum() : extent.y().maximum(),
(corner & 4) == 0 ? extent.z().minimum() : extent.z().maximum());
}
public static final class Profile {
private final IrisObjectRotation rotation;
private final IrisBlockVector translation;
private final boolean rotating;
private final boolean radial;
private final int[] spinsX;
private final int[] spinsY;
private final int[] spinsZ;
private final int additive;
private final int readMargin;
private Profile(IrisObjectPlacement placement) {
IrisObjectRotation configuredRotation = placement.getRotation();
IrisObjectTranslate translate = placement.getTranslate();
this.rotation = configuredRotation;
this.translation = translate != null && translate.canTranslate()
? new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ())
: null;
this.rotating = configuredRotation != null && configuredRotation.canRotate();
this.spinsX = rotating ? distinctSpins(configuredRotation, Axis.X) : new int[]{0};
this.spinsY = rotating ? distinctSpins(configuredRotation, Axis.Y) : new int[]{0};
this.spinsZ = rotating ? distinctSpins(configuredRotation, Axis.Z) : new int[]{0};
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
this.radial = rotating && (placement.isRotateTowardsSlope() || kernels > MAXIMUM_ROTATION_KERNELS);
this.additive = saturate((long) warpReach(placement.getWarp()) + vacuumReach(placement));
this.readMargin = supportReadMargin(placement.getSurfaceSupportBuffer());
}
public PlacementReach of(Extent extent) {
Objects.requireNonNull(extent, "extent");
return new PlacementReach(saturate((long) rotatedReach(extent) + additive), readMargin);
}
public PlacementReach anchored(Extent extent) {
Objects.requireNonNull(extent, "extent");
long span = Math.max(extent.x().span(), extent.z().span());
return new PlacementReach(saturate(span + additive), readMargin);
}
private int rotatedReach(Extent extent) {
if (!rotating) {
return unrotatedReach(extent);
}
if (radial) {
return radialReach(extent);
}
double maximum = 0D;
for (int spinX : spinsX) {
for (int spinY : spinsY) {
for (int spinZ : spinsZ) {
maximum = Math.max(maximum, rotatedCornerMaximum(extent, spinX, spinY, spinZ));
}
}
}
return roundedBound(maximum);
}
private double rotatedCornerMaximum(Extent extent, int spinX, int spinY, int spinZ) {
IrisBlockVector offset = translation == null ? null : rotation.rotate(translation, spinX, spinY, spinZ);
double maximum = 0D;
for (int corner = 0; corner < 8; corner++) {
IrisBlockVector rotated = rotation.rotate(corner(extent, corner), spinX, spinY, spinZ);
double x = offset == null ? rotated.getX() : rotated.getX() + offset.getX();
double z = offset == null ? rotated.getZ() : rotated.getZ() + offset.getZ();
maximum = Math.max(maximum, Math.max(Math.abs(x), Math.abs(z)));
}
return maximum;
}
private int unrotatedReach(Extent extent) {
double translateX = translation == null ? 0D : translation.getX();
double translateZ = translation == null ? 0D : translation.getZ();
double x = Math.max(Math.abs(extent.x().minimum() + translateX), Math.abs(extent.x().maximum() + translateX));
double z = Math.max(Math.abs(extent.z().minimum() + translateZ), Math.abs(extent.z().maximum() + translateZ));
return roundedBound(Math.max(x, z));
}
private int radialReach(Extent extent) {
boolean vertical = rotation.canRotateX() || rotation.canRotateZ();
double translateX = translation == null ? 0D : translation.getX();
double translateY = translation == null ? 0D : translation.getY();
double translateZ = translation == null ? 0D : translation.getZ();
double maximum = 0D;
for (int corner = 0; corner < 8; corner++) {
IrisBlockVector position = corner(extent, corner);
double x = position.getX() + translateX;
double y = vertical ? position.getY() + translateY : 0D;
double z = position.getZ() + translateZ;
maximum = Math.max(maximum, Math.sqrt((x * x) + (y * y) + (z * z)));
}
return roundedBound(maximum);
}
private static int warpReach(IrisGeneratorStyle warp) {
if (warp == null || warp.isFlat()) {
return 0;
}
return saturate((long) Math.ceil(Math.abs(warp.getMultiplier()) / 2D));
}
private static int vacuumReach(IrisObjectPlacement placement) {
if (!IrisObjectVacuum.isVacuumMode(placement.getMode())) {
return 0;
}
return IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings());
}
private static int[] distinctSpins(IrisObjectRotation rotation, Axis axis) {
boolean rotates = switch (axis) {
case X -> rotation.canRotateX();
case Y -> rotation.canRotateY();
case Z -> rotation.canRotateZ();
};
if (!rotates) {
return new int[]{0};
}
LongOpenHashSet angles = new LongOpenHashSet();
IntArrayList spins = new IntArrayList();
for (int spin = 0; spin < SPIN_PERIOD; spin++) {
double angle = switch (axis) {
case X -> rotation.getXRotation(spin);
case Y -> rotation.getYRotation(spin);
case Z -> rotation.getZRotation(spin);
};
if (angles.add(Double.doubleToLongBits(angle))) {
spins.add(spin);
}
}
return spins.toIntArray();
}
}
private enum Axis {
X,
Y,
Z
}
public record Extent(Span x, Span y, Span z) {
public Extent {
Objects.requireNonNull(x, "x");
Objects.requireNonNull(y, "y");
Objects.requireNonNull(z, "z");
}
public static Extent of(IrisObject object) {
Objects.requireNonNull(object, "object");
Vector3i center = object.getCenter();
int[] keys = keyBounds(object.getBlocks());
return new Extent(
Span.of(object.getW(), center.getBlockX(), keys[0], keys[3]),
Span.of(object.getH(), center.getBlockY(), keys[1], keys[4]),
Span.of(object.getD(), center.getBlockZ(), keys[2], keys[5]));
}
public Extent scaled(double scale) {
if (scale == 1D) {
return this;
}
return new Extent(x.scaled(scale), y.scaled(scale), z.scaled(scale));
}
private static int[] keyBounds(VectorMap<NativeBlockState> blocks) {
int[] bounds = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
while (cursor.next()) {
IrisBlockVector key = cursor.key();
bounds[0] = Math.min(bounds[0], key.getBlockX());
bounds[1] = Math.min(bounds[1], key.getBlockY());
bounds[2] = Math.min(bounds[2], key.getBlockZ());
bounds[3] = Math.max(bounds[3], key.getBlockX());
bounds[4] = Math.max(bounds[4], key.getBlockY());
bounds[5] = Math.max(bounds[5], key.getBlockZ());
}
return bounds;
}
}
public record Span(int size, int center, int minimum, int maximum, boolean framed) {
public Span {
if (minimum > -center || maximum < size - 1 - center) {
throw new IllegalArgumentException("Span must cover the object frame: " + minimum + ".." + maximum
+ " for size " + size + " centered at " + center);
}
}
public static Span of(int size, int center, int keyMinimum, int keyMaximum) {
int frameMinimum = -center;
int frameMaximum = size - 1 - center;
boolean framed = keyMinimum >= frameMinimum && keyMaximum <= frameMaximum;
return new Span(size, center, Math.min(keyMinimum, frameMinimum), Math.max(keyMaximum, frameMaximum), framed);
}
public int span() {
return maximum - minimum;
}
public Span scaled(double scale) {
int scaledSize = (int) Math.ceil(size * scale);
int scaledCenter = scaledSize / 2;
int frameMinimum = -scaledCenter;
int frameMaximum = scaledSize - 1 - scaledCenter;
if (framed) {
return new Span(scaledSize, scaledCenter, frameMinimum, frameMaximum, true);
}
int scaledMinimum = (int) Math.floor((minimum + center) * scale) - scaledCenter;
double centerExcess = Math.max(0D, center - (size / 2D));
int scaledMaximum = (int) Math.ceil(((maximum + 1 + centerExcess) * scale) + 0.5D + ROUNDING_SLACK) - 1;
return new Span(scaledSize, scaledCenter, Math.min(scaledMinimum, frameMinimum),
Math.max(scaledMaximum, frameMaximum), false);
}
}
}