Merged placement reach

- True object placement reach bounds shrink the carve window from 13 to 8 chunks
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 21:47:25 -04:00
commit 29e7e0384d
7 files changed
+1560 -271

No files matched your search

@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Rule;
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.mockito.Mockito.mock;
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
@SuppressWarnings("unchecked")
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
File objectFile = temporaryFolder.newFile("floating.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void transformedFloatingPlacementExpandsOwnerRadius() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/floating"))
.setTranslate(new IrisObjectTranslate().setX(32));
.setTranslate(new IrisObjectTranslate().setX(-32));
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
.setFloatingObjects(new KList<>(placement));
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));
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);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
}
}
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
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.Before;
import org.junit.ClassRule;
import org.junit.Rule;
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.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setRotation(IrisObjectRotation.of(90, 0, 0))
.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
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setMode(ObjectPlaceMode.VACUUM)
.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
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
IrisObjectPlacement inherited = new IrisObjectPlacement();
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
IrisObject object = new IrisObject(8, 8, 8);
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
dimension.setAllObjectScaleFactor(0.5D);
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
inherited.setScale(new IrisObjectScale().setSize(1D));
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
}
@Test
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
@Test
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
assertEquals(Integer.MAX_VALUE,
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
}
@Test
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
for (int radius = 0; radius < 300; radius++) {
for (boolean collisions : new boolean[]{false, true}) {
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
}
}
assertEquals(Integer.MAX_VALUE,
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
}
@Test
@SuppressWarnings("unchecked")
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("upper/tree"))
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
placement.getForbiddenCollisions().add("upper/blocker");
IrisData baseData = mock(IrisData.class);
IrisData upperData = mock(IrisData.class);
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
when(proceduralPlacement.getVariantObjects(upperData))
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
when(proceduralPlacement.asPlacement()).thenReturn(placement);
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
when(proceduralObjects.isEmpty()).thenReturn(false);
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
when(upperData.getObjectLoader()).thenReturn(upperObjects);
IrisRegion upperRegion = mock(IrisRegion.class);
when(upperRegion.getObjects()).thenReturn(new KList<>());
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
IrisDimension baseDimension = mock(IrisDimension.class);
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
assertEquals(53, component.getRadius());
assertEquals(182, component.getInputRadius());
assertEquals(5, component.getRadius());
assertEquals(41, component.getInputRadius());
}
@Test
@SuppressWarnings("unchecked")
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
File objectFile = temporaryFolder.newFile("tree.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/tree"))
.setTranslate(new IrisObjectTranslate().setX(32));
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
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);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
assertEquals(33, component.getRadius());
assertEquals(32, component.getRadius());
assertEquals(0, component.getOutputRadius());
assertEquals(82, component.getInputRadius());
assertEquals(68, component.getInputRadius());
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
List<String> generatedChunks = new ArrayList<>();
doAnswer(invocation -> {
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.getRadius(),
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
);
assertEquals(49, replayedSources.size());
assertEquals("-3,-3", replayedSources.getFirst());
assertEquals("-3,-2", replayedSources.get(1));
assertEquals("3,3", replayedSources.getLast());
assertEquals(25, replayedSources.size());
assertEquals("-2,-2", replayedSources.getFirst());
assertEquals("-2,-1", replayedSources.get(1));
assertEquals("2,2", replayedSources.getLast());
placement.getForbiddenCollisions().add("test/tree");
component.hotload();
assertEquals(130, component.getInputRadius());
assertEquals(100, component.getInputRadius());
}
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;
}
}
}