mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +00:00
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:
1 parent
e2430500cf
commit
7aaf346ce3
7 files changed
+1549
-271
No files matched your search
+60
-37
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user