mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Merged placement reach
- True object placement reach bounds shrink the carve window from 13 to 8 chunks
This commit is contained in:
commit
29e7e0384d
7 files changed
+1542
-253
No files matched your search
+42
-19
@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
|||||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
@@ -598,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
|||||||
@Override
|
@Override
|
||||||
protected int computeRadius() {
|
protected int computeRadius() {
|
||||||
int maxObjectExtent = 0;
|
int maxObjectExtent = 0;
|
||||||
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
|
Map<String, PlacementReach.Extent> extents = new HashMap<>();
|
||||||
Set<String> warnedLargeObjects = new HashSet<>();
|
Set<String> warnedLargeObjects = new HashSet<>();
|
||||||
try {
|
try {
|
||||||
IrisData data = getData();
|
IrisData data = getData();
|
||||||
@@ -608,15 +607,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisFloatingChildBiomes entry : entries) {
|
for (IrisFloatingChildBiomes entry : entries) {
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
|
PlacementScan scan = new PlacementScan(data, extents, warnedLargeObjects,
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
|
entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D);
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
|
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getFloatingObjects(), false));
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
|
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 {
|
try {
|
||||||
IrisBiome target = entry.getRealBiome(biome, data);
|
IrisBiome target = entry.getRealBiome(biome, data);
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
|
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveTopObjects(target), true));
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
|
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveBottomObjects(target), true));
|
||||||
} else {
|
} else {
|
||||||
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
||||||
}
|
}
|
||||||
@@ -635,9 +636,21 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
|||||||
return maxObjectExtent;
|
return maxObjectExtent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
|
private final class PlacementScan {
|
||||||
Map<String, IrisBlockVector> sizeCache,
|
private final IrisData data;
|
||||||
Set<String> warnedLargeObjects) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
|
||||||
if (placements == null) {
|
if (placements == null) {
|
||||||
return MINIMUM_PLACEMENT_RADIUS;
|
return MINIMUM_PLACEMENT_RADIUS;
|
||||||
}
|
}
|
||||||
@@ -649,18 +662,16 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
|||||||
if (placement == null || placement.getPlace() == null) {
|
if (placement == null || placement.getPlace() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
PlacementReach.Profile profile = PlacementReach.profile(placement);
|
||||||
|
double scale = placement.getMaximumScale(getDimension());
|
||||||
for (String objectKey : placement.getPlace()) {
|
for (String objectKey : placement.getPlace()) {
|
||||||
try {
|
try {
|
||||||
IrisBlockVector size = sizeCache.get(objectKey);
|
PlacementReach.Extent extent = extent(objectKey);
|
||||||
if (size == null) {
|
if (extent == null) {
|
||||||
File file = data.getObjectLoader().findFile(objectKey);
|
|
||||||
if (file == null) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
size = IrisObject.sampleSize(file);
|
PlacementReach.Extent scaled = extent.scaled(scale).scaled(shrink);
|
||||||
sizeCache.put(objectKey, size);
|
int reach = anchored ? profile.anchored(scaled).horizontal() : profile.of(scaled).horizontal();
|
||||||
}
|
|
||||||
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
|
|
||||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||||
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
||||||
}
|
}
|
||||||
@@ -673,6 +684,18 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
return radius;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class PlacementSequence {
|
private static final class PlacementSequence {
|
||||||
private int ordinal;
|
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.generation.subterrain.SubterrainPlan;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import art.arcane.iris.generation.context.ChunkContext;
|
import art.arcane.iris.generation.context.ChunkContext;
|
||||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
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 it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.iris.generation.block.DataProvider;
|
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.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -94,6 +93,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
private final ObjectSourcePlanCache sourcePlans;
|
private final ObjectSourcePlanCache sourcePlans;
|
||||||
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
|
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
|
||||||
private volatile int collisionRuleState;
|
private volatile int collisionRuleState;
|
||||||
|
private volatile int inputReach;
|
||||||
|
|
||||||
public MantleObjectComponent(EngineMantle engineMantle) {
|
public MantleObjectComponent(EngineMantle engineMantle) {
|
||||||
super(engineMantle, ReservedFlag.OBJECT, 2);
|
super(engineMantle, ReservedFlag.OBJECT, 2);
|
||||||
@@ -134,7 +134,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getInputRadius() {
|
public int getInputRadius() {
|
||||||
return calculateInputRadius(getRadius(), hasCollisionRules());
|
int radius = getRadius();
|
||||||
|
return calculateInputRadius(radius, inputReach, hasCollisionRules());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -297,11 +298,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
|
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);
|
int normalizedRadius = Math.max(0, radius);
|
||||||
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
|
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
|
||||||
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
|
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;
|
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);
|
int id = rng.i(0, Integer.MAX_VALUE);
|
||||||
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
|
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
|
||||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||||
placement.setRotation(buildUpsideDownRotation());
|
placement.setRotation(upsideDownRotation());
|
||||||
placement.setCarvingSupport(CarvingMode.ANYWHERE);
|
placement.setCarvingSupport(CarvingMode.ANYWHERE);
|
||||||
if (forcePlace) {
|
if (forcePlace) {
|
||||||
placement.setForcePlace(true);
|
placement.setForcePlace(true);
|
||||||
@@ -1408,7 +1409,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisObjectRotation buildUpsideDownRotation() {
|
private static IrisObjectRotation upsideDownRotation() {
|
||||||
IrisObjectRotation rt = new IrisObjectRotation();
|
IrisObjectRotation rt = new IrisObjectRotation();
|
||||||
rt.setEnabled(true);
|
rt.setEnabled(true);
|
||||||
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
|
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
|
||||||
@@ -1785,135 +1786,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
protected int computeRadius() {
|
protected int computeRadius() {
|
||||||
KSet<String> warnedLargeObjects = new KSet<>();
|
KSet<String> warnedLargeObjects = new KSet<>();
|
||||||
int radius = computeDimensionRadius(getDimension(), this::getData, warnedLargeObjects);
|
PlacementReach reach = new ReachScan(getDimension(), this::getData, warnedLargeObjects).dimension();
|
||||||
UpperDimensionContext upperContext = activeUpperContext();
|
UpperDimensionContext upperContext = activeUpperContext();
|
||||||
if (upperContext != null) {
|
if (upperContext != null) {
|
||||||
radius = Math.max(radius, computeDimensionRadius(
|
reach = reach.union(new ReachScan(upperContext.getDimension(), upperContext, warnedLargeObjects).upperDimension());
|
||||||
upperContext.getDimension(),
|
|
||||||
upperContext,
|
|
||||||
warnedLargeObjects
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return radius;
|
inputReach = reach.input();
|
||||||
}
|
return reach.horizontal();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean allowsObjectPlacement(
|
static boolean allowsObjectPlacement(
|
||||||
@@ -1926,9 +1805,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
if (complex == null || object == null) {
|
if (complex == null || object == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
int reach = calculatePlacementReach(
|
int reach = transitionFootprint(object, placement);
|
||||||
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
|
|
||||||
placement, 1D);
|
|
||||||
return complex.allowsNewGenerationFootprint(
|
return complex.allowsNewGenerationFootprint(
|
||||||
saturatedOffset(anchorX, -reach),
|
saturatedOffset(anchorX, -reach),
|
||||||
saturatedOffset(anchorZ, -reach),
|
saturatedOffset(anchorZ, -reach),
|
||||||
@@ -1936,20 +1813,34 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
saturatedOffset(anchorZ, reach));
|
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) {
|
private static int saturatedOffset(int coordinate, int offset) {
|
||||||
long result = (long) coordinate + offset;
|
long result = (long) coordinate + offset;
|
||||||
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
|
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int scaledDimension(int dimension, double scale) {
|
private static int transitionTranslation(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
|
||||||
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) {
|
|
||||||
if (translate == null || !translate.canTranslate()) {
|
if (translate == null || !translate.canTranslate()) {
|
||||||
return 0;
|
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));
|
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() {
|
private UpperDimensionContext activeUpperContext() {
|
||||||
if (!getDimension().isUpperDimensionObjects()) {
|
if (!getDimension().isUpperDimensionObjects()) {
|
||||||
return null;
|
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
|
@FunctionalInterface
|
||||||
interface SourceChunkConsumer {
|
interface SourceChunkConsumer {
|
||||||
void accept(int chunkX, int chunkZ);
|
void accept(int chunkX, int chunkZ);
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
/*
|
||||||
|
* 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 static int supportReadMargin(int surfaceSupportBuffer) {
|
||||||
|
return 1 + Math.max(0, surfaceSupportBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 boolean vertical;
|
||||||
|
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 = placement.isRotateTowardsSlope() || (rotating && kernels > MAXIMUM_ROTATION_KERNELS);
|
||||||
|
this.vertical = placement.isRotateTowardsSlope()
|
||||||
|
|| (rotating && (configuredRotation.canRotateX() || configuredRotation.canRotateZ()));
|
||||||
|
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 (radial) {
|
||||||
|
return radialReach(extent);
|
||||||
|
}
|
||||||
|
if (!rotating) {
|
||||||
|
return unrotatedReach(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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-20
@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
|
|||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||||
|
|
||||||
@Rule
|
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
|
public void transformedFloatingPlacementExpandsOwnerRadius() {
|
||||||
File objectFile = temporaryFolder.newFile("floating.iob");
|
|
||||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
|
||||||
output.writeInt(1);
|
|
||||||
output.writeInt(1);
|
|
||||||
output.writeInt(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||||
.setPlace(new KList<>("test/floating"))
|
.setPlace(new KList<>("test/floating"))
|
||||||
.setTranslate(new IrisObjectTranslate().setX(32));
|
.setTranslate(new IrisObjectTranslate().setX(-32));
|
||||||
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
|
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
|
||||||
.setFloatingObjects(new KList<>(placement));
|
.setFloatingObjects(new KList<>(placement));
|
||||||
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
|
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
|
||||||
@@ -69,7 +54,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
|||||||
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
|
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
|
||||||
|
|
||||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||||
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
|
when(objectLoader.load("test/floating")).thenReturn(new IrisObject(1, 1, 1));
|
||||||
IrisData data = mock(IrisData.class);
|
IrisData data = mock(IrisData.class);
|
||||||
when(data.getObjectLoader()).thenReturn(objectLoader);
|
when(data.getObjectLoader()).thenReturn(objectLoader);
|
||||||
|
|
||||||
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
|||||||
when(engineMantle.getEngine()).thenReturn(engine);
|
when(engineMantle.getEngine()).thenReturn(engine);
|
||||||
when(engineMantle.getData()).thenReturn(data);
|
when(engineMantle.getData()).thenReturn(data);
|
||||||
|
|
||||||
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||||
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
|
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
|
||||||
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
-52
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
|
||||||
import art.arcane.iris.generation.context.ChunkContext;
|
import art.arcane.iris.generation.context.ChunkContext;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
@@ -33,13 +32,8 @@ import art.arcane.volmlib.util.matter.Matter;
|
|||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
@ClassRule
|
@ClassRule
|
||||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||||
|
|
||||||
@Rule
|
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void bindPlatform() {
|
public void bindPlatform() {
|
||||||
IrisPlatforms.unbind();
|
IrisPlatforms.unbind();
|
||||||
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||||
|
|
||||||
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
|
assertEquals(27, PlacementReach.of(new IrisObject(5, 48, 7), placement, 1D).horizontal());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
.setMode(ObjectPlaceMode.VACUUM)
|
.setMode(ObjectPlaceMode.VACUUM)
|
||||||
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
|
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
|
||||||
|
|
||||||
assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
|
assertEquals(22, PlacementReach.of(new IrisObject(5, 4, 3), placement, placement.getMaximumScale(null)).horizontal());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
|
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
|
||||||
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
|
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
|
||||||
IrisObjectPlacement inherited = new IrisObjectPlacement();
|
IrisObjectPlacement inherited = new IrisObjectPlacement();
|
||||||
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
|
IrisObject object = new IrisObject(8, 8, 8);
|
||||||
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||||
inherited.getMaximumScale(dimension)));
|
|
||||||
dimension.setAllObjectScaleFactor(0.5D);
|
dimension.setAllObjectScaleFactor(0.5D);
|
||||||
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||||
inherited.getMaximumScale(dimension)));
|
|
||||||
inherited.setScale(new IrisObjectScale().setSize(1D));
|
inherited.setScale(new IrisObjectScale().setSize(1D));
|
||||||
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||||
inherited.getMaximumScale(dimension)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
|
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
|
||||||
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
|
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
|
||||||
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
|
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
|
||||||
|
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
|
||||||
assertEquals(Integer.MAX_VALUE,
|
assertEquals(Integer.MAX_VALUE,
|
||||||
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
|
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
for (int radius = 0; radius < 300; radius++) {
|
for (int radius = 0; radius < 300; radius++) {
|
||||||
for (boolean collisions : new boolean[]{false, true}) {
|
for (boolean collisions : new boolean[]{false, true}) {
|
||||||
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
|
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
|
||||||
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
|
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertEquals(Integer.MAX_VALUE,
|
assertEquals(Integer.MAX_VALUE,
|
||||||
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
when(engineMantle.getEngine()).thenReturn(engine);
|
when(engineMantle.getEngine()).thenReturn(engine);
|
||||||
when(engineMantle.getData()).thenReturn(data);
|
when(engineMantle.getData()).thenReturn(data);
|
||||||
|
|
||||||
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
|
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
|
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
|
||||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||||
|
.setPlace(new KList<>("upper/tree"))
|
||||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||||
placement.getForbiddenCollisions().add("upper/blocker");
|
placement.getForbiddenCollisions().add("upper/blocker");
|
||||||
IrisData baseData = mock(IrisData.class);
|
IrisData baseData = mock(IrisData.class);
|
||||||
IrisData upperData = mock(IrisData.class);
|
IrisData upperData = mock(IrisData.class);
|
||||||
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
|
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
|
||||||
when(proceduralPlacement.getVariantObjects(upperData))
|
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
|
||||||
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
|
when(upperData.getObjectLoader()).thenReturn(upperObjects);
|
||||||
when(proceduralPlacement.asPlacement()).thenReturn(placement);
|
|
||||||
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
|
|
||||||
when(proceduralObjects.isEmpty()).thenReturn(false);
|
|
||||||
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
|
|
||||||
IrisRegion upperRegion = mock(IrisRegion.class);
|
IrisRegion upperRegion = mock(IrisRegion.class);
|
||||||
when(upperRegion.getObjects()).thenReturn(new KList<>());
|
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
|
||||||
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
|
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
|
||||||
IrisDimension baseDimension = mock(IrisDimension.class);
|
IrisDimension baseDimension = mock(IrisDimension.class);
|
||||||
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
|
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
|
||||||
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
|
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
|
||||||
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
|
|
||||||
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
|
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
|
||||||
|
|
||||||
assertEquals(53, component.getRadius());
|
assertEquals(5, component.getRadius());
|
||||||
assertEquals(182, component.getInputRadius());
|
assertEquals(41, component.getInputRadius());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
|
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
|
||||||
File objectFile = temporaryFolder.newFile("tree.iob");
|
|
||||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
|
||||||
output.writeInt(1);
|
|
||||||
output.writeInt(1);
|
|
||||||
output.writeInt(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||||
.setPlace(new KList<>("test/tree"))
|
.setPlace(new KList<>("test/tree"))
|
||||||
.setTranslate(new IrisObjectTranslate().setX(32));
|
.setTranslate(new IrisObjectTranslate().setX(32));
|
||||||
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
|
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
|
||||||
|
|
||||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||||
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
|
when(objectLoader.load("test/tree")).thenReturn(new IrisObject(1, 1, 1));
|
||||||
IrisData data = mock(IrisData.class);
|
IrisData data = mock(IrisData.class);
|
||||||
when(data.getObjectLoader()).thenReturn(objectLoader);
|
when(data.getObjectLoader()).thenReturn(objectLoader);
|
||||||
|
|
||||||
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
when(engineMantle.getData()).thenReturn(data);
|
when(engineMantle.getData()).thenReturn(data);
|
||||||
|
|
||||||
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
||||||
assertEquals(33, component.getRadius());
|
assertEquals(32, component.getRadius());
|
||||||
assertEquals(0, component.getOutputRadius());
|
assertEquals(0, component.getOutputRadius());
|
||||||
assertEquals(82, component.getInputRadius());
|
assertEquals(68, component.getInputRadius());
|
||||||
|
|
||||||
Mantle<Matter> mantle = mock(Mantle.class);
|
Mantle<Matter> mantle = mock(Mantle.class);
|
||||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
|
|
||||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||||
|
|
||||||
assertEquals(49, directOrigins.get());
|
assertEquals(25, directOrigins.get());
|
||||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||||
assertEquals(49, directOrigins.get());
|
assertEquals(25, directOrigins.get());
|
||||||
|
|
||||||
List<String> generatedChunks = new ArrayList<>();
|
List<String> generatedChunks = new ArrayList<>();
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
component.getRadius(),
|
component.getRadius(),
|
||||||
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
||||||
);
|
);
|
||||||
assertEquals(49, replayedSources.size());
|
assertEquals(25, replayedSources.size());
|
||||||
assertEquals("-3,-3", replayedSources.getFirst());
|
assertEquals("-2,-2", replayedSources.getFirst());
|
||||||
assertEquals("-3,-2", replayedSources.get(1));
|
assertEquals("-2,-1", replayedSources.get(1));
|
||||||
assertEquals("3,3", replayedSources.getLast());
|
assertEquals("2,2", replayedSources.getLast());
|
||||||
|
|
||||||
placement.getForbiddenCollisions().add("test/tree");
|
placement.getForbiddenCollisions().add("test/tree");
|
||||||
component.hotload();
|
component.hotload();
|
||||||
assertEquals(130, component.getInputRadius());
|
assertEquals(100, component.getInputRadius());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TestMatterGenerator implements MatterGenerator {
|
private static final class TestMatterGenerator implements MatterGenerator {
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
package art.arcane.iris.generation.mantle;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
|
||||||
|
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.MeteredCache;
|
||||||
|
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||||
|
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
|
||||||
|
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.IrisObjectScale;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||||
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.math.Vector3i;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.Answers;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.withSettings;
|
||||||
|
|
||||||
|
public class PlacementReachPackTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||||
|
|
||||||
|
private static final int SPIN_PERIOD = 360;
|
||||||
|
private static final int MAXIMUM_CHECKED_KERNELS = 4096;
|
||||||
|
private static final String V2_HEADER = "Iris V2 IOB;";
|
||||||
|
|
||||||
|
private File pack;
|
||||||
|
private IrisData data;
|
||||||
|
private IrisDimension dimension;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void loadPack() throws IOException {
|
||||||
|
String configured = System.getenv("IRIS_TEST_PACK");
|
||||||
|
Assume.assumeTrue(configured != null && !configured.isBlank());
|
||||||
|
pack = new File(configured).getAbsoluteFile();
|
||||||
|
Assume.assumeTrue(new File(pack, "objects").isDirectory());
|
||||||
|
File[] dimensions = new File(pack, "dimensions").listFiles((folder, name) -> name.endsWith(".json"));
|
||||||
|
Assume.assumeTrue(dimensions != null && dimensions.length > 0);
|
||||||
|
bindPlatform(Files.createTempDirectory("iris-placement-reach").toFile());
|
||||||
|
data = IrisData.get(pack);
|
||||||
|
String dimensionKey = System.getenv().getOrDefault("IRIS_TEST_PACK_DIMENSION",
|
||||||
|
dimensions[0].getName().substring(0, dimensions[0].getName().length() - 5));
|
||||||
|
dimension = data.getDimensionLoader().load(dimensionKey);
|
||||||
|
assertTrue("dimension " + dimensionKey + " must load", dimension != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unbindPlatform() {
|
||||||
|
IrisServices.remove(PreservationRegistry.class);
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyObjectFileStaysInsideItsReachUnderRightAngleRotations() throws IOException {
|
||||||
|
List<IrisObjectPlacement> placements = List.of(
|
||||||
|
new IrisObjectPlacement(),
|
||||||
|
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||||
|
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp())),
|
||||||
|
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||||
|
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D))),
|
||||||
|
new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D)),
|
||||||
|
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||||
|
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()))
|
||||||
|
.setTranslate(new IrisObjectTranslate().setX(7).setZ(-3)));
|
||||||
|
List<Path> files;
|
||||||
|
try (Stream<Path> walk = Files.walk(new File(pack, "objects").toPath())) {
|
||||||
|
files = walk.filter(path -> path.toString().endsWith(".iob")).sorted().toList();
|
||||||
|
}
|
||||||
|
assertTrue(!files.isEmpty());
|
||||||
|
Statistics statistics = new Statistics();
|
||||||
|
NativeBlockState marker = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||||
|
for (Path file : files) {
|
||||||
|
IrisObject object = readPositions(file, marker);
|
||||||
|
for (IrisObjectPlacement placement : placements) {
|
||||||
|
verifyCentered(file.toString(), object, placement, PlacementReach.of(object, placement, 1D), statistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("PLACEMENT_REACH_FILES objects=" + files.size() + " " + statistics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyPackPlacementStaysInsideItsReach() {
|
||||||
|
Statistics statistics = new Statistics();
|
||||||
|
Set<IrisObjectPlacement> placements = new LinkedHashSet<>();
|
||||||
|
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||||
|
placements.addAll(region.getObjects());
|
||||||
|
}
|
||||||
|
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||||
|
placements.addAll(biome.getObjects());
|
||||||
|
}
|
||||||
|
for (IrisObjectPlacement placement : placements) {
|
||||||
|
for (String key : placement.getPlace()) {
|
||||||
|
IrisObject object = data.getObjectLoader().load(key);
|
||||||
|
if (object == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(dimension));
|
||||||
|
forEachVariant(object, placement, 1D, variant -> verifyCentered(key, variant, placement, reach, statistics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (IrisProceduralPlacement procedural : proceduralPlacements()) {
|
||||||
|
KList<IrisObject> variants = procedural.getVariantObjects(data);
|
||||||
|
if (variants == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisObjectPlacement placement = procedural.asPlacement();
|
||||||
|
for (IrisObject variant : variants) {
|
||||||
|
PlacementReach reach = PlacementReach.of(variant, placement, placement.getMaximumScale(dimension));
|
||||||
|
forEachVariant(variant, placement, 1D,
|
||||||
|
scaled -> verifyCentered(variant.getLoadKey(), scaled, placement, reach, statistics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(statistics.checked > 0);
|
||||||
|
System.out.println("PLACEMENT_REACH_PLACEMENTS placements=" + placements.size() + " " + statistics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void floatingPlacementsStayInsideTheirReach() {
|
||||||
|
Statistics statistics = new Statistics();
|
||||||
|
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||||
|
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
|
||||||
|
if (entries == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisFloatingChildBiomes entry : entries) {
|
||||||
|
double shrink = entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D;
|
||||||
|
IrisBiome target = entry.getRealBiome(biome, data);
|
||||||
|
for (IrisObjectPlacement placement : listOrEmpty(entry.getFloatingObjects())) {
|
||||||
|
verifyFloating(placement, shrink, false, statistics);
|
||||||
|
}
|
||||||
|
List<IrisObjectPlacement> anchored = new ArrayList<>();
|
||||||
|
anchored.addAll(listOrEmpty(entry.getExtraObjects()));
|
||||||
|
anchored.addAll(listOrEmpty(entry.getTopObjectOverrides()));
|
||||||
|
anchored.addAll(listOrEmpty(entry.getBottomObjectOverrides()));
|
||||||
|
if (target != null) {
|
||||||
|
anchored.addAll(listOrEmpty(entry.resolveTopObjects(target)));
|
||||||
|
anchored.addAll(listOrEmpty(entry.resolveBottomObjects(target)));
|
||||||
|
}
|
||||||
|
for (IrisObjectPlacement placement : anchored) {
|
||||||
|
verifyFloating(placement, shrink, true, statistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("PLACEMENT_REACH_FLOATING " + statistics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reportsTheMantleWindow() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||||
|
when(engineMantle.getEngine()).thenReturn(engine);
|
||||||
|
when(engineMantle.getData()).thenReturn(data);
|
||||||
|
MantleObjectComponent objects = new MantleObjectComponent(engineMantle);
|
||||||
|
MantleFloatingObjectComponent floating = new MantleFloatingObjectComponent(engineMantle);
|
||||||
|
int objectRadius = objects.getRadius();
|
||||||
|
int objectInput = objects.getInputRadius();
|
||||||
|
int objectEager = objects.getEagerInputRadius(objectInput);
|
||||||
|
int floatingRadius = floating.getRadius();
|
||||||
|
int floatingPassRadius = chunkRadius(floatingRadius);
|
||||||
|
int objectPassRadius = chunkRadius(floatingRadius);
|
||||||
|
int terrainRadius = chunkRadius(floatingRadius + objectEager + 1);
|
||||||
|
int writerRadius = 2 * chunkRadius(floatingRadius + objectInput + 1);
|
||||||
|
System.out.println("PLACEMENT_REACH_WINDOW objectReach=" + objectRadius
|
||||||
|
+ " objectInput=" + objectInput
|
||||||
|
+ " objectEager=" + objectEager
|
||||||
|
+ " sourceChunkRadius=" + MantleObjectComponent.sourceChunkRadius(objectRadius)
|
||||||
|
+ " floatingReach=" + floatingRadius
|
||||||
|
+ " floatingPassRadius=" + floatingPassRadius
|
||||||
|
+ " objectPassRadius=" + objectPassRadius
|
||||||
|
+ " terrainRadius=" + terrainRadius
|
||||||
|
+ " terrainWindow=" + ((2 * terrainRadius) + 1) + "x" + ((2 * terrainRadius) + 1)
|
||||||
|
+ " writerRadius=" + writerRadius);
|
||||||
|
assertTrue(objectRadius > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyFloating(IrisObjectPlacement placement, double shrink, boolean anchored, Statistics statistics) {
|
||||||
|
if (placement == null || placement.getPlace() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String key : placement.getPlace()) {
|
||||||
|
IrisObject object = data.getObjectLoader().load(key);
|
||||||
|
if (object == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PlacementReach.Extent extent = PlacementReach.Extent.of(object)
|
||||||
|
.scaled(placement.getMaximumScale(dimension)).scaled(shrink);
|
||||||
|
if (!anchored) {
|
||||||
|
PlacementReach reach = PlacementReach.of(extent, placement);
|
||||||
|
forEachVariant(object, placement, shrink, variant -> verifyCentered(key, variant, placement, reach, statistics));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PlacementReach reach = PlacementReach.anchored(extent, placement);
|
||||||
|
forEachVariant(object, placement, shrink, variant -> verifyAnchored(key, variant, reach, statistics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyAnchored(String key, IrisObject variant, PlacementReach reach, Statistics statistics) {
|
||||||
|
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(variant);
|
||||||
|
List<IrisBlockVector> positions = positions(variant);
|
||||||
|
List<IrisObjectRotation> rotations = List.of(IrisObjectRotation.of(0D, 0D, 0D),
|
||||||
|
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
|
||||||
|
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D));
|
||||||
|
for (IrisObjectRotation rotation : rotations) {
|
||||||
|
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
|
||||||
|
int actual = 0;
|
||||||
|
for (IrisBlockVector position : positions) {
|
||||||
|
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
|
||||||
|
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()));
|
||||||
|
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()));
|
||||||
|
}
|
||||||
|
record(key, actual, reach.horizontal(), statistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyCentered(String key, IrisObject object, IrisObjectPlacement placement, PlacementReach reach,
|
||||||
|
Statistics statistics) {
|
||||||
|
IrisObjectRotation rotation = placement.getRotation();
|
||||||
|
IrisObjectTranslate translate = placement.getTranslate();
|
||||||
|
boolean translating = translate != null && translate.canTranslate();
|
||||||
|
int[] spinsX = spins(rotation, 0);
|
||||||
|
int[] spinsY = spins(rotation, 1);
|
||||||
|
int[] spinsZ = spins(rotation, 2);
|
||||||
|
List<IrisBlockVector> positions = positions(object);
|
||||||
|
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
|
||||||
|
if (kernels > MAXIMUM_CHECKED_KERNELS) {
|
||||||
|
fail("rotation for " + key + " has " + kernels + " kernels");
|
||||||
|
}
|
||||||
|
for (int spinX : spinsX) {
|
||||||
|
for (int spinY : spinsY) {
|
||||||
|
for (int spinZ : spinsZ) {
|
||||||
|
IrisBlockVector offset = translating
|
||||||
|
? rotation.rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinX, spinY, spinZ)
|
||||||
|
: null;
|
||||||
|
int actual = 0;
|
||||||
|
for (IrisBlockVector position : positions) {
|
||||||
|
IrisBlockVector moved = rotation.rotate(position, spinX, spinY, spinZ).clone();
|
||||||
|
if (offset != null) {
|
||||||
|
moved.add(offset);
|
||||||
|
}
|
||||||
|
actual = Math.max(actual, Math.abs((int) Math.round(moved.getX())));
|
||||||
|
actual = Math.max(actual, Math.abs((int) Math.round(moved.getZ())));
|
||||||
|
}
|
||||||
|
record(key, actual, reach.horizontal(), statistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObject readPositions(Path file, NativeBlockState marker) throws IOException {
|
||||||
|
try (DataInputStream input = new DataInputStream(new BufferedInputStream(Files.newInputStream(file)))) {
|
||||||
|
IrisObject object = new IrisObject(input.readInt(), input.readInt(), input.readInt());
|
||||||
|
input.mark(Short.BYTES + V2_HEADER.length());
|
||||||
|
int headerLength = input.readUnsignedShort();
|
||||||
|
boolean v2 = headerLength == V2_HEADER.length()
|
||||||
|
&& V2_HEADER.equals(new String(input.readNBytes(headerLength), StandardCharsets.US_ASCII));
|
||||||
|
if (!v2) {
|
||||||
|
input.reset();
|
||||||
|
}
|
||||||
|
int paletteSize = v2 ? input.readShort() : 0;
|
||||||
|
for (int index = 0; index < paletteSize; index++) {
|
||||||
|
input.readUTF();
|
||||||
|
}
|
||||||
|
int blocks = input.readInt();
|
||||||
|
for (int index = 0; index < blocks; index++) {
|
||||||
|
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
|
||||||
|
if (v2) {
|
||||||
|
input.readShort();
|
||||||
|
} else {
|
||||||
|
input.readUTF();
|
||||||
|
}
|
||||||
|
object.getBlocks().put(position, marker);
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void record(String key, int actual, int reach, Statistics statistics) {
|
||||||
|
statistics.checked++;
|
||||||
|
statistics.maximumActual = Math.max(statistics.maximumActual, actual);
|
||||||
|
statistics.maximumReach = Math.max(statistics.maximumReach, reach);
|
||||||
|
statistics.slack += reach - actual;
|
||||||
|
if (actual > reach) {
|
||||||
|
fail(key + " places a block " + actual + " blocks from its origin, beyond its reach " + reach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<IrisBlockVector> positions(IrisObject object) {
|
||||||
|
List<IrisBlockVector> positions = new ArrayList<>(object.getBlocks().size() + 8);
|
||||||
|
object.getBlocks().forEach((position, state) -> positions.add(position));
|
||||||
|
Vector3i center = object.getCenter();
|
||||||
|
int[] xs = {-center.getBlockX(), object.getW() - center.getBlockX() - 1};
|
||||||
|
int[] ys = {-center.getBlockY(), object.getH() - center.getBlockY() - 1};
|
||||||
|
int[] zs = {-center.getBlockZ(), object.getD() - center.getBlockZ() - 1};
|
||||||
|
for (int x : xs) {
|
||||||
|
for (int y : ys) {
|
||||||
|
for (int z : zs) {
|
||||||
|
positions.add(new IrisBlockVector(x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forEachVariant(IrisObject object, IrisObjectPlacement placement, double shrink, Consumer<IrisObject> consumer) {
|
||||||
|
IrisObjectScale scale = placement.getScale();
|
||||||
|
IrisObjectPlacementScaleInterpolator interpolation = scale == null || scale.getInterpolation() == null
|
||||||
|
? IrisObjectPlacementScaleInterpolator.NONE : scale.getInterpolation();
|
||||||
|
for (double factor : scales(placement)) {
|
||||||
|
IrisObject variant = factor == 1D && (scale == null || !scale.shouldScale())
|
||||||
|
? object
|
||||||
|
: scale == null ? IrisObjectScale.getFixed(object, factor) : object.scaled(factor, interpolation);
|
||||||
|
consumer.accept(shrink < 1D ? variant.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE) : variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double[] scales(IrisObjectPlacement placement) {
|
||||||
|
IrisObjectScale scale = placement.getScale();
|
||||||
|
if (scale == null) {
|
||||||
|
return new double[]{dimension.getAllObjectScaleFactor()};
|
||||||
|
}
|
||||||
|
if (!scale.shouldScale()) {
|
||||||
|
return new double[]{1D};
|
||||||
|
}
|
||||||
|
if (scale.getSize() != 1D) {
|
||||||
|
return new double[]{scale.getSize()};
|
||||||
|
}
|
||||||
|
if (scale.getMinimumScale() == scale.getMaximumScale()) {
|
||||||
|
return new double[]{scale.getMinimumScale()};
|
||||||
|
}
|
||||||
|
int count = Math.max(1, Math.min(scale.getVariations(), 32));
|
||||||
|
double[] scales = new double[count];
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
scales[index] = scale.getMinimumScale() + (((scale.getMaximumScale() - scale.getMinimumScale()) / count) * index);
|
||||||
|
}
|
||||||
|
return scales;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IrisProceduralPlacement> proceduralPlacements() {
|
||||||
|
List<IrisProceduralPlacement> placements = new ArrayList<>();
|
||||||
|
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||||
|
addProcedural(placements, region.getProceduralObjects());
|
||||||
|
}
|
||||||
|
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||||
|
addProcedural(placements, biome.getProceduralObjects());
|
||||||
|
}
|
||||||
|
return placements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addProcedural(List<IrisProceduralPlacement> placements, IrisProceduralObjects procedural) {
|
||||||
|
if (procedural != null && !procedural.isEmpty()) {
|
||||||
|
placements.addAll(procedural.getAllPlacements());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] spins(IrisObjectRotation rotation, int axis) {
|
||||||
|
if (rotation == null || !rotation.canRotate()) {
|
||||||
|
return new int[]{0};
|
||||||
|
}
|
||||||
|
boolean rotates = switch (axis) {
|
||||||
|
case 0 -> rotation.canRotateX();
|
||||||
|
case 1 -> rotation.canRotateY();
|
||||||
|
default -> rotation.canRotateZ();
|
||||||
|
};
|
||||||
|
if (!rotates) {
|
||||||
|
return new int[]{0};
|
||||||
|
}
|
||||||
|
LongOpenHashSet angles = new LongOpenHashSet();
|
||||||
|
IntArrayList spins = new IntArrayList();
|
||||||
|
for (int spin = -SPIN_PERIOD + 1; spin < SPIN_PERIOD; spin++) {
|
||||||
|
double angle = switch (axis) {
|
||||||
|
case 0 -> rotation.getXRotation(spin);
|
||||||
|
case 1 -> rotation.getYRotation(spin);
|
||||||
|
default -> rotation.getZRotation(spin);
|
||||||
|
};
|
||||||
|
if (angles.add(Double.doubleToLongBits(angle))) {
|
||||||
|
spins.add(spin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spins.toIntArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> List<T> listOrEmpty(KList<T> list) {
|
||||||
|
return list == null ? List.of() : list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||||
|
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||||
|
rotation.setEnabled(true);
|
||||||
|
rotation.setXAxis(x);
|
||||||
|
rotation.setYAxis(y);
|
||||||
|
rotation.setZAxis(z);
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int chunkRadius(int blockRadius) {
|
||||||
|
return blockRadius > 0 ? Math.ceilDiv(blockRadius, 16) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindPlatform(File dataFolder) {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||||
|
when(registries.block(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||||
|
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||||
|
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||||
|
when(registries.air()).thenAnswer(invocation -> state(states, "minecraft:air"));
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
|
||||||
|
when(platform.dataFolder()).thenReturn(dataFolder);
|
||||||
|
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
|
||||||
|
Object[] path = invocation.getRawArguments();
|
||||||
|
return new File(dataFolder, String.join(File.separator, (String[]) path[0]));
|
||||||
|
});
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
IrisServices.register(PreservationRegistry.class, new IgnoredPreservation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeBlockState state(Map<String, NativeBlockState> states, String key) {
|
||||||
|
return states.computeIfAbsent(key, ignored -> {
|
||||||
|
String material = materialOf(key);
|
||||||
|
boolean air = material.endsWith(":air") || material.endsWith("cave_air") || material.endsWith("void_air");
|
||||||
|
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||||
|
when(state.key()).thenReturn(key);
|
||||||
|
when(state.materialKey()).thenReturn(material);
|
||||||
|
when(state.isAir()).thenReturn(air);
|
||||||
|
when(state.isSolid()).thenReturn(!air);
|
||||||
|
when(state.isOccluding()).thenReturn(!air);
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String materialOf(String key) {
|
||||||
|
String trimmed = key.trim().toLowerCase(Locale.ROOT);
|
||||||
|
int properties = trimmed.indexOf('[');
|
||||||
|
String head = properties < 0 ? trimmed : trimmed.substring(0, properties);
|
||||||
|
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class IgnoredPreservation implements PreservationRegistry {
|
||||||
|
@Override
|
||||||
|
public void register(Thread thread) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(ExecutorService service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCache(MeteredCache cache) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dereference() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Statistics {
|
||||||
|
private long checked;
|
||||||
|
private long slack;
|
||||||
|
private int maximumActual;
|
||||||
|
private int maximumReach;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "checked=" + checked + " maxActual=" + maximumActual + " maxReach=" + maximumReach
|
||||||
|
+ " meanSlack=" + (checked == 0 ? 0D : (double) slack / checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
package art.arcane.iris.generation.mantle;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.TileData;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||||
|
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||||
|
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||||
|
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||||
|
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||||
|
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
|
||||||
|
import art.arcane.iris.structure.object.IrisObject;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||||
|
import art.arcane.iris.structure.object.ObjectPlaceMode;
|
||||||
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.withSettings;
|
||||||
|
|
||||||
|
public class PlacementReachTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||||
|
|
||||||
|
private static final int ORIGIN_X = 1000;
|
||||||
|
private static final int ORIGIN_Y = 90;
|
||||||
|
private static final int ORIGIN_Z = -2000;
|
||||||
|
private static final int GROUND_Y = 64;
|
||||||
|
|
||||||
|
private final Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||||
|
private IrisData data;
|
||||||
|
private IrisData objectData;
|
||||||
|
private Engine engine;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||||
|
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||||
|
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||||
|
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||||
|
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
|
||||||
|
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
|
||||||
|
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
|
||||||
|
when(complex.getRockStream()).thenReturn(rock);
|
||||||
|
engine = mock(Engine.class, withSettings().stubOnly());
|
||||||
|
when(engine.getHeight()).thenReturn(256);
|
||||||
|
when(engine.getMinHeight()).thenReturn(0);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
data = mock(IrisData.class, withSettings().stubOnly());
|
||||||
|
when(data.getEngine()).thenReturn(engine);
|
||||||
|
objectData = mock(IrisData.class, withSettings().stubOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unbindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void halfExtentReachIsTheCenteredFrameNotTheFullWidth() {
|
||||||
|
IrisObject object = singleBlockObject(103, 75, 73);
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||||
|
|
||||||
|
assertEquals(51, PlacementReach.of(object, placement, 1D).horizontal());
|
||||||
|
assertEquals(54, PlacementReach.of(object, placement, 1D).input());
|
||||||
|
assertEquals(51, PlacementReach.of(object, placement.setRotation(IrisObjectRotation.of(0D, 0D, 0D)), 1D).horizontal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void arbitraryAngleRotationUsesTheRotatedFrame() {
|
||||||
|
IrisObject object = singleBlockObject(21, 5, 21);
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||||
|
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()));
|
||||||
|
|
||||||
|
assertEquals(14, PlacementReach.of(object, placement, 1D).horizontal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void slopeRotationIsBoundedEvenWhenTheConfiguredRotationIsDisabled() {
|
||||||
|
IrisObject object = singleBlockObject(21, 9, 21);
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D));
|
||||||
|
|
||||||
|
assertEquals(10, PlacementReach.of(object, placement, 1D).horizontal());
|
||||||
|
assertEquals(15, PlacementReach.of(object, placement.setRotateTowardsSlope(true), 1D).horizontal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unionKeepsTheLargestReachAndTheLargestInput() {
|
||||||
|
PlacementReach union = new PlacementReach(10, 2).union(new PlacementReach(4, 12));
|
||||||
|
|
||||||
|
assertEquals(10, union.horizontal());
|
||||||
|
assertEquals(16, union.input());
|
||||||
|
assertEquals(19, union.withReadMargin(9).input());
|
||||||
|
assertEquals(16, union.withReadMargin(1).input());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scaledSpanBoundsEveryScaledVariant() {
|
||||||
|
Random random = new Random(91L);
|
||||||
|
double[] scales = {0.01D, 0.25D, 0.45D, 0.5D, 0.75D, 0.95D, 1D, 1.3D, 1.5D, 2D, 2.6D, 3.7D, 4D};
|
||||||
|
for (int trial = 0; trial < 120; trial++) {
|
||||||
|
int outside = trial % 3 == 0 ? 1 + random.nextInt(3) : 0;
|
||||||
|
IrisObject object = randomObject(random, outside);
|
||||||
|
PlacementReach.Extent extent = PlacementReach.Extent.of(object);
|
||||||
|
for (double scale : scales) {
|
||||||
|
for (IrisObjectPlacementScaleInterpolator interpolation : List.of(IrisObjectPlacementScaleInterpolator.NONE,
|
||||||
|
IrisObjectPlacementScaleInterpolator.TRILINEAR)) {
|
||||||
|
if (interpolation != IrisObjectPlacementScaleInterpolator.NONE && scale > 2D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisObject scaled = object.scaled(scale, interpolation);
|
||||||
|
for (double maximum : new double[]{scale, Math.min(IrisObjectScale.MAXIMUM_FACTOR, scale * 1.4D)}) {
|
||||||
|
assertContains(extent.scaled(maximum), scaled, "scale " + scale + " bounded at " + maximum);
|
||||||
|
for (double shrink : new double[]{0.45D, 0.8D}) {
|
||||||
|
assertContains(extent.scaled(maximum).scaled(shrink),
|
||||||
|
scaled.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE),
|
||||||
|
"scale " + scale + " shrink " + shrink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void runnerWritesAndReadsStayInsideTheReach() {
|
||||||
|
Random random = new Random(1337L);
|
||||||
|
for (int trial = 0; trial < 260; trial++) {
|
||||||
|
IrisObject object = randomObject(random, trial % 5 == 0 ? 2 : 0);
|
||||||
|
object.setLoader(objectData);
|
||||||
|
IrisObjectPlacement placement = randomPlacement(random);
|
||||||
|
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(null));
|
||||||
|
for (int seed = 0; seed < 12; seed++) {
|
||||||
|
RNG rng = new RNG((trial * 7919L) + seed);
|
||||||
|
IrisObject variant = placement.getScale() == null ? object : placement.getScale().get(rng, object);
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
variant.place(ORIGIN_X, ORIGIN_Y, ORIGIN_Z, placer, placement, rng, null, null, data);
|
||||||
|
String context = "trial " + trial + " seed " + seed + " mode " + placement.getMode()
|
||||||
|
+ " rotation " + placement.getRotation() + " translate " + placement.getTranslate()
|
||||||
|
+ " scale " + placement.getScale() + " warp " + placement.getWarp().getMultiplier();
|
||||||
|
assertTrue(context + " writes " + placer.writeReach + " > " + reach.horizontal(),
|
||||||
|
placer.writeReach <= reach.horizontal());
|
||||||
|
assertTrue(context + " reads " + placer.readReach + " > " + reach.input(),
|
||||||
|
placer.readReach <= reach.input());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchoredReachCoversTheFloatingFootprintAnchor() {
|
||||||
|
Random random = new Random(7L);
|
||||||
|
for (int trial = 0; trial < 80; trial++) {
|
||||||
|
IrisObject object = randomObject(random, trial % 4 == 0 ? 1 : 0);
|
||||||
|
PlacementReach reach = PlacementReach.anchored(PlacementReach.Extent.of(object), new IrisObjectPlacement());
|
||||||
|
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(object);
|
||||||
|
for (IrisObjectRotation rotation : List.of(IrisObjectRotation.of(0D, 0D, 0D),
|
||||||
|
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
|
||||||
|
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D))) {
|
||||||
|
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
|
||||||
|
object.getBlocks().forEach((position, state) -> {
|
||||||
|
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
|
||||||
|
assertTrue(Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()) <= reach.horizontal());
|
||||||
|
assertTrue(Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()) <= reach.horizontal());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObjectPlacement randomPlacement(Random random) {
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||||
|
placement.setRotation(randomRotation(random));
|
||||||
|
if (random.nextInt(3) > 0) {
|
||||||
|
placement.setTranslate(new IrisObjectTranslate()
|
||||||
|
.setX(random.nextInt(41) - 20)
|
||||||
|
.setY(random.nextInt(21) - 10)
|
||||||
|
.setZ(random.nextInt(41) - 20));
|
||||||
|
}
|
||||||
|
if (random.nextInt(3) == 0) {
|
||||||
|
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(9)));
|
||||||
|
}
|
||||||
|
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||||
|
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG,
|
||||||
|
ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH, ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC,
|
||||||
|
ObjectPlaceMode.VACUUM_WAVY, ObjectPlaceMode.STRUCTURE_PIECE);
|
||||||
|
placement.setMode(modes.get(random.nextInt(modes.size())));
|
||||||
|
if (random.nextInt(4) == 0) {
|
||||||
|
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(16)));
|
||||||
|
}
|
||||||
|
if (random.nextInt(4) == 0) {
|
||||||
|
placement.setStiltSettings(new IrisStiltSettings());
|
||||||
|
}
|
||||||
|
placement.setBore(random.nextInt(4) == 0);
|
||||||
|
placement.setBoreExtendMaxY(random.nextInt(3));
|
||||||
|
placement.setSnow(random.nextInt(4) == 0 ? 0.6D : 0D);
|
||||||
|
placement.setSurfaceSupportBuffer(random.nextInt(5));
|
||||||
|
switch (random.nextInt(5)) {
|
||||||
|
case 0 -> placement.setScale(new IrisObjectScale().setSize(0.3D + (random.nextDouble() * 1.6D)));
|
||||||
|
case 1 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.5D).setMaximumScale(2.3D)
|
||||||
|
.setVariations(5).setInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR));
|
||||||
|
case 2 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.4D).setMaximumScale(0.9D).setVariations(3));
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObjectRotation randomRotation(Random random) {
|
||||||
|
return switch (random.nextInt(9)) {
|
||||||
|
case 0 -> new IrisObjectRotation();
|
||||||
|
case 1 -> IrisObjectRotation.of(0D, 0D, 0D);
|
||||||
|
case 2 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp());
|
||||||
|
case 3 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp());
|
||||||
|
case 4 -> IrisObjectRotation.of(90D, 0D, 0D);
|
||||||
|
case 5 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||||
|
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
|
||||||
|
case 6 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 0D),
|
||||||
|
new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp(true, false, 0D, 0D, 30D));
|
||||||
|
case 7 -> IrisObjectRotation.of(30D, 135D, 0D);
|
||||||
|
default -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 20D, 110D, 15D),
|
||||||
|
new IrisAxisRotationClamp(true, true, 180D, 180D, 0D));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject randomObject(Random random, int outside) {
|
||||||
|
int width = 1 + random.nextInt(18);
|
||||||
|
int height = 1 + random.nextInt(14);
|
||||||
|
int depth = 1 + random.nextInt(18);
|
||||||
|
IrisObject object = new IrisObject(width, height, depth);
|
||||||
|
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 3));
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
int x = random.nextInt(width) - (width / 2);
|
||||||
|
int y = random.nextInt(height) - (height / 2);
|
||||||
|
int z = random.nextInt(depth) - (depth / 2);
|
||||||
|
object.getBlocks().put(new IrisBlockVector(x, y, z), state(random.nextInt(5) == 0 ? "minecraft:oak_leaves" : "minecraft:stone"));
|
||||||
|
}
|
||||||
|
for (int index = 0; index < outside; index++) {
|
||||||
|
int x = random.nextInt(width + 10) - ((width + 10) / 2);
|
||||||
|
int y = random.nextInt(height + 6) - ((height + 6) / 2);
|
||||||
|
int z = random.nextInt(depth + 10) - ((depth + 10) / 2);
|
||||||
|
object.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject singleBlockObject(int width, int height, int depth) {
|
||||||
|
IrisObject object = new IrisObject(width, height, depth);
|
||||||
|
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertContains(PlacementReach.Extent extent, IrisObject object, String context) {
|
||||||
|
object.getBlocks().forEach((position, state) -> {
|
||||||
|
assertTrue(context + " x " + position.getBlockX() + " outside " + extent.x(),
|
||||||
|
position.getBlockX() >= extent.x().minimum() && position.getBlockX() <= extent.x().maximum());
|
||||||
|
assertTrue(context + " y " + position.getBlockY() + " outside " + extent.y(),
|
||||||
|
position.getBlockY() >= extent.y().minimum() && position.getBlockY() <= extent.y().maximum());
|
||||||
|
assertTrue(context + " z " + position.getBlockZ() + " outside " + extent.z(),
|
||||||
|
position.getBlockZ() >= extent.z().minimum() && position.getBlockZ() <= extent.z().maximum());
|
||||||
|
});
|
||||||
|
int centerX = object.getCenter().getBlockX();
|
||||||
|
int centerZ = object.getCenter().getBlockZ();
|
||||||
|
assertTrue(context + " frame x", -centerX >= extent.x().minimum() && object.getW() - 1 - centerX <= extent.x().maximum());
|
||||||
|
assertTrue(context + " frame z", -centerZ >= extent.z().minimum() && object.getD() - 1 - centerZ <= extent.z().maximum());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||||
|
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||||
|
rotation.setEnabled(true);
|
||||||
|
rotation.setXAxis(x);
|
||||||
|
rotation.setYAxis(y);
|
||||||
|
rotation.setZAxis(z);
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeBlockState state(String key) {
|
||||||
|
return states.computeIfAbsent(key, ignored -> {
|
||||||
|
String material = key.toLowerCase(Locale.ROOT);
|
||||||
|
boolean air = material.endsWith("air");
|
||||||
|
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||||
|
when(state.key()).thenReturn(key);
|
||||||
|
when(state.materialKey()).thenReturn(material);
|
||||||
|
when(state.isAir()).thenReturn(air);
|
||||||
|
when(state.isSolid()).thenReturn(!air);
|
||||||
|
when(state.isOccluding()).thenReturn(!air);
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingPlacer implements IObjectPlacer {
|
||||||
|
private final Engine engine;
|
||||||
|
private int writeReach;
|
||||||
|
private int readReach;
|
||||||
|
|
||||||
|
private RecordingPlacer(Engine engine) {
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(int x, int z) {
|
||||||
|
writeReach = Math.max(writeReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
|
||||||
|
read(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void read(int x, int z) {
|
||||||
|
readReach = Math.max(readReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHighest(int x, int z, IrisData data) {
|
||||||
|
read(x, z);
|
||||||
|
return GROUND_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||||
|
read(x, z);
|
||||||
|
return GROUND_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int x, int y, int z, NativeBlockState d) {
|
||||||
|
write(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NativeBlockState get(int x, int y, int z) {
|
||||||
|
read(x, z);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPreventingDecay() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCarved(int x, int y, int z) {
|
||||||
|
read(x, z);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSolid(int x, int y, int z) {
|
||||||
|
read(x, z);
|
||||||
|
return y < GROUND_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUnderwater(int x, int z) {
|
||||||
|
read(x, z);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getFluidHeight() {
|
||||||
|
return GROUND_Y - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDebugSmartBore() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||||
|
write(xx, zz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||||
|
write(xx, zz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||||
|
read(xx, zz);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine getEngine() {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user