mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
True placement reach bound
- Add PlacementReach: centered half-extent reach from loaded object keys, scale and shrink chains, enumerated rotation kernels, translate, warp and vacuum - Size the object pass write radius and read radius from PlacementReach instead of full object width - Size floating reach from scaled and shrunk object spans - Count cave profile vacuum modes and upper-dimension effective rotation in the object reach - Keep the transition-band footprint check on its existing footprint - Add runner-backed and pack-wide reach tests
This commit is contained in:
1 parent
e2430500cf
commit
7aaf346ce3
7 files changed
+1549
-271
No files matched your search
+5
-20
@@ -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());
|
||||
}
|
||||
}
|
||||
+32
-52
@@ -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,407 @@
|
||||
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 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